diff --git a/README.md b/README.md index 193508d3..aebb0d30 100644 --- a/README.md +++ b/README.md @@ -102,46 +102,112 @@ you can run: `TEST_DATABASE_URL=postgres://... TOX_TESTENV_PASSENV=TEST_DATABASE ## Configuration for repositories -Repositories scanned by the Zoo may contain the `.zoo.yml` file. This file contains additional +Repositories scanned by the Zoo may contain the `.entities.yml` file. This file contains additional information about the scanned repository and how and where it's used in production. If the Zoo -finds this file in the root of the project, it will read it and create a Service or a Library within -the Zoo with the provided information based on the collected data automatically. -Otherwise the data have to be added to the Zoo manually. +finds this file in the root of the project, it will read it and create specified Entities within +the Zoo with the provided information based on the collected data automatically. Currently The Zoo +is supporting 3 types of Entities: + +- Base Entity - schema can be found in [component_base.py](zoo/entities/yaml_definitions/component_base.yaml) +- Service Entity - schema can be found in [component_service.py](zoo/entities/yaml_definitions/component_service.yaml) +- Library Entity - schema can be found in [component_library.py](zoo/entities/yaml_definitions/component_library.yaml) Here is an example of this file: ```yaml -type: service -name: hello-world-service -owner: booking -impact: profit -status: beta -docs_url: 'https://example.com/hello-world-service' -slack_channel: 'http://example.com/slack/channel' -sentry_project: 'http://example.com/sentry/hello-world-service' -sonarqube_project: hello-world-service -pagerduty_url: 'https://example.com/pager/hello-world-service' -tags: - - tag1 - - tag2 - - tag3 -environments: - - - name: staging - dashboard_url: 'https://staging.example.com/dashboard' - service_urls: - - 'https://staging.example.com/service' - health_check_url: 'https://staging.example.com/health_check' - - - name: production - dashboard_url: 'https://production.example.com/dashboard' - service_urls: - - 'https://production.example.com/service' - health_check_url: 'https://production.example.com/health_check' +apiVersion: v1alpha1 +kind: component +metadata: + name: zoo-prod-db + label: The Zoo Production Database + owner: platform-software + group: + product_owner: John + project_owner: Doe + maintainers: + - thor@avengers.com + - hulk@avengers.com + description: Production DB for The Zoo + tags: + - Django + - Production + - Service Catalog + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: datadog + - name: Sentry + url: https://sentry.kiwi.com +spec: + type: database +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: the-zoo + label: The Zoo + owner: platform-software + group: + product_owner: John + project_owner: Doe + maintainers: + - loki@avengers.com + - iron-man@avengers.com + description: Application providing service catalog and much more + tags: + - Zoo + - catalog + - production + links: + - name: Datadog + url: https://datadog.zoo.com +spec: + type: service + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://zoo.health.com + service_urls: + - https://zoo.com + - name: sandbox + dashboard_url: https://dashboard.datadog.sandbox.com + health_check_url: https://zoo.health.sandbox.com + service_urls: + - https://zoo.sandbox.com + impact: employees + integrations: + pagerduty_service: pagerduty_service1234 + sentry_project: sentry project 15234 + lifecycle: production +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: superfast-sort + label: Super Fast Sort + owner: platform-software + group: + product_owner: Sort + project_owner: Maker + maintainers: + - sort@maker.com + description: Library providing super fast sort + tags: + - Sort + - Python + links: + - name: Datadog + url: https://dashboard.datadog.com + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: library + impact: employees + integrations: + sonarqube_project: sonarqube + lifecycle: production ``` -Full schema for this file can be found in [zoo_yml.py](zoo/repos/zoo_yml.py) - ## Documentation ### Architecture Decision Records diff --git a/test/api/query/snapshots/snap_test_entities.py b/test/api/query/snapshots/snap_test_entities.py new file mode 100644 index 00000000..8b74be36 --- /dev/null +++ b/test/api/query/snapshots/snap_test_entities.py @@ -0,0 +1,704 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_all 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component', + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy service', + 'id': 'RW50aXR5OjEyNDU=', + 'kind': None, + 'label': 'Service', + 'name': 'service', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy library', + 'id': 'RW50aXR5OjE1NTQ1', + 'kind': None, + 'label': 'Library', + 'name': 'library', + 'owner': 'platform', + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_empty 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + ], + 'totalCount': 0 + } + } +} + +snapshots['test_first 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component', + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_first_after 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': True, + 'startCursor': 'Mg==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_last 1'] = { + 'data': { + 'allEntities': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': 'Pagination "last" works only in combination with "before" argument.', + 'path': [ + 'allEntities' + ] + } + ] +} + +snapshots['test_last_before 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 5 + } + } +} + +snapshots['test_with_group 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component', + 'group': { + 'id': 'R3JvdXA6MTY=', + 'maintainers': [ + ], + 'productOwner': 'joseph32', + 'projectOwner': 'omclean' + }, + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 2', + 'group': { + 'id': 'R3JvdXA6MTc=', + 'maintainers': [ + ], + 'productOwner': 'tateandrew', + 'projectOwner': 'mark24' + }, + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'group': { + 'id': 'R3JvdXA6MTg=', + 'maintainers': [ + ], + 'productOwner': 'anelson', + 'projectOwner': 'richard32' + }, + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy service', + 'group': { + 'id': 'R3JvdXA6MTk=', + 'maintainers': [ + ], + 'productOwner': 'ofowler', + 'projectOwner': 'qkline' + }, + 'id': 'RW50aXR5OjEyNDU=', + 'kind': None, + 'label': 'Service', + 'name': 'service', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy library', + 'group': { + 'id': 'R3JvdXA6MjA=', + 'maintainers': [ + ], + 'productOwner': 'martha97', + 'projectOwner': 'susan27' + }, + 'id': 'RW50aXR5OjE1NTQ1', + 'kind': None, + 'label': 'Library', + 'name': 'library', + 'owner': 'platform', + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_with_library 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component', + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'library': None, + 'name': 'base', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'library': None, + 'name': 'base_2', + 'owner': 'platformm2', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'library': None, + 'name': 'base_3', + 'owner': 'platformm3', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy service', + 'id': 'RW50aXR5OjEyNDU=', + 'kind': None, + 'label': 'Service', + 'library': None, + 'name': 'service', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy library', + 'id': 'RW50aXR5OjE1NTQ1', + 'kind': None, + 'label': 'Library', + 'library': { + 'docsUrl': None, + 'id': 'TGlicmFyeTo1', + 'impact': 'profit', + 'libraryUrl': None, + 'lifecycle': 'beta', + 'name': 'taylor', + 'owner': 'brownnathaniel', + 'slackChannel': 'robbins', + 'sonarqubeProject': 'sloan-cook' + }, + 'name': 'library', + 'owner': 'platform', + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_with_links 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'allLinks': { + 'edges': [ + { + 'node': { + 'icon': 'poop', + 'name': 'Datadog', + 'url': 'https://dashboard.datadog.com' + } + } + ], + 'pageInfo': { + 'endCursor': 'MQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 1 + }, + 'description': 'This is my fancy component', + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'allLinks': { + 'edges': [ + { + 'node': { + 'icon': None, + 'name': 'Sentry', + 'url': 'https://sentry.skypicker.com' + } + } + ], + 'pageInfo': { + 'endCursor': 'MQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 1 + }, + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'type': None + } + }, + { + 'node': { + 'allLinks': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 0 + }, + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'type': None + } + }, + { + 'node': { + 'allLinks': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 0 + }, + 'description': 'This is my fancy service', + 'id': 'RW50aXR5OjEyNDU=', + 'kind': None, + 'label': 'Service', + 'name': 'service', + 'owner': 'platform', + 'type': None + } + }, + { + 'node': { + 'allLinks': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 0 + }, + 'description': 'This is my fancy library', + 'id': 'RW50aXR5OjE1NTQ1', + 'kind': None, + 'label': 'Library', + 'name': 'library', + 'owner': 'platform', + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_with_service 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component', + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'service': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'service': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'service': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy service', + 'id': 'RW50aXR5OjEyNDU=', + 'kind': None, + 'label': 'Service', + 'name': 'service', + 'owner': 'platform', + 'service': { + 'docsUrl': 'https://docs.com', + 'id': 'U2VydmljZToz', + 'impact': 'employees', + 'lifecycle': 'production', + 'owner': 'platform', + 'pagerdutyService': None, + 'slackChannel': '#platform-software' + }, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy library', + 'id': 'RW50aXR5OjE1NTQ1', + 'kind': None, + 'label': 'Library', + 'name': 'library', + 'owner': 'platform', + 'service': None, + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_with_source 1'] = { + 'data': { + 'allEntities': { + 'edges': [ + { + 'node': { + 'description': 'This is my fancy component', + 'id': 'RW50aXR5OjE=', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'source': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 2', + 'id': 'RW50aXR5OjI=', + 'kind': None, + 'label': 'Base 2', + 'name': 'base_2', + 'owner': 'platformm2', + 'source': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy component 3', + 'id': 'RW50aXR5OjMy', + 'kind': None, + 'label': 'Base 3', + 'name': 'base_3', + 'owner': 'platformm3', + 'source': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy service', + 'id': 'RW50aXR5OjEyNDU=', + 'kind': None, + 'label': 'Service', + 'name': 'service', + 'owner': 'platform', + 'source': None, + 'type': None + } + }, + { + 'node': { + 'description': 'This is my fancy library', + 'id': 'RW50aXR5OjE1NTQ1', + 'kind': None, + 'label': 'Library', + 'name': 'library', + 'owner': 'platform', + 'source': None, + 'type': None + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} diff --git a/test/api/query/snapshots/snap_test_groups.py b/test/api/query/snapshots/snap_test_groups.py new file mode 100644 index 00000000..c7d6a039 --- /dev/null +++ b/test/api/query/snapshots/snap_test_groups.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_all 1'] = { + 'data': { + 'allGroups': { + 'edges': [ + { + 'node': { + 'id': 'R3JvdXA6MQ==', + 'maintainers': [ + 'clark', + 'kent' + ], + 'productOwner': 'john', + 'projectOwner': 'doe' + } + }, + { + 'node': { + 'id': 'R3JvdXA6MjQ=', + 'maintainers': [ + 'black', + 'smith' + ], + 'productOwner': 'black', + 'projectOwner': 'smith' + } + }, + { + 'node': { + 'id': 'R3JvdXA6MjQ0', + 'maintainers': [ + 'vanguard', + 'shield' + ], + 'productOwner': 'vanguard', + 'projectOwner': 'shield' + } + }, + { + 'node': { + 'id': 'R3JvdXA6MjQ2', + 'maintainers': [ + 'lich', + 'king' + ], + 'productOwner': 'lich', + 'projectOwner': 'king' + } + } + ], + 'pageInfo': { + 'endCursor': 'NA==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 4 + } + } +} + +snapshots['test_empty 1'] = { + 'data': { + 'allGroups': { + 'edges': [ + ], + 'totalCount': 0 + } + } +} + +snapshots['test_first 1'] = { + 'data': { + 'allGroups': { + 'edges': [ + { + 'node': { + 'id': 'R3JvdXA6MQ==', + 'maintainers': [ + 'clark', + 'kent' + ], + 'productOwner': 'john', + 'projectOwner': 'doe' + } + }, + { + 'node': { + 'id': 'R3JvdXA6MjQ=', + 'maintainers': [ + 'black', + 'smith' + ], + 'productOwner': 'black', + 'projectOwner': 'smith' + } + }, + { + 'node': { + 'id': 'R3JvdXA6MjQ0', + 'maintainers': [ + 'vanguard', + 'shield' + ], + 'productOwner': 'vanguard', + 'projectOwner': 'shield' + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 4 + } + } +} + +snapshots['test_first_after 1'] = { + 'data': { + 'allGroups': { + 'edges': [ + { + 'node': { + 'id': 'R3JvdXA6MjQ=', + 'maintainers': [ + 'black', + 'smith' + ], + 'productOwner': 'black', + 'projectOwner': 'smith' + } + }, + { + 'node': { + 'id': 'R3JvdXA6MjQ0', + 'maintainers': [ + 'vanguard', + 'shield' + ], + 'productOwner': 'vanguard', + 'projectOwner': 'shield' + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': True, + 'startCursor': 'Mg==' + }, + 'totalCount': 4 + } + } +} + +snapshots['test_last 1'] = { + 'data': { + 'allGroups': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': 'Pagination "last" works only in combination with "before" argument.', + 'path': [ + 'allGroups' + ] + } + ] +} + +snapshots['test_last_before 1'] = { + 'data': { + 'allGroups': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 4 + } + } +} diff --git a/test/api/query/snapshots/snap_test_libraries.py b/test/api/query/snapshots/snap_test_libraries.py new file mode 100644 index 00000000..53960d5c --- /dev/null +++ b/test/api/query/snapshots/snap_test_libraries.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_all 1'] = { + 'data': { + 'allLibraries': { + 'edges': [ + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeTox', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy one', + 'owner': 'platform', + 'slackChannel': '#slack', + 'sonarqubeProject': 'sonar' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToy', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy second', + 'owner': 'platform soft', + 'slackChannel': '#slack-second', + 'sonarqubeProject': 'sonar-qub' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToz', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy third', + 'owner': 'platform software', + 'slackChannel': '#slack-third', + 'sonarqubeProject': 'sonar-qube' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeTo0', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy fourth', + 'owner': 'platform software plus', + 'slackChannel': '#slack-fourth', + 'sonarqubeProject': 'sonar-qubeee' + } + } + ], + 'pageInfo': { + 'endCursor': 'NA==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 4 + } + } +} + +snapshots['test_empty 1'] = { + 'data': { + 'allLibraries': { + 'edges': [ + ], + 'totalCount': 0 + } + } +} + +snapshots['test_first 1'] = { + 'data': { + 'allLibraries': { + 'edges': [ + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeTox', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy one', + 'owner': 'platform', + 'slackChannel': '#slack', + 'sonarqubeProject': 'sonar' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToy', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy second', + 'owner': 'platform soft', + 'slackChannel': '#slack-second', + 'sonarqubeProject': 'sonar-qub' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToz', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy third', + 'owner': 'platform software', + 'slackChannel': '#slack-third', + 'sonarqubeProject': 'sonar-qube' + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 4 + } + } +} + +snapshots['test_first_after 1'] = { + 'data': { + 'allLibraries': { + 'edges': [ + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToy', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy second', + 'owner': 'platform soft', + 'slackChannel': '#slack-second', + 'sonarqubeProject': 'sonar-qub' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToz', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy third', + 'owner': 'platform software', + 'slackChannel': '#slack-third', + 'sonarqubeProject': 'sonar-qube' + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': True, + 'startCursor': 'Mg==' + }, + 'totalCount': 4 + } + } +} + +snapshots['test_last 1'] = { + 'data': { + 'allLibraries': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': 'Pagination "last" works only in combination with "before" argument.', + 'path': [ + 'allLibraries' + ] + } + ] +} + +snapshots['test_last_before 1'] = { + 'data': { + 'allLibraries': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 4 + } + } +} + +snapshots['test_with_repository 1'] = { + 'data': { + 'allLibraries': { + 'edges': [ + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeTox', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy one', + 'owner': 'platform', + 'repository': { + 'name': 'thiwer', + 'owner': 'jasckson', + 'remoteId': 125, + 'url': 'https://gitlab.com/thiwer/thiwer' + }, + 'slackChannel': '#slack', + 'sonarqubeProject': 'sonar' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToy', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy second', + 'owner': 'platform soft', + 'repository': { + 'name': 'parker', + 'owner': 'peter', + 'remoteId': 234, + 'url': 'https://gitlab.com/peter/parker' + }, + 'slackChannel': '#slack-second', + 'sonarqubeProject': 'sonar-qub' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeToz', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy third', + 'owner': 'platform software', + 'repository': { + 'name': 'smith', + 'owner': 'black', + 'remoteId': 2134539, + 'url': 'https://gitlab.com/black/smith' + }, + 'slackChannel': '#slack-third', + 'sonarqubeProject': 'sonar-qube' + } + }, + { + 'node': { + 'docsUrl': None, + 'id': 'TGlicmFyeTo0', + 'impact': 'employees', + 'libraryUrl': None, + 'lifecycle': 'fixed', + 'name': 'fancy fourth', + 'owner': 'platform software plus', + 'repository': { + 'name': 'kent', + 'owner': 'clark', + 'remoteId': 124, + 'url': 'https://gitlab.com/clark/kent' + }, + 'slackChannel': '#slack-fourth', + 'sonarqubeProject': 'sonar-qubeee' + } + } + ], + 'pageInfo': { + 'endCursor': 'NA==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 4 + } + } +} diff --git a/test/api/query/snapshots/snap_test_links.py b/test/api/query/snapshots/snap_test_links.py new file mode 100644 index 00000000..feaea2c5 --- /dev/null +++ b/test/api/query/snapshots/snap_test_links.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_all 1'] = { + 'data': { + 'allLinks': { + 'edges': [ + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazox', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazoy', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazoz', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazo0', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazo1', + 'name': 'link', + 'url': 'https://some.url.com' + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_empty 1'] = { + 'data': { + 'allLinks': { + 'edges': [ + ], + 'totalCount': 0 + } + } +} + +snapshots['test_first 1'] = { + 'data': { + 'allLinks': { + 'edges': [ + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazox', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazoy', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazoz', + 'name': 'link', + 'url': 'https://some.url.com' + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_first_after 1'] = { + 'data': { + 'allLinks': { + 'edges': [ + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazoy', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'icon': 'smile', + 'id': 'TGluazoz', + 'name': 'link', + 'url': 'https://some.url.com' + } + } + ], + 'pageInfo': { + 'endCursor': 'Mw==', + 'hasNextPage': True, + 'hasPreviousPage': True, + 'startCursor': 'Mg==' + }, + 'totalCount': 5 + } + } +} + +snapshots['test_last 1'] = { + 'data': { + 'allLinks': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': 'Pagination "last" works only in combination with "before" argument.', + 'path': [ + 'allLinks' + ] + } + ] +} + +snapshots['test_last_before 1'] = { + 'data': { + 'allLinks': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 5 + } + } +} + +snapshots['test_with_entity 1'] = { + 'data': { + 'allLinks': { + 'edges': [ + { + 'node': { + 'entity': { + 'description': 'This is my fancy component', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + }, + 'icon': 'smile', + 'id': 'TGluazox', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'entity': { + 'description': 'This is my fancy component', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + }, + 'icon': 'smile', + 'id': 'TGluazoy', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'entity': { + 'description': 'This is my fancy component', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + }, + 'icon': 'smile', + 'id': 'TGluazoz', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'entity': { + 'description': 'This is my fancy component', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + }, + 'icon': 'smile', + 'id': 'TGluazo0', + 'name': 'link', + 'url': 'https://some.url.com' + } + }, + { + 'node': { + 'entity': { + 'description': 'This is my fancy component', + 'kind': None, + 'label': 'Base', + 'name': 'base', + 'owner': 'platform', + 'type': None + }, + 'icon': 'smile', + 'id': 'TGluazo1', + 'name': 'link', + 'url': 'https://some.url.com' + } + } + ], + 'pageInfo': { + 'endCursor': 'NQ==', + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': 'MQ==' + }, + 'totalCount': 5 + } + } +} diff --git a/test/api/query/snapshots/snap_test_node.py b/test/api/query/snapshots/snap_test_node.py index ded7b484..7a6b31ec 100644 --- a/test/api/query/snapshots/snap_test_node.py +++ b/test/api/query/snapshots/snap_test_node.py @@ -7,14 +7,25 @@ snapshots = Snapshot() -snapshots['test_repository 1'] = { +snapshots['test_dependency 1'] = { 'data': { 'node': { - 'id': 'UmVwb3NpdG9yeToxMA==', - 'name': 'james-rivera', - 'owner': 'sharon54', - 'remoteId': 2783, - 'url': 'https://gitlab.com/sharon54/jones-rivera' + 'id': 'RGVwZW5kZW5jeToxMA==', + 'name': 'python', + 'type': 'Language' + } + } +} + +snapshots['test_dependency_usage 1'] = { + 'data': { + 'node': { + 'forProduction': 'false', + 'id': 'RGVwZW5kZW5jeVVzYWdlOjEw', + 'majorVersion': 3, + 'minorVersion': 2, + 'patchVersion': 4, + 'version': '3.2.4' } } } @@ -33,25 +44,14 @@ } } -snapshots['test_dependency_usage 1'] = { - 'data': { - 'node': { - 'forProduction': 'false', - 'id': 'RGVwZW5kZW5jeVVzYWdlOjEw', - 'majorVersion': 3, - 'minorVersion': 2, - 'patchVersion': 4, - 'version': '3.2.4' - } - } -} - -snapshots['test_dependency 1'] = { +snapshots['test_repository 1'] = { 'data': { 'node': { - 'id': 'RGVwZW5kZW5jeToxMA==', - 'name': 'python', - 'type': 'Language' + 'id': 'UmVwb3NpdG9yeToxMA==', + 'name': 'james-rivera', + 'owner': 'sharon54', + 'remoteId': 2783, + 'url': 'https://gitlab.com/sharon54/jones-rivera' } } } @@ -62,11 +62,11 @@ 'docsUrl': 'https://docs/skypicker/docs/', 'id': 'U2VydmljZToxMA==', 'impact': 'sales', + 'lifecycle': 'fixed', 'name': 'allen-nobles', 'owner': 'bradltwat', 'pagerdutyService': 'sales/P019873X9', - 'slackChannel': 'https://gitlab.slack', - 'status': 'fixed' + 'slackChannel': 'https://gitlab.slack' } } } diff --git a/test/api/query/snapshots/snap_test_service_links.py b/test/api/query/snapshots/snap_test_service_links.py deleted file mode 100644 index c4aeee00..00000000 --- a/test/api/query/snapshots/snap_test_service_links.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import Snapshot - - -snapshots = Snapshot() - -snapshots['test_empty 1'] = { - 'data': { - 'allServices': { - 'edges': [ - ], - 'totalCount': 0 - } - } -} - -snapshots['test_all 1'] = { - 'data': { - 'allServices': { - 'edges': [ - { - 'node': { - 'allLinks': None - } - } - ], - 'totalCount': 1 - } - }, - 'errors': [ - { - 'locations': [ - { - 'column': 13, - 'line': 7 - } - ], - 'message': 'Resolved value from the connection field have to be iterable or instance of LinkConnection. Received "None"', - 'path': [ - 'allServices', - 'edges', - 0, - 'node', - 'allLinks' - ] - } - ] -} - -snapshots['test_first 1'] = { - 'data': { - 'allServices': { - 'edges': [ - { - 'node': { - 'allLinks': None - } - } - ], - 'totalCount': 1 - } - }, - 'errors': [ - { - 'locations': [ - { - 'column': 13, - 'line': 7 - } - ], - 'message': 'Resolved value from the connection field have to be iterable or instance of LinkConnection. Received "None"', - 'path': [ - 'allServices', - 'edges', - 0, - 'node', - 'allLinks' - ] - } - ] -} - -snapshots['test_last 1'] = { - 'errors': [ - { - 'locations': [ - { - 'column': 19, - 'line': 13 - } - ], - 'message': 'Cannot query field "url" on type "Environment".' - }, - { - 'locations': [ - { - 'column': 19, - 'line': 14 - } - ], - 'message': 'Cannot query field "icon" on type "Environment".' - } - ] -} diff --git a/test/api/query/snapshots/snap_test_services.py b/test/api/query/snapshots/snap_test_services.py index f7c2093f..3934343a 100644 --- a/test/api/query/snapshots/snap_test_services.py +++ b/test/api/query/snapshots/snap_test_services.py @@ -7,16 +7,6 @@ snapshots = Snapshot() -snapshots['test_empty 1'] = { - 'data': { - 'allServices': { - 'edges': [ - ], - 'totalCount': 0 - } - } -} - snapshots['test_all 1'] = { 'data': { 'allServices': { @@ -26,11 +16,11 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZTox', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'martinez', 'owner': 'michaelbennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -38,11 +28,11 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToy', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'alex', 'owner': 'amstrong', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -50,11 +40,11 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToz', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'artinez', 'owner': 'bennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -62,11 +52,11 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZTo0', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'john', 'owner': 'benneto', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -74,11 +64,11 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToxMg==', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'simmons-mitchell', 'owner': 'dedward', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } } ], @@ -93,108 +83,12 @@ } } -snapshots['test_with_repository 1'] = { +snapshots['test_empty 1'] = { 'data': { 'allServices': { 'edges': [ - { - 'node': { - 'docsUrl': 'https://docsurl', - 'id': 'U2VydmljZTox', - 'impact': 'profit', - 'name': 'martinez', - 'owner': 'michaelbennett', - 'pagerdutyService': '/services', - 'repository': { - 'name': 'thiwer', - 'owner': 'jasckson', - 'remoteId': 239, - 'url': 'https://gitlab.com/thiwer/thiwer' - }, - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' - } - }, - { - 'node': { - 'docsUrl': 'https://docsurl', - 'id': 'U2VydmljZToy', - 'impact': 'profit', - 'name': 'alex', - 'owner': 'amstrong', - 'pagerdutyService': '/services', - 'repository': { - 'name': 'farel', - 'owner': 'colisn', - 'remoteId': 99, - 'url': 'https://gitlab.com/farel/colins' - }, - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' - } - }, - { - 'node': { - 'docsUrl': 'https://docsurl', - 'id': 'U2VydmljZToz', - 'impact': 'profit', - 'name': 'artinez', - 'owner': 'bennett', - 'pagerdutyService': '/services', - 'repository': { - 'name': 'Amstrong', - 'owner': 'Daniel', - 'remoteId': 9234, - 'url': 'https://gitlab.com/daniel/amstrong' - }, - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' - } - }, - { - 'node': { - 'docsUrl': 'https://docsurl', - 'id': 'U2VydmljZTo0', - 'impact': 'profit', - 'name': 'john', - 'owner': 'benneto', - 'pagerdutyService': '/services', - 'repository': { - 'name': 'blanc', - 'owner': 'josh', - 'remoteId': 349, - 'url': 'https://gitlab.com/josh/blanc' - }, - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' - } - }, - { - 'node': { - 'docsUrl': 'https://docsurl', - 'id': 'U2VydmljZToxMg==', - 'impact': 'profit', - 'name': 'simmons-mitchell', - 'owner': 'dedward', - 'pagerdutyService': '/services', - 'repository': { - 'name': 'leblanc', - 'owner': 'imosley', - 'remoteId': 990, - 'url': 'https://gitlab.com/schultzcarolyn/leblanc' - }, - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' - } - } ], - 'pageInfo': { - 'endCursor': 'NQ==', - 'hasNextPage': False, - 'hasPreviousPage': False, - 'startCursor': 'MQ==' - }, - 'totalCount': 5 + 'totalCount': 0 } } } @@ -208,10 +102,10 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZTox', 'impact': 'profit', + 'lifecycle': 'fixed', 'owner': 'michaelbennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -219,10 +113,10 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToy', 'impact': 'profit', + 'lifecycle': 'fixed', 'owner': 'amstrong', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -230,10 +124,10 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToz', 'impact': 'profit', + 'lifecycle': 'fixed', 'owner': 'bennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } } ], @@ -257,10 +151,10 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToy', 'impact': 'profit', + 'lifecycle': 'fixed', 'owner': 'amstrong', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } }, { @@ -268,10 +162,10 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZToz', 'impact': 'profit', + 'lifecycle': 'fixed', 'owner': 'bennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } } ], @@ -286,22 +180,6 @@ } } -snapshots['test_last_before 1'] = { - 'data': { - 'allServices': { - 'edges': [ - ], - 'pageInfo': { - 'endCursor': None, - 'hasNextPage': False, - 'hasPreviousPage': False, - 'startCursor': None - }, - 'totalCount': 5 - } - } -} - snapshots['test_last 1'] = { 'data': { 'allServices': None @@ -322,6 +200,22 @@ ] } +snapshots['test_last_before 1'] = { + 'data': { + 'allServices': { + 'edges': [ + ], + 'pageInfo': { + 'endCursor': None, + 'hasNextPage': False, + 'hasPreviousPage': False, + 'startCursor': None + }, + 'totalCount': 5 + } + } +} + snapshots['test_with_environment 1'] = { 'data': { 'allServices': { @@ -364,11 +258,11 @@ 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZTox', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'martinez', 'owner': 'michaelbennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'slackChannel': 'https://slackchannel' } } ], @@ -383,49 +277,108 @@ } } -snapshots['test_with_links 1'] = { +snapshots['test_with_repository 1'] = { 'data': { 'allServices': { 'edges': [ { 'node': { - 'allLinks': None, 'docsUrl': 'https://docsurl', 'id': 'U2VydmljZTox', 'impact': 'profit', + 'lifecycle': 'fixed', 'name': 'martinez', 'owner': 'michaelbennett', 'pagerdutyService': '/services', - 'slackChannel': 'https://slackchannel', - 'status': 'fixed' + 'repository': { + 'name': 'thiwer', + 'owner': 'jasckson', + 'remoteId': 239, + 'url': 'https://gitlab.com/thiwer/thiwer' + }, + 'slackChannel': 'https://slackchannel' + } + }, + { + 'node': { + 'docsUrl': 'https://docsurl', + 'id': 'U2VydmljZToy', + 'impact': 'profit', + 'lifecycle': 'fixed', + 'name': 'alex', + 'owner': 'amstrong', + 'pagerdutyService': '/services', + 'repository': { + 'name': 'farel', + 'owner': 'colisn', + 'remoteId': 99, + 'url': 'https://gitlab.com/farel/colins' + }, + 'slackChannel': 'https://slackchannel' + } + }, + { + 'node': { + 'docsUrl': 'https://docsurl', + 'id': 'U2VydmljZToz', + 'impact': 'profit', + 'lifecycle': 'fixed', + 'name': 'artinez', + 'owner': 'bennett', + 'pagerdutyService': '/services', + 'repository': { + 'name': 'Amstrong', + 'owner': 'Daniel', + 'remoteId': 9234, + 'url': 'https://gitlab.com/daniel/amstrong' + }, + 'slackChannel': 'https://slackchannel' + } + }, + { + 'node': { + 'docsUrl': 'https://docsurl', + 'id': 'U2VydmljZTo0', + 'impact': 'profit', + 'lifecycle': 'fixed', + 'name': 'john', + 'owner': 'benneto', + 'pagerdutyService': '/services', + 'repository': { + 'name': 'blanc', + 'owner': 'josh', + 'remoteId': 349, + 'url': 'https://gitlab.com/josh/blanc' + }, + 'slackChannel': 'https://slackchannel' + } + }, + { + 'node': { + 'docsUrl': 'https://docsurl', + 'id': 'U2VydmljZToxMg==', + 'impact': 'profit', + 'lifecycle': 'fixed', + 'name': 'simmons-mitchell', + 'owner': 'dedward', + 'pagerdutyService': '/services', + 'repository': { + 'name': 'leblanc', + 'owner': 'imosley', + 'remoteId': 990, + 'url': 'https://gitlab.com/schultzcarolyn/leblanc' + }, + 'slackChannel': 'https://slackchannel' } } ], 'pageInfo': { - 'endCursor': 'MQ==', + 'endCursor': 'NQ==', 'hasNextPage': False, 'hasPreviousPage': False, 'startCursor': 'MQ==' }, - 'totalCount': 1 - } - }, - 'errors': [ - { - 'locations': [ - { - 'column': 13, - 'line': 15 - } - ], - 'message': 'Resolved value from the connection field have to be iterable or instance of LinkConnection. Received "None"', - 'path': [ - 'allServices', - 'edges', - 0, - 'node', - 'allLinks' - ] + 'totalCount': 5 } - ] + } } diff --git a/test/api/query/test_entities.py b/test/api/query/test_entities.py new file mode 100644 index 00000000..1b0d385d --- /dev/null +++ b/test/api/query/test_entities.py @@ -0,0 +1,479 @@ +import pytest + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def generate_components( + component_base_factory, + service_factory, + repository_factory, + link_factory, + library_factory, +): + c1 = component_base_factory( + id=1, + name="base", + label="Base", + type="database", + description="This is my fancy component", + kind="component", + owner="platform", + service=None, + library=None, + source__id=1, + source__remote_id=1, + source__owner="jasckson", + source__name="thiwer", + source__url="https://gitlab.com/thiwer/thiwer", + ) + + c2 = component_base_factory( + id=2, + name="base_2", + label="Base 2", + type="database 2", + description="This is my fancy component 2", + kind="component", + owner="platformm2", + service=None, + library=None, + source__id=12, + source__remote_id=13, + source__owner="peter", + source__name="parker", + source__url="https://gitlab.com/peter/parker", + ) + + component_base_factory( + id=32, + name="base_3", + label="Base 3", + type="database 3", + description="This is my fancy component 3", + kind="component", + owner="platformm3", + service=None, + library=None, + source__id=23, + source__remote_id=234, + source__owner="clark", + source__name="kent", + source__url="https://gitlab.com/clark/kent", + ) + + repository = repository_factory( + id=22, + remote_id=22, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + + service = service_factory( + owner="platform", + name="my-service", + lifecycle="production", + impact="employees", + repository=repository, + slack_channel="#platform-software", + docs_url="https://docs.com", + ) + + component_base_factory( + id=1245, + name="service", + label="Service", + type="service", + description="This is my fancy service", + kind="component", + owner="platform", + service=service, + library=None, + source=repository, + ) + + link_factory( + id=1, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=c1, + ) + link_factory( + id=2, + name="Sentry", + url="https://sentry.skypicker.com", + entity=c2, + ) + + library = library_factory(repository=repository) + + component_base_factory( + id=15545, + name="library", + label="Library", + type="library", + description="This is my fancy library", + kind="component", + owner="platform", + service=None, + library=library, + source=repository, + ) + + +def test_empty(snapshot, call_api): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + } + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_all(snapshot, call_api, generate_components): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_source(snapshot, call_api, generate_components): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + source { + remoteId + owner + name + url + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_service(snapshot, call_api, generate_components): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + service { + id + owner + lifecycle + impact + slackChannel + pagerdutyService + docsUrl + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_group(snapshot, call_api, generate_components): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + group { + id + productOwner + projectOwner + maintainers + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_library(snapshot, call_api, generate_components): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + library { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_links(snapshot, call_api, generate_components): + query = """ + query { + allEntities { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + allLinks { + totalCount + edges { + node { + name + icon + url + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first(snapshot, call_api, generate_components): + query = """ + query { + allEntities(first: 3) { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first_after(snapshot, call_api, generate_components): + query = """ + query { + allEntities(first: 2, after: "MQ==") { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last(snapshot, call_api, generate_components): + query = """ + query { + allEntities(last: 3) { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last_before(snapshot, call_api, generate_components): + query = """ + query { + allEntities(last: 1, before: "MQ==") { + totalCount + edges { + node { + id + name + label + kind + type + owner + description + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) diff --git a/test/api/query/test_groups.py b/test/api/query/test_groups.py new file mode 100644 index 00000000..06e48007 --- /dev/null +++ b/test/api/query/test_groups.py @@ -0,0 +1,172 @@ +import pytest + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def generate_groups(group_factory): + group_factory( + id=1, product_owner="john", project_owner="doe", maintainers=["clark", "kent"] + ) + group_factory( + id=24, + product_owner="black", + project_owner="smith", + maintainers=["black", "smith"], + ) + group_factory( + id=244, + product_owner="vanguard", + project_owner="shield", + maintainers=["vanguard", "shield"], + ) + group_factory( + id=246, product_owner="lich", project_owner="king", maintainers=["lich", "king"] + ) + + +def test_empty(snapshot, call_api): + query = """ + query { + allGroups { + totalCount + edges { + node { + id + } + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_all(snapshot, call_api, generate_groups): + query = """ + query { + allGroups { + totalCount + edges { + node { + id + productOwner + projectOwner + maintainers + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first(snapshot, call_api, generate_groups): + query = """ + query { + allGroups(first: 3) { + totalCount + edges { + node { + id + productOwner + projectOwner + maintainers + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first_after(snapshot, call_api, generate_groups): + query = """ + query { + allGroups(first: 2, after: "MQ==") { + totalCount + edges { + node { + id + productOwner + projectOwner + maintainers + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last(snapshot, call_api, generate_groups): + query = """ + query { + allGroups(last: 3) { + totalCount + edges { + node { + id + productOwner + projectOwner + maintainers + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last_before(snapshot, call_api, generate_groups): + query = """ + query { + allGroups(last: 1, before: "MQ==") { + totalCount + edges { + node { + id + productOwner + projectOwner + maintainers + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) diff --git a/test/api/query/test_libraries.py b/test/api/query/test_libraries.py new file mode 100644 index 00000000..9b1210ce --- /dev/null +++ b/test/api/query/test_libraries.py @@ -0,0 +1,272 @@ +import pytest + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def generate_libraries(library_factory): + library_factory( + id=1, + owner="platform", + name="fancy one", + lifecycle="fixed", + impact="employees", + slack_channel="#slack", + sonarqube_project="sonar", + repository__id=124, + repository__remote_id=125, + repository__owner="jasckson", + repository__name="thiwer", + repository__url="https://gitlab.com/thiwer/thiwer", + ) + library_factory( + id=2, + owner="platform soft", + name="fancy second", + lifecycle="fixed", + impact="employees", + slack_channel="#slack-second", + sonarqube_project="sonar-qub", + repository__id=22, + repository__remote_id=234, + repository__owner="peter", + repository__name="parker", + repository__url="https://gitlab.com/peter/parker", + ) + library_factory( + id=3, + owner="platform software", + name="fancy third", + lifecycle="fixed", + impact="employees", + slack_channel="#slack-third", + sonarqube_project="sonar-qube", + repository__id=455, + repository__remote_id=2134539, + repository__owner="black", + repository__name="smith", + repository__url="https://gitlab.com/black/smith", + ) + library_factory( + id=4, + owner="platform software plus", + name="fancy fourth", + lifecycle="fixed", + impact="employees", + slack_channel="#slack-fourth", + sonarqube_project="sonar-qubeee", + repository__id=467, + repository__remote_id=124, + repository__owner="clark", + repository__name="kent", + repository__url="https://gitlab.com/clark/kent", + ) + + +def test_empty(snapshot, call_api): + query = """ + query { + allLibraries { + totalCount + edges { + node { + id + } + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_all(snapshot, call_api, generate_libraries): + query = """ + query { + allLibraries { + totalCount + edges { + node { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_repository(snapshot, call_api, generate_libraries): + query = """ + query { + allLibraries { + totalCount + edges { + node { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + repository { + remoteId + owner + name + url + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first(snapshot, call_api, generate_libraries): + query = """ + query { + allLibraries(first: 3) { + totalCount + edges { + node { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first_after(snapshot, call_api, generate_libraries): + query = """ + query { + allLibraries(first: 2, after: "MQ==") { + totalCount + edges { + node { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last(snapshot, call_api, generate_libraries): + query = """ + query { + allLibraries(last: 3) { + totalCount + edges { + node { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last_before(snapshot, call_api, generate_libraries): + query = """ + query { + allLibraries(last: 1, before: "MQ==") { + totalCount + edges { + node { + id + owner + name + lifecycle + impact + slackChannel + sonarqubeProject + docsUrl + libraryUrl + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) diff --git a/test/api/query/test_links.py b/test/api/query/test_links.py new file mode 100644 index 00000000..87285989 --- /dev/null +++ b/test/api/query/test_links.py @@ -0,0 +1,219 @@ +import pytest + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def generate_links(component_base_factory, link_factory): + component = component_base_factory( + id=1, + name="base", + label="Base", + type="database", + description="This is my fancy component", + kind="component", + owner="platform", + service=None, + library=None, + source__id=1, + source__remote_id=1, + source__owner="jasckson", + source__name="thiwer", + source__url="https://gitlab.com/thiwer/thiwer", + ) + link_factory( + id=1, name="link", entity=component, icon="smile", url="https://some.url.com" + ) + link_factory( + id=2, name="link", entity=component, icon="smile", url="https://some.url.com" + ) + link_factory( + id=3, name="link", entity=component, icon="smile", url="https://some.url.com" + ) + link_factory( + id=4, name="link", entity=component, icon="smile", url="https://some.url.com" + ) + link_factory( + id=5, name="link", entity=component, icon="smile", url="https://some.url.com" + ) + + +def test_empty(snapshot, call_api): + query = """ + query { + allLinks { + totalCount + edges { + node { + id + } + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_all(snapshot, call_api, generate_links): + query = """ + query { + allLinks { + totalCount + edges { + node { + id + name + icon + url + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_with_entity(snapshot, call_api, generate_links): + query = """ + query { + allLinks { + totalCount + edges { + node { + id + name + icon + url + entity { + name + label + kind + type + owner + description + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first(snapshot, call_api, generate_links): + query = """ + query { + allLinks(first: 3) { + totalCount + edges { + node { + id + name + icon + url + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_first_after(snapshot, call_api, generate_links): + query = """ + query { + allLinks(first: 2, after: "MQ==") { + totalCount + edges { + node { + id + name + icon + url + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last(snapshot, call_api, generate_links): + query = """ + query { + allLinks(last: 3) { + totalCount + edges { + node { + id + name + icon + url + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) + + +def test_last_before(snapshot, call_api, generate_links): + query = """ + query { + allLinks(last: 1, before: "MQ==") { + totalCount + edges { + node { + id + name + icon + url + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + """ + response = call_api(query) + snapshot.assert_match(response) diff --git a/test/api/query/test_node.py b/test/api/query/test_node.py index f89a8319..240ec47b 100644 --- a/test/api/query/test_node.py +++ b/test/api/query/test_node.py @@ -9,7 +9,7 @@ def test_service(snapshot, call_api, service_factory): id=10, owner="bradltwat", name="allen-nobles", - status="fixed", + lifecycle="fixed", impact="sales", slack_channel="https://gitlab.slack", pagerduty_service="sales/P019873X9", @@ -23,7 +23,7 @@ def test_service(snapshot, call_api, service_factory): id owner name - status + lifecycle impact slackChannel pagerdutyService diff --git a/test/api/query/test_service_environments.py b/test/api/query/test_service_environments.py index 668ef21e..5bc17159 100644 --- a/test/api/query/test_service_environments.py +++ b/test/api/query/test_service_environments.py @@ -16,7 +16,7 @@ def generate_environments(service_factory, environment_factory): docs_url="https://docsurl", pagerduty_service="sales/P019873X9", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", ) environment_factory( id=1, diff --git a/test/api/query/test_service_links.py b/test/api/query/test_service_links.py deleted file mode 100644 index a0b3fe45..00000000 --- a/test/api/query/test_service_links.py +++ /dev/null @@ -1,150 +0,0 @@ -import pytest - -pytestmark = pytest.mark.django_db - - -@pytest.fixture -def generate_links(service_factory, link_factory): - service = service_factory( - id=1, - name="martinez", - owner="michaelbennett", - impact="profit", - docs_url="https://docsurl", - pagerduty_service="sales/P019873X9", - slack_channel="https://slackchannel", - status="fixed", - ) - link_factory( - id=1, - service=service, - name="Datadog", - url="https://datadog.com", - icon="datadog", - ) - link_factory( - id=2, service=service, name="Sentry", url="https://sentry.com", icon="sentry" - ) - - -def test_empty(snapshot, call_api): - query = """ - query { - allServices { - totalCount - edges { - node { - allLinks { - totalCount - edges { - node { - id - } - } - } - } - } - } - } - """ - response = call_api(query) - snapshot.assert_match(response) - - -def test_all(snapshot, call_api, generate_links): - query = """ - query { - allServices { - totalCount - edges { - node { - allLinks { - totalCount - edges { - node { - id - name - url - icon - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - } - } - } - """ - response = call_api(query) - snapshot.assert_match(response) - - -def test_first(snapshot, call_api, generate_links): - query = """ - query { - allServices { - totalCount - edges { - node { - allLinks(first: 2) { - totalCount - edges { - node { - id - name - url - icon - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - } - } - } - """ - response = call_api(query) - snapshot.assert_match(response) - - -def test_last(snapshot, call_api, generate_links): - query = """ - query { - allServices { - totalCount - edges { - node { - allEnvironments(last: 2, before: "Mg==") { - totalCount - edges { - node { - id - name - url - icon - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - } - } - } - """ - response = call_api(query) - snapshot.assert_match(response) diff --git a/test/api/query/test_services.py b/test/api/query/test_services.py index 06f05c8a..a3946d27 100644 --- a/test/api/query/test_services.py +++ b/test/api/query/test_services.py @@ -1,8 +1,5 @@ import pytest -from zoo.repos.models import Repository -from zoo.services.models import Service - pytestmark = pytest.mark.django_db @@ -16,7 +13,7 @@ def generate_services(service_factory): docs_url="https://docsurl", pagerduty_service="/services", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", repository__id=78, repository__remote_id=239, repository__owner="jasckson", @@ -31,7 +28,7 @@ def generate_services(service_factory): docs_url="https://docsurl", pagerduty_service="/services", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", repository__id=48, repository__remote_id=99, repository__owner="colisn", @@ -46,7 +43,7 @@ def generate_services(service_factory): docs_url="https://docsurl", pagerduty_service="/services", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", repository__id=234, repository__remote_id=9234, repository__owner="Daniel", @@ -61,7 +58,7 @@ def generate_services(service_factory): docs_url="https://docsurl", pagerduty_service="/services", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", repository__id=3434, repository__remote_id=349, repository__owner="josh", @@ -77,7 +74,7 @@ def generate_services(service_factory): docs_url="https://docsurl", pagerduty_service="/services", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", repository__id=4543, repository__remote_id=990, repository__owner="imosley", @@ -96,7 +93,7 @@ def generate_services_with_environments(service_factory, environment_factory): docs_url="https://docsurl", pagerduty_service="/services", slack_channel="https://slackchannel", - status="fixed", + lifecycle="fixed", repository__id=78, repository__remote_id=239, repository__owner="jasckson", @@ -121,39 +118,6 @@ def generate_services_with_environments(service_factory, environment_factory): ) -@pytest.fixture -def generate_services_with_links(service_factory, link_factory): - service = service_factory( - id=1, - name="martinez", - owner="michaelbennett", - impact="profit", - docs_url="https://docsurl", - pagerduty_service="/services", - slack_channel="https://slackchannel", - status="fixed", - repository__id=78, - repository__remote_id=239, - repository__owner="jasckson", - repository__name="thiwer", - repository__url="https://gitlab.com/thiwer/thiwer", - ) - link_factory( - id=1, - service=service, - name="Datadog", - url="https://datadog.com", - icon="datadog", - ) - link_factory( - id=2, - service=service, - name="Sentry", - url="https://sentry.com", - icon="Sentry", - ) - - def test_empty(snapshot, call_api): query = """ query { @@ -181,7 +145,7 @@ def test_all(snapshot, call_api, generate_services): id owner name - status + lifecycle impact slackChannel pagerdutyService @@ -211,7 +175,7 @@ def test_with_repository(snapshot, call_api, generate_services): id name owner - status + lifecycle impact slackChannel pagerdutyService @@ -247,7 +211,7 @@ def test_with_environment(snapshot, call_api, generate_services_with_environment id name owner - status + lifecycle impact slackChannel pagerdutyService @@ -293,7 +257,7 @@ def test_first(snapshot, call_api, generate_services): node { id owner - status + lifecycle impact slackChannel pagerdutyService @@ -322,7 +286,7 @@ def test_first_after(snapshot, call_api, generate_services): node { id owner - status + lifecycle impact slackChannel pagerdutyService @@ -351,7 +315,7 @@ def test_last(snapshot, call_api, generate_services): node { id owner - status + lifecycle impact slackChannel pagerdutyService @@ -380,7 +344,7 @@ def test_last_before(snapshot, call_api, generate_services): node { id owner - status + lifecycle impact slackChannel pagerdutyService @@ -398,49 +362,3 @@ def test_last_before(snapshot, call_api, generate_services): """ response = call_api(query) snapshot.assert_match(response) - - -def test_with_links(snapshot, call_api, generate_services_with_links): - query = """ - query { - allServices { - totalCount - edges { - node { - id - name - owner - status - impact - slackChannel - pagerdutyService - docsUrl - allLinks { - totalCount - edges { - node { - name - url - icon - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - """ - response = call_api(query) - snapshot.assert_match(response) diff --git a/test/conftest.py b/test/conftest.py index cb94acc7..45ae0a0a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -3,10 +3,8 @@ import tarfile from pathlib import Path -import meilisearch import pytest import testing.postgresql -from django.conf import settings from django.db import connections from environ import Env from faker import Faker @@ -15,12 +13,17 @@ from zoo.auditing.runner import CheckContext from zoo.factories import ( ApiTokenFactory, + ComponentBaseFactory, + ComponentLibraryFactory, + ComponentServiceFactory, DependencyFactory, DependencyUsageFactory, EnvironmentFactory, + GroupFactory, InfraNodeFactory, IssueFactory, KindFactory, + LibraryFactory, LinkFactory, RepositoryFactory, ServiceFactory, @@ -42,6 +45,11 @@ register(DependencyUsageFactory) register(KindFactory) register(InfraNodeFactory) +register(ComponentBaseFactory) +register(ComponentLibraryFactory) +register(ComponentServiceFactory) +register(LibraryFactory) +register(GroupFactory) fake = Faker() diff --git a/test/entities/__init__.py b/test/entities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/entities/test_objects_to_yaml.py b/test/entities/test_objects_to_yaml.py new file mode 100644 index 00000000..e2dd1b81 --- /dev/null +++ b/test/entities/test_objects_to_yaml.py @@ -0,0 +1,386 @@ +import pytest + +from zoo.repos.entities_yaml import generate, validate +from zoo.repos.models import Repository + +pytestmark = pytest.mark.django_db + + +def test_generate_base_component(group_factory, component_base_factory, link_factory): + group = group_factory(id=1, product_owner="john", project_owner="doe") + component = component_base_factory( + id=1, + name="base", + label="Base", + type="database", + description="This is my fancy component", + kind="component", + owner="platform", + service=None, + library=None, + group=group, + source__id=1, + source__remote_id=1, + source__owner="jasckson", + source__name="thiwer", + source__url="https://gitlab.com/thiwer/thiwer", + ) + + link_factory( + id=1, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=component, + ) + link_factory( + id=2, + name="Sentry", + url="https://sentry.skypicker.com", + entity=component, + ) + + expected = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: base + label: Base + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: database +""" + repository = Repository.objects.get(pk=1) + content = generate(repository) + assert validate(content) + assert expected.strip() == content.strip() + + +def test_generate_component_service( + component_base_factory, + service_factory, + group_factory, + link_factory, + repository_factory, + environment_factory, +): + repository = repository_factory( + id=22, + remote_id=22, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + + service = service_factory( + owner="platform", + name="my-service", + lifecycle="production", + impact="employees", + repository=repository, + slack_channel="#platform-software", + docs_url="https://docs.com", + ) + environment_factory( + name="production", + service=service, + health_check_url="https://health.com", + dashboard_url="https://dashboard.datadog.com", + service_urls=["https://service.com"], + ) + group = group_factory(id=1, product_owner="john", project_owner="doe") + component_service = component_base_factory( + id=1, + name="service", + label="Service", + type="service", + description="This is my fancy service", + kind="component", + owner="platform", + service=service, + library=None, + group=group, + source=repository, + ) + link_factory( + id=1, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=component_service, + ) + expected = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: service + label: Service + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy service + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop +spec: + type: service + lifecycle: production + impact: employees + analysis: [] + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://health.com + service_urls: + - https://service.com + integrations: + slack_channel: '#platform-software' +""" + repository = Repository.objects.get(pk=22) + content = generate(repository) + assert validate(content) + assert expected.strip() == content.strip() + + +def test_generate_component_library( + component_base_factory, + group_factory, + link_factory, + repository_factory, + library_factory, +): + repository = repository_factory( + id=22, + remote_id=22, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + group = group_factory(id=1, product_owner="john", project_owner="doe") + lib = library_factory( + id=1, + owner="platform", + name="fancy one", + lifecycle="fixed", + impact="employees", + slack_channel="#slack", + sonarqube_project="sonar", + repository=repository, + ) + component_service = component_base_factory( + id=1, + name="library", + label="Lib", + type="library", + description="This is my fancy library", + kind="component", + owner="platform", + service=None, + library=lib, + group=group, + source=repository, + ) + link_factory( + id=1, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=component_service, + ) + + expected = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: library + label: Lib + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy library + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop +spec: + type: library + lifecycle: fixed + impact: employees + analysis: [] + integrations: + sonarqube_project: sonar + slack_channel: '#slack' +""" + repository = Repository.objects.get(pk=22) + content = generate(repository) + assert validate(content) + assert expected.strip() == content.strip() + + +def test_generate_multiple_components( + component_base_factory, + group_factory, + link_factory, + repository_factory, + library_factory, + service_factory, + environment_factory, +): + repository = repository_factory( + id=22, + remote_id=22, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + group = group_factory(id=1, product_owner="john", project_owner="doe") + group_service = group_factory(id=2, product_owner="jason", project_owner="jasenson") + lib = library_factory( + id=1, + owner="platform", + name="fancy one", + lifecycle="fixed", + impact="employees", + slack_channel="#slack", + sonarqube_project="sonar", + repository=repository, + ) + service = service_factory( + owner="platform", + name="my-service", + lifecycle="production", + impact="employees", + repository=repository, + slack_channel="#platform-software", + docs_url="https://docs.com", + ) + environment_factory( + name="production", + service=service, + health_check_url="https://health.com", + dashboard_url="https://dashboard.datadog.com", + service_urls=["https://service.com"], + ) + component_lib = component_base_factory( + id=1, + name="library", + label="Lib", + type="library", + description="This is my fancy library", + kind="component", + owner="platform", + service=None, + library=lib, + group=group, + source=repository, + ) + component_service = component_base_factory( + id=2, + name="service", + label="Service", + type="service", + description="This is my fancy service", + kind="component", + owner="platform", + service=service, + library=None, + group=group_service, + source=repository, + ) + link_factory( + id=1, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=component_lib, + ) + link_factory( + id=2, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=component_service, + ) + + expected = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: library + label: Lib + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy library + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop +spec: + type: library + lifecycle: fixed + impact: employees + analysis: [] + integrations: + sonarqube_project: sonar + slack_channel: '#slack' +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: service + label: Service + owner: platform + group: + product_owner: jason + project_owner: jasenson + maintainers: [] + description: This is my fancy service + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop +spec: + type: service + lifecycle: production + impact: employees + analysis: [] + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://health.com + service_urls: + - https://service.com + integrations: + slack_channel: '#platform-software' +""" + repository = Repository.objects.get(pk=22) + content = generate(repository) + assert validate(content) + assert expected.strip() == content.strip() diff --git a/test/entities/test_yaml_to_objects.py b/test/entities/test_yaml_to_objects.py new file mode 100644 index 00000000..329ba85d --- /dev/null +++ b/test/entities/test_yaml_to_objects.py @@ -0,0 +1,680 @@ +import pytest + +import zoo.repos.tasks as uut +from zoo.entities.models import Entity +from zoo.libraries.models import Library +from zoo.services.models import Service + +pytestmark = pytest.mark.django_db + + +def test_create_base_component(mocker, repository_factory): + repository = repository_factory( + id=1, + remote_id=11, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + repository_dict = { + "id": repository.id, + "remote_id": repository.remote_id, + "provider": repository.provider, + "name": repository.name, + } + + base_component = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: base-component + label: Base Component + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: database +""" + + mocker.patch("zoo.repos.tasks.get_entity_file_content", return_value=base_component) + uut.update_project_from_entity_file(proj=repository_dict) + component_entity = Entity.objects.first() + assert Entity.objects.all().count() == 1 + assert component_entity.kind == "component" + assert component_entity.name == "base-component" + assert component_entity.label == "Base Component" + assert component_entity.owner == "platform" + assert component_entity.service is None + assert component_entity.library is None + assert component_entity.type == "database" + assert component_entity.links.all().count() == 2 + assert component_entity.group is not None + + +def test_create_base_component_and_service(mocker, repository_factory): + repository = repository_factory( + id=1, + remote_id=11, + name="test_proj1", + owner="john_doe1", + url="https://github.co m/john_doe1/test_proj1", + provider="github", + ) + repository_dict = { + "id": repository.id, + "remote_id": repository.remote_id, + "provider": repository.provider, + "name": repository.name, + } + + service_component = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: base-service + label: Base Service + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: service + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://dashboard.datadog.com + service_urls: + - https://service.prod.com + - name: sandbox + dashboard_url: https://dashboard.datadog.sandbox.com + health_check_url: https://dashboard.datadog.sandbox.com + service_urls: + - https://service.sandbox.com + impact: profit + integrations: + pagerduty_service: pagerduty_service1234 + sentry_project: sentry project 15234 + lifecycle: production +""" + + mocker.patch( + "zoo.repos.tasks.get_entity_file_content", return_value=service_component + ) + uut.update_project_from_entity_file(proj=repository_dict) + component_entity = Entity.objects.first() + assert Entity.objects.all().count() == 1 + assert Service.objects.all().count() == 1 + assert component_entity.kind == "component" + assert component_entity.name == "base-service" + assert component_entity.label == "Base Service" + assert component_entity.owner == "platform" + assert component_entity.library is None + assert component_entity.type == "service" + assert component_entity.links.all().count() == 2 + assert component_entity.group is not None + assert component_entity.service is not None + assert component_entity.service.environments.all().count() == 2 + + +def test_create_base_component_and_library(mocker, repository_factory): + repository = repository_factory( + id=1, + remote_id=11, + name="test_proj1", + owner="john_doe1", + url="https://github.co m/john_doe1/test_proj1", + provider="github", + ) + repository_dict = { + "id": repository.id, + "remote_id": repository.remote_id, + "provider": repository.provider, + "name": repository.name, + } + + library_component = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: base_lib + label: Base Lib + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: library + impact: profit + integrations: + sonarqube_project: sonarqube + lifecycle: production +""" + + mocker.patch( + "zoo.repos.tasks.get_entity_file_content", return_value=library_component + ) + uut.update_project_from_entity_file(proj=repository_dict) + component_entity = Entity.objects.first() + assert Entity.objects.all().count() == 1 + assert Library.objects.all().count() == 1 + assert component_entity.kind == "component" + assert component_entity.name == "base_lib" + assert component_entity.label == "Base Lib" + assert component_entity.owner == "platform" + assert component_entity.type == "library" + assert component_entity.links.all().count() == 2 + assert component_entity.group is not None + assert component_entity.service is None + assert component_entity.library is not None + + +def test_create_multiple_components_one_service(mocker, repository_factory): + multiple_components = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: base + label: Base + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: database +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: service + label: Service + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://service.dashboard.datadog.com + icon: poop + - name: Sentry + url: https://service..skypicker.com +spec: + type: service + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://dashboard.datadog.com + service_urls: + - https://service.prod.com + - name: sandbox + dashboard_url: https://dashboard.datadog.sandbox.com + health_check_url: https://dashboard.datadog.sandbox.com + service_urls: + - https://service.sandbox.com + impact: profit + integrations: + pagerduty_service: pagerduty_service1234 + sentry_project: sentry project 15234 + lifecycle: production +""" + + repository = repository_factory( + id=1, + remote_id=11, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + repository_dict = { + "id": repository.id, + "remote_id": repository.remote_id, + "provider": repository.provider, + "name": repository.name, + } + mocker.patch( + "zoo.repos.tasks.get_entity_file_content", return_value=multiple_components + ) + uut.update_project_from_entity_file(proj=repository_dict) + assert Entity.objects.all().count() == 2 + assert Service.objects.all().count() == 1 + + +def test_create_multiple_components_multiple_services(mocker, repository_factory): + multiple_components = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: base + label: Base + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: database +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: service + label: Base + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://service.dashboard.datadog.com + icon: poop + - name: Sentry + url: https://service..skypicker.com +spec: + type: service + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://dashboard.datadog.com + service_urls: + - https://service.prod.com + - name: sandbox + dashboard_url: https://dashboard.datadog.sandbox.com + health_check_url: https://dashboard.datadog.sandbox.com + service_urls: + - https://service.sandbox.com + impact: profit + integrations: + pagerduty_service: pagerduty_service1234 + sentry_project: sentry project 15234 + lifecycle: production +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: service-the-second + label: Service The Second + owner: platform software + group: + product_owner: john the first + project_owner: doe the second + maintainers: [john, doe] + description: This is my fancy second service + tags: [fancy, service, python] + links: + - name: Datadog + url: https://service-second.dashboard.datadog.com + icon: poop + - name: Sentry + url: https://service.second.skypicker.com +spec: + type: service + environments: + - name: production + dashboard_url: https://dashboard.second.datadog.com + health_check_url: https://dashboard.second.datadog.com + service_urls: + - https://service.second.prod.com + - name: sandbox + dashboard_url: https://dashboard.datadog.second.sandbox.com + health_check_url: https://dashboard.datadog.second.sandbox.com + service_urls: + - https://service.second.sandbox.com + impact: profit + integrations: + pagerduty_service: pagerduty_service1234_second + sentry_project: sentry project 15234 second + lifecycle: deprecated +""" + + repository = repository_factory( + id=1, + remote_id=11, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + repository_dict = { + "id": repository.id, + "remote_id": repository.remote_id, + "provider": repository.provider, + "name": repository.name, + } + mocker.patch( + "zoo.repos.tasks.get_entity_file_content", return_value=multiple_components + ) + uut.update_project_from_entity_file(proj=repository_dict) + assert Entity.objects.all().count() == 3 + assert Service.objects.all().count() == 2 + + +def test_update_base_component( + mocker, repository_factory, component_base_factory, link_factory, group_factory +): + group = group_factory(id=1, product_owner="Old John", project_owner="Old Doe") + group.save() + component = component_base_factory( + id=1, + name="old-component", + label="Old Component", + type="database", + description="Old Component Description", + kind="component", + owner="platform", + group=group, + service=None, + library=None, + source__id=1, + source__remote_id=1, + source__owner="jasckson", + source__name="thiwer", + source__url="https://gitlab.com/thiwer/thiwer", + source__provider="gitlab", + ) + component.save() + + link_factory( + id=1, + name="Datadog", + url="https://dashboard.datadog.com", + icon="poop", + entity=component, + ) + + base_component = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: new-component + label: New Component + owner: platform + group: + product_owner: New John + project_owner: New Doe + maintainers: [] + description: New Component Description + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: database + """ + + repository_dict = { + "remote_id": component.source.remote_id, + "id": component.source.id, + "provider": component.source.provider, + "name": component.source.name, + } + mocker.patch("zoo.repos.tasks.get_entity_file_content", return_value=base_component) + + component_entity = Entity.objects.first() + assert Entity.objects.all().count() == 1 + assert component_entity.kind == "component" + assert component_entity.name == "old-component" + assert component_entity.label == "Old Component" + assert component_entity.owner == "platform" + assert component_entity.type == "database" + assert component_entity.links.all().count() == 1 + assert component_entity.group is not None + assert component_entity.group.product_owner == "Old John" + assert component_entity.group.project_owner == "Old Doe" + assert component_entity.service is None + assert component_entity.library is None + + uut.update_project_from_entity_file(proj=repository_dict) + + component_entity = Entity.objects.first() + assert Entity.objects.all().count() == 1 + assert component_entity.kind == "component" + assert component_entity.name == "new-component" + assert component_entity.label == "New Component" + assert component_entity.owner == "platform" + assert component_entity.type == "database" + assert component_entity.links.all().count() == 2 + assert component_entity.group is not None + assert component_entity.group.product_owner == "New John" + assert component_entity.group.project_owner == "New Doe" + assert component_entity.service is None + assert component_entity.library is None + + +def test_update_service_library( + mocker, + component_base_factory, + service_factory, + repository_factory, + library_factory, + group_factory, +): + repository = repository_factory( + id=12345, + remote_id=12463, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + group_1 = group_factory(id=1, product_owner="Old John", project_owner="Old Doe") + group_1.save() + + group_2 = group_factory(id=2, product_owner="Old John", project_owner="Old Doe") + group_2.save() + service = service_factory(repository=repository) + library = library_factory(repository=repository) + component_base_factory( + source=repository, service=service, group=group_1, type="service" + ) + component_base_factory( + source=repository, library=library, group=group_2, type="library" + ) + + component_service_and_library = """ +apiVersion: v1alpha1 +kind: component +metadata: + name: new-service + label: New Service + owner: platform + group: + product_owner: New John + project_owner: New Doe + maintainers: [] + description: New Description + tags: [] + links: + - name: Datadog + url: https://service.dashboard.datadog.com + icon: poop + - name: Sentry + url: https://service..skypicker.com +spec: + type: service + environments: + - name: production + dashboard_url: https://dashboard.datadog.com + health_check_url: https://dashboard.datadog.com + service_urls: + - https://service.prod.com + - name: sandbox + dashboard_url: https://dashboard.datadog.sandbox.com + health_check_url: https://dashboard.datadog.sandbox.com + service_urls: + - https://service.sandbox.com + impact: profit + integrations: + pagerduty_service: pagerduty_service1234 + sentry_project: sentry project 15234 + lifecycle: production +--- +apiVersion: v1alpha1 +kind: component +metadata: + name: new-lib + label: New Lib + owner: platform + group: + product_owner: john + project_owner: doe + maintainers: [] + description: This is my fancy component + tags: [] + links: + - name: Datadog + url: https://dashboard.datadog.com + icon: poop + - name: Sentry + url: https://sentry.skypicker.com +spec: + type: library + impact: profit + integrations: + sonarqube_project: sonarqube + lifecycle: production +""" + + repository_dict = { + "remote_id": repository.remote_id, + "id": repository.id, + "provider": repository.provider, + "name": repository.name, + } + mocker.patch( + "zoo.repos.tasks.get_entity_file_content", + return_value=component_service_and_library, + ) + + assert Entity.objects.all().count() == 2 + assert Service.objects.all().count() == 1 + assert Library.objects.all().count() == 1 + + uut.update_project_from_entity_file(proj=repository_dict) + + assert Entity.objects.all().count() == 2 + assert Service.objects.all().count() == 1 + assert Library.objects.all().count() == 1 + + service_entity = Entity.objects.filter(service__isnull=False).first() + library_entity = Entity.objects.filter(library__isnull=False).first() + + assert service_entity.kind == "component" + assert service_entity.type == "service" + assert service_entity.name == "new-service" + assert service_entity.label == "New Service" + assert service_entity.owner == "platform" + assert service_entity.links.all().count() == 2 + assert service_entity.service.environments.all().count() == 2 + + assert library_entity.kind == "component" + assert library_entity.type == "library" + assert library_entity.name == "new-lib" + assert library_entity.label == "New Lib" + assert library_entity.owner == "platform" + assert library_entity.links.all().count() == 2 + + +def test_delete_entities( + mocker, + component_base_factory, + service_factory, + repository_factory, + library_factory, + group_factory, +): + repository = repository_factory( + id=22, + remote_id=22, + name="test_proj1", + owner="john_doe1", + url="https://github.com/john_doe1/test_proj1", + provider="github", + ) + group_1 = group_factory(id=1, product_owner="Old John", project_owner="Old Doe") + group_1.save() + + group_2 = group_factory(id=2, product_owner="Old John", project_owner="Old Doe") + group_2.save() + + service = service_factory(repository=repository) + library = library_factory(repository=repository) + component_base_factory(source=repository, service=service, group=group_1) + component_base_factory(source=repository, library=library, group=group_2) + + component_service_and_library = "" + + repository_dict = { + "remote_id": repository.remote_id, + "id": repository.id, + "provider": repository.provider, + "name": repository.name, + } + mocker.patch( + "zoo.repos.tasks.get_entity_file_content", + return_value=component_service_and_library, + ) + + assert Entity.objects.all().count() == 2 + assert Service.objects.all().count() == 1 + assert Library.objects.all().count() == 1 + + uut.update_project_from_entity_file(proj=repository_dict) + + assert Entity.objects.all().count() == 0 + assert Service.objects.all().count() == 0 + assert Library.objects.all().count() == 0 diff --git a/test/globalsearch/test_indexer.py b/test/globalsearch/test_indexer.py index 0f4e5034..79dfd7a7 100644 --- a/test/globalsearch/test_indexer.py +++ b/test/globalsearch/test_indexer.py @@ -11,9 +11,7 @@ def _initialize(client, models): for model, index in models: - client.create_index( - uid=index, options={"name": model.__name__, "primaryKey": "id"} - ) + client.create_index(uid=index, options={"primaryKey": "id"}) def test_index_model(): diff --git a/test/globalsearch/test_utils.py b/test/globalsearch/test_utils.py index 7fa255ab..c42aaa1a 100644 --- a/test/globalsearch/test_utils.py +++ b/test/globalsearch/test_utils.py @@ -16,7 +16,7 @@ def test_model_instance_serialization(): "owner": "zoo", "name": "jungle", "description": "", - "status": None, + "lifecycle": None, "impact": None, "tier": None, "slack_channel": None, @@ -25,7 +25,7 @@ def test_model_instance_serialization(): "rating_grade": None, "rating_reason": None, "repository": None, - "pagerduty_service": "", + "pagerduty_service": None, "docs_url": None, "owner_slug": "zoo", "name_slug": "jungle", diff --git a/test/repos/tasks/test_sync_zoo_file.py b/test/repos/tasks/test_sync_zoo_file.py deleted file mode 100644 index 79ad1fd8..00000000 --- a/test/repos/tasks/test_sync_zoo_file.py +++ /dev/null @@ -1,157 +0,0 @@ -from typing import Dict, List, Union - -import pytest - -from zoo.repos import tasks as uut -from zoo.repos.models import Repository -from zoo.repos.zoo_yml import parse -from zoo.services.models import Environment, Service - -pytestmark = pytest.mark.django_db - - -@pytest.fixture() -def generate_repositories(repository_factory): - repository_factory( - id=1, - remote_id=11, - name="test_proj1", - owner="john_doe1", - url="https://github.com/john_doe1/test_proj1", - provider="github", - ) - - -def test_update_or_create_service(generate_repositories): - data = { - "type": "service", - "name": "test_proj1", - "owner": "john_doe1", - "impact": "profit", - "status": "beta", - "docs_url": "http://test_proj1/docs", - "slack_channel": "#test_proj1", - "sentry_project": "http://test_proj1/sentry", - "sonarqube_project": "http://test_proj1/sonarqube", - "pagerduty_service": "/services", - "tags": ["tag1", "tag2", "tag3"], - "environments": [ - { - "name": "staging", - "dashboard_url": "http://staging.test_proj1/dashboard", - "service_urls": [ - "http://staging.test_proj1/service1", - "http://staging.test_proj1/service2", - ], - "health_check_url": "http://staging.test_proj1/health_check", - }, - { - "name": "production", - "dashboard_url": "http://production.test_proj1/dashboard", - "service_urls": [ - "http://production.test_proj1/service1", - "http://production.test_proj1/service2", - ], - "health_check_url": "http://production.test_proj1/health_check", - }, - ], - } - - proj = {"id": 11, "provider": "github"} - - uut.update_or_create_service(data, proj) - service = Service.objects.filter(owner=data["owner"], name=data["name"]).first() - assert ( - service is not None - ), f"Service with owner: {data['owner']} and name: {data['name']} not found" - assert_service(service, data) - - envs = Environment.objects.filter(service=service) - assert envs.count() == 2, f"Got {envs.count()} , want: 2 environments" - - for env in envs.all(): - expected = get_expected_env(env.name, data["environments"]) - assert expected is not None - assert_environment(env, expected) - - -def test_update_project_from_zoo_file(mocker): - zoo_yml = """ - type: service - name: test_proj1 - owner: john_doe1 - impact: profit - status: beta - docs_url: http://test_proj1/docs - slack_channel: "#test_proj1" - sentry_project: http://test_proj1/sentry - sonarqube_project: http://test_proj1/sonarqube - pagerduty_service: /services - tags: - - tag1 - - tag2 - - tag3 - environments: - - - name: staging - dashboard_url: http://staging.test_proj1/dashboard - service_urls: - - http://staging.test_proj1/service1 - - http://staging.test_proj1/service2 - health_check_url: http://staging.test_proj1/health_check - - - name: production - dashboard_url: http://production.test_proj1/dashboard - service_urls: - - http://production.test_proj1/service1 - - http://production.test_proj1/service2 - health_check_url: http://production.test_proj1/health_check - """ - data = parse(zoo_yml) - - m_get_zoo_file_content = mocker.patch( - "zoo.repos.tasks.get_zoo_file_content", return_value=zoo_yml - ) - m_update_or_create_service = mocker.patch( - "zoo.repos.tasks.update_or_create_service", return_value=None - ) - - proj = {"id": 11, "provider": "github"} - uut.update_project_from_zoo_file(proj) - - m_get_zoo_file_content.assert_called_once_with(proj) - m_update_or_create_service.assert_called_once_with(data, proj) - - -def assert_service(got: Service, expected: Dict) -> None: - assert got.owner == expected["owner"] - assert got.name == expected["name"] - assert got.impact == expected["impact"] - assert got.status == expected["status"] - assert got.docs_url == expected["docs_url"] - assert got.slack_channel == expected["slack_channel"] - assert got.sentry_project == expected["sentry_project"] - assert got.sonarqube_project == expected["sonarqube_project"] - assert got.pagerduty_service == expected["pagerduty_service"] - assert_tags(got.tags, expected["tags"]) - - -def assert_environment(got: Environment, expected: Dict) -> None: - assert got.name == expected["name"] - assert got.dashboard_url == expected["dashboard_url"] - assert len(got.service_urls) == len(expected["service_urls"]) - assert got.health_check_url == expected["health_check_url"] - - -def assert_tags(got: List, expected: List): - # because pre_save signal on Service - - assert len(got) == len(expected) - assert sorted(got) == sorted(expected) - - -def get_expected_env(name: str, envs: List) -> Union[Dict, None]: - for env in envs: - if env["name"] == name: - return env - return None diff --git a/test/repos/test_zoo_yml.py b/test/repos/test_zoo_yml.py deleted file mode 100644 index 418c1124..00000000 --- a/test/repos/test_zoo_yml.py +++ /dev/null @@ -1,94 +0,0 @@ -import pytest -import yaml - -from zoo.repos import zoo_yml as uut -from zoo.repos.models import Repository -from zoo.services.models import Environment, Service - -pytestmark = pytest.mark.django_db - - -@pytest.fixture -def generate_services_with_environments_and_links( - service_factory, environment_factory, link_factory -): - service = service_factory( - id=1, - name="martinez", - owner="michaelbennett", - impact="profit", - docs_url="https://docsurl", - pagerduty_service="/services", - slack_channel="https://slackchannel", - status="beta", - repository__id=78, - repository__remote_id=239, - repository__owner="jasckson", - repository__name="thiwer", - repository__url="https://gitlab.com/thiwer/thiwer", - ) - environment_factory( - id=1, - service=service, - name="production", - service_urls=["https://serviceurl1", "https://serviceurl2"], - dashboard_url="https://dashboardurl", - health_check_url="https://healthcheckurl", - ) - environment_factory( - id=2, - service=service, - name="staging", - service_urls=["https://serviceurl1", "https://serviceurl2"], - dashboard_url="https://dashboardurl", - ) - link_factory( - id=1, - name="Datadog", - url="https://dashboard.datadog.com", - icon="poop", - service=service, - ) - link_factory( - id=2, name="Sentry", url="https://dashboard.sentry.com", service=service - ) - - -def test_generate(generate_services_with_environments_and_links): - expected = """ -type: service -name: martinez -owner: michaelbennett -impact: profit -status: beta -docs_url: https://docsurl -slack_channel: https://slackchannel -sentry_project: null -sonarqube_project: null -pagerduty_service: /services -tags: [] -environments: -- name: production - dashboard_url: https://dashboardurl - health_check_url: https://healthcheckurl - service_urls: - - https://serviceurl1 - - https://serviceurl2 -- name: staging - dashboard_url: https://dashboardurl - health_check_url: null - service_urls: - - https://serviceurl1 - - https://serviceurl2 -links: -- name: Datadog - url: https://dashboard.datadog.com - icon: poop -- name: Sentry - url: https://dashboard.sentry.com - icon: null -""" - service_1 = Service.objects.get(pk=1) - content = uut.generate(service_1) - assert uut.validate(content) - assert expected.strip() == content.strip() diff --git a/test/services/test_forms.py b/test/services/test_forms.py deleted file mode 100644 index 92b07c7d..00000000 --- a/test/services/test_forms.py +++ /dev/null @@ -1,168 +0,0 @@ -import pytest -from faker import Faker - -from zoo.instance.models import Helpers, Placeholders -from zoo.repos.models import Repository -from zoo.services.forms import ( - ServiceEnvironmentsFormSet, - ServiceForm, - ServiceLinksFormSet, -) -from zoo.services.models import Service - -pytestmark = pytest.mark.django_db - - -def test_service_form__metadata__basic(): - fake = Faker() - form = ServiceForm( - data={"owner": fake.user_name(), "name": fake.word(), "exclusions": fake.word()} - ) - - assert form.is_valid() - assert form.fields["owner"].help_text == getattr(Service.owner, "help_text", "") - assert form.fields["owner"].widget.attrs["placeholder"] == "" - - -def test_service_form__metadata__overridden(): - fake = Faker() - Helpers(service_owner="customhelper").save() - Placeholders(service_owner="customplaceholder").save() - - form = ServiceForm( - data={"owner": fake.user_name(), "name": fake.word(), "exclusions": fake.word()} - ) - - assert form.is_valid() - assert form.fields["owner"].help_text == Helpers.resolve().service_owner - assert ( - form.fields["owner"].widget.attrs["placeholder"] - == Placeholders.resolve().service_owner - ) - - -def test_service_form__basic__correct(): - fake = Faker() - form = ServiceForm( - data={"owner": fake.user_name(), "name": fake.word(), "exclusions": fake.word()} - ) - - assert form.is_valid() - new_service = form.save() - assert Service.objects.filter(id=new_service.id).exists() - - -def test_service_form__basic__incorrect(): - fake = Faker() - form = ServiceForm(data={"owner": "a" * 101, "name": fake.word()}) - - assert not form.is_valid() - assert form.errors == { - "owner": ["Ensure this value has at most 100 characters (it has 101)."] - } - - -fake = Faker() -service_form_data = { - "owner": fake.user_name(), - "name": fake.word(), - "description": fake.sentence(), - "status": "production", - "impact": "profit", - "tier": "", - "slack_channel": "dev-null", - "repository": "", - "pagerduty_service": fake.word(), - "docs_url": fake.url(), - "service_url": fake.url(), - "exclusions": fake.word(), -} - - -def test_service_form__complete__correct(repository): - form = ServiceForm(data={**service_form_data, "repository": repository.pk}) - - assert form.is_valid() - - -def test_service_form__exclusions__correct(): - repository = Repository.objects.create(remote_id=1) - service = Service.objects.create(repository=repository) - form = ServiceForm( - instance=service, data={**service_form_data, "repository": repository.pk} - ) - form.save() - assert repository.exclusions == service_form_data["exclusions"] - - -def test_service_form__complete__incorrect_status(repository): - form = ServiceForm( - data={**service_form_data, "status": "live", "repository": repository.pk} - ) - - assert not form.is_valid() - assert form.errors == { - "status": ["Select a valid choice. live is not one of the available choices."] - } - - -service_environments_form__set_data = { - "environments-TOTAL_FORMS": "2", - "environments-INITIAL_FORMS": "0", - "environments-MIN_NUM_FORMS": "2", - "environments-MAX_NUM_FORMS": "2", - "environments-0-name": fake.word(), - "environments-0-healthcheck_url": fake.url(), - "environments-0-service_urls_0": fake.url(), - "environments-0-service_urls_1": fake.url(), - "environments-0-DELETE": False, - "environments-0-type": "zoo", - "environments-1-name": fake.word(), - "environments-1-dashboard_url": fake.url(), - "environments-1-service_urls_0": fake.url(), - "environments-1-service_urls_1": fake.url(), - "environments-1-DELETE": False, - "environments-1-type": "zoo", -} - - -service_links_form__set_data = { - "links-TOTAL_FORMS": "2", - "links-INITIAL_FORMS": "0", - "links-MIN_NUM_FORMS": "2", - "links-MAX_NUM_FORMS": "2", - "links-0-name": fake.word(), - "links-0-url": fake.url(), - "links-0-icon": fake.word(), - "links-0-DELETE": False, - "links-1-name": fake.word(), - "links-1-url": fake.url(), - "links-1-icon": fake.word(), - "links-1-DELETE": False, -} - - -def test_service_environment_formset__complete__correct(repository): - form = ServiceEnvironmentsFormSet(data=service_environments_form__set_data) - - assert form.is_valid() - - -def test_service_link_formset__complete__correct(repository): - form = ServiceLinksFormSet(data=service_links_form__set_data) - - assert form.is_valid() - - -def test_service_environment_formset__complete__incorrect(repository): - form = ServiceEnvironmentsFormSet( - {**service_environments_form__set_data, "environments-0-dashboard_url": "-"} - ) - assert not form.is_valid() - assert form.forms[0].errors == {"dashboard_url": ["Enter a valid URL."]} - - -def test_service_link_formset__complete__incorrect(repository): - form = ServiceLinksFormSet({**service_links_form__set_data, "links-0-url": "-"}) - assert not form.is_valid() - assert form.forms[0].errors == {"url": ["Enter a valid URL."]} diff --git a/zoo/api/query.py b/zoo/api/query.py index 32b734f0..27ff7db4 100644 --- a/zoo/api/query.py +++ b/zoo/api/query.py @@ -6,8 +6,10 @@ from ..analytics.models import Dependency, DependencyType, DependencyUsage from ..auditing.models import Issue +from ..entities.models import Entity, Group, Link from ..globalsearch.indexer import IndexType from ..globalsearch.meili_client import meili_client +from ..libraries.models import Library from ..repos.models import Repository from ..services.models import Service from . import types @@ -48,6 +50,22 @@ class Query(graphene.ObjectType): search_query=graphene.String(), search_type=SearchTypeEnum(), ) + all_entities = relay.ConnectionField( + types.EntityConnection, + description="List of entities. Returns first 10 nodes if pagination is not specified.", + ) + all_libraries = relay.ConnectionField( + types.LibraryConnection, + description="List of libraries. Returns first 10 nodes if pagination is not specified.", + ) + all_links = relay.ConnectionField( + types.LinkConnection, + description="List of links. Returns first 10 nodes if pagination is not specified.", + ) + all_groups = relay.ConnectionField( + types.GroupConnection, + description="List of groups. Returns first 10 nodes if pagination is not specified.", + ) def resolve_all_issues(self, info, **kwargs): paginator = Paginator(**kwargs) @@ -156,6 +174,80 @@ def resolve_all_dependency_usages(self, info, **kwargs): page_info=page_info, edges=edges, total_count=total ) + def resolve_all_entities(self, info, **kwargs): + paginator = Paginator(**kwargs) + total = Entity.objects.all().count() + page_info = paginator.get_page_info(total) + edges = [] + + for i, entity in enumerate( + Entity.objects.all()[ + paginator.slice_from : paginator.slice_to # Ignore PEP8Bear + ] + ): + cursor = paginator.get_edge_cursor(i + 1) + node = types.Entity.from_db(entity) + edges.append(types.EntityConnection.Edge(node=node, cursor=cursor)) + + return types.EntityConnection( + page_info=page_info, edges=edges, total_count=total + ) + + def resolve_all_links(self, info, **kwargs): + paginator = Paginator(**kwargs) + total = Link.objects.all().count() + page_info = paginator.get_page_info(total) + edges = [] + + for i, link in enumerate( + Link.objects.all()[ + paginator.slice_from : paginator.slice_to # Ignore PEP8Bear + ] + ): + cursor = paginator.get_edge_cursor(i + 1) + node = types.Link.from_db(link) + edges.append(types.LinkConnection.Edge(node=node, cursor=cursor)) + + return types.LinkConnection(page_info=page_info, edges=edges, total_count=total) + + def resolve_all_libraries(self, info, **kwargs): + paginator = Paginator(**kwargs) + total = Library.objects.all().count() + page_info = paginator.get_page_info(total) + edges = [] + + for i, library in enumerate( + Library.objects.all()[ + paginator.slice_from : paginator.slice_to # Ignore PEP8Bear + ] + ): + cursor = paginator.get_edge_cursor(i + 1) + node = types.Library.from_db(library) + edges.append(types.LibraryConnection.Edge(node=node, cursor=cursor)) + + return types.LibraryConnection( + page_info=page_info, edges=edges, total_count=total + ) + + def resolve_all_groups(self, info, **kwargs): + paginator = Paginator(**kwargs) + total = Group.objects.all().count() + page_info = paginator.get_page_info(total) + edges = [] + + for i, group in enumerate( + Group.objects.all()[ + paginator.slice_from : paginator.slice_to # Ignore PEP8Bear + ] + ): + cursor = paginator.get_edge_cursor(i + 1) + node = types.Group.from_db(group) + edges.append(types.GroupConnection.Edge(node=node, cursor=cursor)) + + return types.GroupConnection( + page_info=page_info, edges=edges, total_count=total + ) + def resolve_all_search_results(self, info, **kwargs): paginator = Paginator(**kwargs) query_param = kwargs.get("search_query", "") diff --git a/zoo/api/schema.py b/zoo/api/schema.py index 34d353e3..88c3aca2 100644 --- a/zoo/api/schema.py +++ b/zoo/api/schema.py @@ -20,7 +20,11 @@ class Mutation(ZooMutation, graphene.ObjectType): types.CheckResult, types.Dependency, types.DependencyUsage, + types.Entity, + types.Group, types.Issue, + types.Library, + types.Link, types.Repository, types.Service, ], diff --git a/zoo/api/types.py b/zoo/api/types.py index 4b31e3ab..0f49842e 100644 --- a/zoo/api/types.py +++ b/zoo/api/types.py @@ -6,6 +6,8 @@ from ..analytics import models as analytics_models from ..auditing import check_discovery from ..auditing import models as auditing_models +from ..entities import models as entities_models +from ..libraries import models as libraries_models from ..repos import models as repos_models from ..services import models as services_models from .paginator import Paginator @@ -95,11 +97,64 @@ class Meta: interfaces = (relay.Node,) +class Library(graphene.ObjectType): + owner = graphene.String() + name = graphene.String() + lifecycle = graphene.String() + impact = graphene.String() + slack_channel = graphene.String() + sonarqube_project = graphene.String() + repository = graphene.Field(lambda: Repository) + docs_url = graphene.String() + library_url = graphene.String() + + @classmethod + def from_db(cls, library): + return cls( + id=library.id, + owner=library.owner, + name=library.name, + lifecycle=library.lifecycle, + impact=library.impact, + slack_channel=library.slack_channel, + sonarqube_project=library.sonarqube_project, + repository=library.repository_id, + docs_url=library.docs_url, + library_url=library.library_url, + ) + + @classmethod + def get_node(cls, info, library_id): + try: + library = libraries_models.Library.objects.get(id=library_id) + return cls.from_db(library) + except ObjectDoesNotExist: + return None + + def resolve_repository(self, info): + try: + return Repository.from_db( + repos_models.Repository.objects.get(id=self.repository) + ) + except ObjectDoesNotExist: + return None + + class Meta: + interfaces = (relay.Node,) + + +class LibraryConnection(relay.Connection): + total_count = graphene.Int() + + class Meta: + node = Library + + class Link(graphene.ObjectType): name = graphene.String() - url = graphene.List(graphene.String) + url = graphene.String() icon = graphene.String() - service = graphene.Field(lambda: Service) + entity = graphene.Field(lambda: Entity) @classmethod def from_db(cls, link): @@ -108,52 +163,166 @@ def from_db(cls, link): name=link.name, url=link.url, icon=link.icon, - service=link.service_id, + entity=link.entity_id, ) @classmethod def get_node(cls, info, link_id): try: - link = services_models.Link.objects.get(id=link_id) + link = entities_models.Link.objects.get(id=link_id) return cls.from_db(link) except ObjectDoesNotExist: return None + def resolve_entity(self, info): + try: + return Entity.from_db(entities_models.Entity.objects.get(id=self.entity)) + except ObjectDoesNotExist: + return None + + class Meta: + interfaces = (relay.Node,) + + +class LinkConnection(relay.Connection): + total_count = graphene.Int() + + class Meta: + node = Link + + +class Group(graphene.ObjectType): + product_owner = graphene.String() + project_owner = graphene.String() + maintainers = graphene.List(graphene.String) + + class Meta: + interfaces = (relay.Node,) + + @classmethod + def from_db(cls, group): + return cls( + id=group.id, + product_owner=group.product_owner, + project_owner=group.project_owner, + maintainers=group.maintainers, + ) + + @classmethod + def get_node(cls, info, group_id): + try: + group = entities_models.Group.objects.get(id=group_id) + return cls.from_db(group) + except ObjectDoesNotExist: + return None + + +class GroupConnection(relay.Connection): + total_count = graphene.Int() + + class Meta: + node = Group + + +class Entity(graphene.ObjectType): + name = graphene.String() + label = graphene.String() + kind = graphene.String() + type = graphene.String() + owner = graphene.String() + description = graphene.String() + group = graphene.Field(lambda: Group) + source = graphene.Field(lambda: Repository) + service = graphene.Field(lambda: Service) + library = graphene.Field(lambda: Library) + all_links = relay.ConnectionField(LinkConnection) + + @classmethod + def from_db(cls, entity): + return cls( + id=entity.id, + name=entity.name, + label=entity.label, + owner=entity.owner, + description=entity.description, + group=entity.group_id, + service=entity.service_id, + library=entity.library_id, + all_links=entity.links, + ) + + @classmethod + def get_node(cls, info, entity_id): + try: + entity = entities_models.Entity.objects.get(id=entity_id) + return cls.from_db(entity) + except ObjectDoesNotExist: + return None + def resolve_service(self, info): try: return Service.from_db(services_models.Service.objects.get(id=self.service)) except ObjectDoesNotExist: return None + def resolve_library(self, info): + try: + return Library.from_db( + libraries_models.Library.objects.get(id=self.library) + ) + except ObjectDoesNotExist: + return None + + def resolve_group(self, info): + try: + return Group.from_db(entities_models.Group.objects.get(id=self.group)) + except ObjectDoesNotExist: + return None + + def resolve_all_links(self, info, **kwargs): + paginator = Paginator(**kwargs) + edges = [] + filtered_links = entities_models.Link.objects.filter(entity_id=self.id) + total = filtered_links.count() + page_info = paginator.get_page_info(total) + + for i, issue in enumerate( + filtered_links[paginator.slice_from : paginator.slice_to] # Ignore PEP8Bear + ): + cursor = paginator.get_edge_cursor(i + 1) + node = Link.from_db(issue) + edges.append(LinkConnection.Edge(node=node, cursor=cursor)) + + return LinkConnection(page_info=page_info, edges=edges, total_count=total) + class Meta: interfaces = (relay.Node,) -class EnvironmentConnection(relay.Connection): +class EntityConnection(relay.Connection): total_count = graphene.Int() class Meta: - node = Environment + node = Entity -class LinkConnection(relay.Connection): +class EnvironmentConnection(relay.Connection): total_count = graphene.Int() class Meta: - node = Link + node = Environment class Service(graphene.ObjectType): owner = graphene.String() name = graphene.String() - status = graphene.String() + lifecycle = graphene.String() impact = graphene.String() repository = graphene.Field(lambda: Repository) slack_channel = graphene.String() pagerduty_service = graphene.String() docs_url = graphene.String() all_environments = relay.ConnectionField(EnvironmentConnection) - all_links = relay.ConnectionField(LinkConnection) @classmethod def from_db(cls, service): @@ -161,7 +330,7 @@ def from_db(cls, service): id=service.id, owner=service.owner, name=service.name, - status=service.status, + lifecycle=service.lifecycle, impact=service.impact, slack_channel=service.slack_channel, pagerduty_service=service.pagerduty_service, diff --git a/zoo/base/apps.py b/zoo/base/apps.py index 68dff2b3..3b924fb2 100644 --- a/zoo/base/apps.py +++ b/zoo/base/apps.py @@ -24,7 +24,7 @@ def ready(self): celery_app.add_periodic_task(timedelta(hours=1), repos_tasks.sync_repos) celery_app.add_periodic_task(timedelta(hours=1), repos_tasks.schedule_pulls) - celery_app.add_periodic_task(timedelta(days=1), repos_tasks.sync_zoo_file) + celery_app.add_periodic_task(timedelta(days=1), repos_tasks.sync_entity_file) celery_app.add_periodic_task( timedelta(hours=1), service_tasks.schedule_sentry_sync ) diff --git a/zoo/base/management/commands/generatezooyml.py b/zoo/base/management/commands/generatezooyml.py index 7cfbb1da..c137126b 100644 --- a/zoo/base/management/commands/generatezooyml.py +++ b/zoo/base/management/commands/generatezooyml.py @@ -1,14 +1,14 @@ from django.core.management.base import BaseCommand -from zoo.repos import github, gitlab, zoo_yml +from zoo.repos import entities_yaml, github, gitlab from zoo.services.models import Service class Command(BaseCommand): - help = "generate .zoo.yml file for all services in the database that do not have it" + help = "generate .entities.yml file for all entities in the database that do not have it" - ZOO_YML = ".zoo.yml" - ZOO_COMMIT_MSG = "feat(zoo): generate .zoo.yml file" + ENTITY_YML = ".entities.yml" + ENTITY_COMMIT_MSG = "feat(zoo): generate .entities.yml file" def handle(self, *args, **options): for service in Service.objects.all(): @@ -19,17 +19,17 @@ def handle(self, *args, **options): if not provider: continue - if self.file_exists(remote_id, Command.ZOO_YML, provider): + if self.file_exists(remote_id, Command.ENTITY_YML, provider): continue - yml = zoo_yml.generate(service) + yml = entities_yaml.generate(service) actions = [ - {"action": "create", "content": yml, "file_path": Command.ZOO_YML} + {"action": "create", "content": yml, "file_path": Command.ENTITY_YML} ] branch = "master" provider.create_remote_commit( - remote_id, Command.ZOO_COMMIT_MSG, actions, branch, provider + remote_id, Command.ENTITY_COMMIT_MSG, actions, branch, provider ) def get_provider(self, provider): diff --git a/zoo/base/settings.py b/zoo/base/settings.py index f19318d4..9b04dbad 100644 --- a/zoo/base/settings.py +++ b/zoo/base/settings.py @@ -113,6 +113,7 @@ "zoo.repos.apps.ReposConfig", "zoo.resources.apps.ResourcesConfig", "zoo.services.apps.ServicesConfig", + "zoo.entities.apps.EntitiesConfig", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -247,9 +248,6 @@ ZOO_AUDITING_CHECKS = env("ZOO_AUDITING_CHECKS") ZOO_AUDITING_DROP_ISSUES = env("ZOO_AUDITING_DROP_ISSUES") -ZOO_YAML_FILE = env("ZOO_YAML_FILE") -ZOO_YAML_DEFAULT_REF = env("ZOO_YAML_DEFAULT_REF") - AWS_CONFIG = env("AWS_CONFIG") AWS_CONFIG_FILE = env("AWS_CONFIG_FILE") AWS_CREDENTIALS = env("AWS_SHARED_CREDENTIALS") @@ -273,3 +271,6 @@ MEILI_MASTER_KEY = env("MEILI_MASTER_KEY") MEILI_HOST = env("MEILI_HOST") logs.configure_structlog(DEBUG) + +ZOO_ALLOWD_ENTITY_KINDS = ["component"] +ZOO_ALLOWED_COMPONENT_TYPES = ["service", "library"] diff --git a/zoo/base/templates/shared/base_list.html b/zoo/base/templates/shared/base_list.html index 50400215..e2db5922 100644 --- a/zoo/base/templates/shared/base_list.html +++ b/zoo/base/templates/shared/base_list.html @@ -89,10 +89,10 @@

{% endif %} -{% if project_create_view %} - - - -{% endif %} +{#{% if project_create_view %}#} +{# #} +{# #} +{# #} +{#{% endif %}#} diff --git a/zoo/base/templates/shared/project_detail.html b/zoo/base/templates/shared/project_detail.html index edcdda66..978d525d 100644 --- a/zoo/base/templates/shared/project_detail.html +++ b/zoo/base/templates/shared/project_detail.html @@ -10,8 +10,8 @@ {{ block.super }}
- {% if object.status %} - status:{{ object.status }} + {% if object.lifecycle %} + lifecycle:{{ object.lifecycle }} {% endif %} {% if object.impact %} @@ -21,20 +21,6 @@ {% block navbuttons %} {% endblock %} -
{% endblock %} diff --git a/zoo/base/templatetags/utils.py b/zoo/base/templatetags/utils.py index 0c5b92ba..0abb6402 100644 --- a/zoo/base/templatetags/utils.py +++ b/zoo/base/templatetags/utils.py @@ -12,7 +12,8 @@ from ...analytics.models import DependencyType, DependencyUsage from ...auditing.check_discovery import Effort, Severity from ...objectives.models import Objective -from ...services.models import Impact, SentryIssueCategory, Status +from ...services.constants import Lifecycle +from ...services.models import Impact, SentryIssueCategory register = template.Library() @@ -32,11 +33,11 @@ } known_colors = { - "status": { - Status.BETA.value: "yellow", - Status.DEPRECATED.value: "red", - Status.DISCONTINUED.value: "grey", - Status.PRODUCTION.value: "green", + "lifecycle": { + Lifecycle.BETA.value: "yellow", + Lifecycle.DEPRECATED.value: "red", + Lifecycle.DISCONTINUED.value: "grey", + Lifecycle.PRODUCTION.value: "green", }, "impact": { Impact.CUSTOMERS.value: "red", diff --git a/zoo/checklists/templates/checklists/service_checklist.html b/zoo/checklists/templates/checklists/service_checklist.html index 561d4dc9..b4ebfcb2 100644 --- a/zoo/checklists/templates/checklists/service_checklist.html +++ b/zoo/checklists/templates/checklists/service_checklist.html @@ -53,7 +53,6 @@

No checklist steps found for the current tags - Edit service {% endfor %} {% endblock %} diff --git a/zoo/checklists/views.py b/zoo/checklists/views.py index 006ec639..f251b653 100644 --- a/zoo/checklists/views.py +++ b/zoo/checklists/views.py @@ -26,7 +26,7 @@ def get_queryset(self): return ( super() .get_queryset() - .filter(status=service_models.Status.BETA.value) + .filter(lifecycle=service_models.Lifecycle.BETA.value) .prefetch_related("checkmarks") ) @@ -62,7 +62,7 @@ def get_context_data(self, **kwargs): service = service_models.Service.objects.get( owner_slug=service_owner_slug, name_slug=service_name_slug, - status=service_models.Status.BETA.value, + lifecycle=service_models.Lifecycle.BETA.value, ) except service_models.Service.DoesNotExist: raise Http404 diff --git a/zoo/entities/__init__.py b/zoo/entities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zoo/entities/admin.py b/zoo/entities/admin.py new file mode 100644 index 00000000..ac18c886 --- /dev/null +++ b/zoo/entities/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from . import models + + +@admin.register(models.Entity) +class EntityAdmin(admin.ModelAdmin): + search_fields = ("name", "owner", "kind", "type") + + +@admin.register(models.Link) +class LinkAdmin(admin.ModelAdmin): + search_fields = ("name",) + + +@admin.register(models.Group) +class GroupAdmin(admin.ModelAdmin): + search_fields = ("product_owner", "project_owner") diff --git a/zoo/entities/apps.py b/zoo/entities/apps.py new file mode 100644 index 00000000..af049ded --- /dev/null +++ b/zoo/entities/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class EntitiesConfig(AppConfig): + name = "zoo.entities" diff --git a/zoo/entities/builder.py b/zoo/entities/builder.py new file mode 100644 index 00000000..b6e5bdcc --- /dev/null +++ b/zoo/entities/builder.py @@ -0,0 +1,97 @@ +from zoo.libraries.models import Library +from zoo.services.models import Environment, Service + +from .models import Entity, Group, Link + + +# TODO: refactor to registry pattern(or any better one) +class EntityBuilder: + def sync_entities(self, data, repository): + if data["kind"] != "component": + return NotImplemented + if data["spec"]["type"] == "service": + self._build_service(data, repository) + elif data["spec"]["type"] == "library": + self._build_library(data, repository) + else: + self._build_base_component(data, repository) + + @staticmethod + def _build_base_component(data, repository): + group = Group.objects.create( + product_owner=data["metadata"].get("group", {}).get("product_owner"), + project_owner=data["metadata"]["group"]["project_owner"], + maintainers=data["metadata"]["group"]["maintainers"], + ) + + obj = Entity.objects.create( + name=data["metadata"]["name"], + label=data["metadata"]["label"], + kind=data["kind"], + type=data["spec"]["type"], + source=repository, + owner=data["metadata"]["owner"], + description=data["metadata"]["description"], + tags=data["metadata"]["tags"], + group=group, + ) + + for link in data["metadata"]["links"]: + Link.objects.create( + url=link["url"], + entity=obj, + name=link["name"], + icon=getattr(link, "icon", None), + ) + + return obj + + def _build_service(self, data, repository): + base_component = self._build_base_component(data, repository) + + service = Service.objects.create( + name=base_component.label, + owner=base_component.owner, + repository=repository, + lifecycle=data["spec"]["lifecycle"], + impact=data["spec"]["impact"], + sentry_project=data["spec"].get("integrations", {}).get("sentry_project"), + sonarqube_project=data["spec"] + .get("integrations", {}) + .get("sonarqube_project"), + slack_channel=data["spec"].get("integrations", {}).get("slack_channel"), + pagerduty_service=data["spec"] + .get("integrations", {}) + .get("pagerduty_service"), + description=data["metadata"]["description"], + ) + + for env in data["spec"]["environments"]: + Environment.objects.create( + service=service, + name=env.get("name"), + dashboard_url=env.get("dashboard_url"), + health_check_url=env.get("health_check_url"), + service_urls=env.get("service_urls"), + ) + + base_component.service = service + base_component.save() + + def _build_library(self, data, repository): + base_component = self._build_base_component(data, repository) + + library = Library.objects.create( + name=base_component.label, + owner=base_component.owner, + repository=repository, + lifecycle=data["spec"]["lifecycle"], + impact=data["spec"]["impact"], + sonarqube_project=data["spec"] + .get("integrations", {}) + .get("sonarqube_project"), + slack_channel=data["spec"].get("integrations", {}).get("slack_channel"), + ) + + base_component.library = library + base_component.save() diff --git a/zoo/entities/enums.py b/zoo/entities/enums.py new file mode 100644 index 00000000..eedf2bfe --- /dev/null +++ b/zoo/entities/enums.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class Kind(Enum): + COMPONENT = "component" diff --git a/zoo/entities/management/__init__.py b/zoo/entities/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zoo/entities/management/commands/__init__.py b/zoo/entities/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zoo/entities/management/commands/import_entities.py b/zoo/entities/management/commands/import_entities.py new file mode 100644 index 00000000..69b12d1d --- /dev/null +++ b/zoo/entities/management/commands/import_entities.py @@ -0,0 +1,110 @@ +import structlog +from django.core.management.base import BaseCommand + +from zoo.entities.enums import Kind +from zoo.entities.models import Entity, Link +from zoo.libraries.models import Library +from zoo.services.models import Service + +log = structlog.get_logger() + + +class Command(BaseCommand): + help = "Create entities from services and libraries" + + def handle(self, *args, **options): + self.service_to_entity() + self.library_to_entity() + + @staticmethod + def service_to_entity(): + with open("service_exceptions.csv", "w+") as exceptions: + exceptions.write( + "Service name;Service owner;Service repository;Exception\n" + ) + for service in Service.objects.all(): + if service.repository: + log.info("entity.import_services.processing", service=service) + try: + entity = Entity.objects.create( + name=service.name, + owner=service.owner, + description=service.description, + kind=Kind.COMPONENT, + type="service", + source=service.repository, + tags=service.tags, + service=service, + ) + + if service.docs_url: + Link.objects.create( + name="Documentation", + url=service.docs_url, + entity=entity, + ) + + if service.slack_channel: + Link.objects.create( + name="Discussion", + url=f"https://app.slack.com/client/T024Z3H2Y/{service.slack_channel}", + entity=entity, + ) + except Exception as err: + exceptions.write( + f"{getattr(service, 'name', '')};{getattr(service, 'owner', '')};{getattr(service, 'repository', '')};{repr((err))}\n" + ) + else: + exceptions.write( + f"{getattr(service, 'name', '')};{getattr(service, 'owner', '')};;Missing repository for service\n" + ) + + @staticmethod + def library_to_entity(): + with open("library_exceptions.csv", "w+") as exceptions: + exceptions.write( + "Library name;Library owner;Library repository;Exception\n" + ) + for library in Library.objects.all(): + if library.repository: + log.info("entity.import_libraries.processing", library=library) + try: + entity = Entity.objects.create( + name=library.name, + owner=library.owner, + description=library.description, + kind=Kind.COMPONENT, + type="library", + source=library.repository, + tags=library.tags, + library=library, + ) + + if library.docs_url: + Link.objects.create( + name="Documentation", + url=library.docs_url, + entity=entity, + ) + + if library.slack_channel: + Link.objects.create( + name="Discussion", + url=f"https://app.slack.com/client/T024Z3H2Y/{library.slack_channel}", + entity=entity, + ) + + if library.library_url: + Link.objects.create( + name="Library URL", + url=library.library_url, + entity=entity, + ) + except Exception as err: + exceptions.write( + f"{getattr(library, 'name', '')};{getattr(library, 'owner', '')};{getattr(library, 'repository', '')};{repr((err))}\n" + ) + else: + exceptions.write( + f"{getattr(library, 'name', '')};{getattr(library, 'owner', '')};;Missing repository for library\n" + ) diff --git a/zoo/entities/migrations/0001_initial.py b/zoo/entities/migrations/0001_initial.py new file mode 100644 index 00000000..ebfc55e7 --- /dev/null +++ b/zoo/entities/migrations/0001_initial.py @@ -0,0 +1,159 @@ +# Generated by Django 2.2.19 on 2021-07-27 08:29 + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Entity", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("label", models.CharField(max_length=100)), + ("owner", models.CharField(max_length=50)), + ( + "kind", + models.CharField( + blank=True, + choices=[("component", "component")], + max_length=32, + null=True, + ), + ), + ("type", models.CharField(max_length=32)), + ( + "description", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "tags", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=list, + size=None, + ), + ), + ], + ), + migrations.CreateModel( + name="Group", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("product_owner", models.CharField(max_length=100)), + ("project_owner", models.CharField(max_length=100)), + ( + "maintainers", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + default=list, + help_text="List of maintainers", + size=None, + validators=[django.core.validators.EmailValidator], + ), + ), + ], + ), + migrations.CreateModel( + name="Link", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=32)), + ( + "icon", + models.CharField( + blank=True, + help_text="https://fomantic-ui.com/elements/icon.html", + max_length=16, + null=True, + ), + ), + ("url", models.URLField()), + ( + "entity", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="links", + related_query_name="link", + to="entities.Entity", + ), + ), + ], + ), + migrations.AddField( + model_name="entity", + name="group", + field=models.OneToOneField( + default=None, + on_delete=django.db.models.deletion.CASCADE, + to="entities.Group", + ), + ), + migrations.AddField( + model_name="entity", + name="library", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="libraries.Library", + ), + ), + migrations.AddField( + model_name="entity", + name="service", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="services.Service", + ), + ), + migrations.AddField( + model_name="entity", + name="source", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="entities", + related_query_name="entity", + to="repos.Repository", + ), + ), + migrations.AlterUniqueTogether( + name="entity", + unique_together={("name", "source")}, + ), + ] diff --git a/zoo/entities/migrations/0002_allow_null_product_owner_on_group.py b/zoo/entities/migrations/0002_allow_null_product_owner_on_group.py new file mode 100644 index 00000000..a09362f7 --- /dev/null +++ b/zoo/entities/migrations/0002_allow_null_product_owner_on_group.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.19 on 2021-10-07 10:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("entities", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="group", + name="product_owner", + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/zoo/entities/migrations/0003_allow_blank_on_group.py b/zoo/entities/migrations/0003_allow_blank_on_group.py new file mode 100644 index 00000000..39075ade --- /dev/null +++ b/zoo/entities/migrations/0003_allow_blank_on_group.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.19 on 2021-10-13 09:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("entities", "0002_allow_null_product_owner_on_group"), + ] + + operations = [ + migrations.AlterField( + model_name="entity", + name="group", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="entities.Group", + ), + ), + ] diff --git a/zoo/entities/migrations/__init__.py b/zoo/entities/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zoo/entities/models.py b/zoo/entities/models.py new file mode 100644 index 00000000..8013a114 --- /dev/null +++ b/zoo/entities/models.py @@ -0,0 +1,84 @@ +from django.contrib.postgres import fields as pg_fields +from django.core.exceptions import ValidationError +from django.core.validators import EmailValidator +from django.db import models + +from zoo.entities.enums import Kind + + +class Group(models.Model): + product_owner = models.CharField(max_length=100, null=True, blank=True) + project_owner = models.CharField(max_length=100) + maintainers = pg_fields.ArrayField( + base_field=models.CharField(max_length=50), + default=list, + help_text="List of maintainers", + validators=[EmailValidator], + ) + + +class Link(models.Model): + name = models.CharField(max_length=32) + icon = models.CharField( + max_length=16, + null=True, + blank=True, + help_text="https://fomantic-ui.com/elements/icon.html", + ) + url = models.URLField() + entity = models.ForeignKey( + "entities.Entity", + related_name="links", + related_query_name="link", + on_delete=models.CASCADE, + ) + + +class Entity(models.Model): + class Meta: + unique_together = ("name", "source") + + name = models.CharField(max_length=100) + label = models.CharField(max_length=100) + owner = models.CharField(max_length=50) + kind = models.CharField( + choices=((item.value, item.value) for item in Kind), + null=True, + blank=True, + max_length=32, + ) + type = models.CharField(max_length=32) + description = models.CharField(max_length=255, null=True, blank=True) + tags = pg_fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=list, + ) + group = models.OneToOneField( + "entities.Group", + on_delete=models.CASCADE, + null=True, + blank=True, + ) + source = models.ForeignKey( + "repos.Repository", + related_name="entities", + related_query_name="entity", + on_delete=models.PROTECT, + ) + service = models.OneToOneField( + "services.Service", on_delete=models.CASCADE, null=True, blank=True + ) + library = models.OneToOneField( + "libraries.Library", on_delete=models.CASCADE, null=True, blank=True + ) + + def __str__(self): + return f"{self.name}/{self.source.name}" + + def clean(self): + if self.service and self.library: + raise ValidationError( + "Entity instance can be related to Service or " + "Library at the same time." + ) diff --git a/zoo/entities/yaml_definitions/component_base.yaml b/zoo/entities/yaml_definitions/component_base.yaml new file mode 100644 index 00000000..83fa85d7 --- /dev/null +++ b/zoo/entities/yaml_definitions/component_base.yaml @@ -0,0 +1,62 @@ +--- +additionalProperties: false +properties: + apiVersion: + type: string + kind: + enum: + - component + type: string + metadata: + properties: + description: + type: string + group: + properties: + maintainers: + items: + type: string + type: array + product_owner: + type: string + project_owner: + type: string + type: object + links: + items: + properties: + icon: + type: string + name: + type: string + url: + type: string + type: object + type: array + name: + type: string + label: + type: string + owner: + type: string + tags: + items: + type: string + type: array + type: object + required: + - name + - label + - owner + spec: + properties: + type: + enum: + - database + type: string + type: object + required: + - type +type: object +required: + - kind diff --git a/zoo/entities/yaml_definitions/component_library.yaml b/zoo/entities/yaml_definitions/component_library.yaml new file mode 100644 index 00000000..35c6f7a7 --- /dev/null +++ b/zoo/entities/yaml_definitions/component_library.yaml @@ -0,0 +1,99 @@ +--- +additionalProperties: false +properties: + apiVersion: + type: string + kind: + enum: + - component + type: string + metadata: + properties: + description: + type: + - string + group: + properties: + maintainers: + items: + type: string + type: array + product_owner: + type: string + project_owner: + type: string + type: object + links: + items: + properties: + icon: + type: + - string + name: + type: string + url: + type: string + type: object + type: + - array + name: + type: string + label: + type: string + owner: + type: string + tags: + items: + type: string + type: + - array + type: object + required: + - name + - label + - owner + spec: + properties: + analysis: + items: + properties: + exclusions: + items: + type: string + type: array + type: object + type: + - array + impact: + enum: + - profit + - customers + - employees + type: string + integrations: + items: + properties: + slack_channel: + type: string + sonarqube_project: + type: string + type: object + type: object + lifecycle: + enum: + - beta + - production + - deprecated + - discontinued + - fixed + type: string + type: + enum: + - library + type: string + type: object + required: + - type +required: + - kind +type: object diff --git a/zoo/entities/yaml_definitions/component_service.yaml b/zoo/entities/yaml_definitions/component_service.yaml new file mode 100644 index 00000000..f21804f8 --- /dev/null +++ b/zoo/entities/yaml_definitions/component_service.yaml @@ -0,0 +1,118 @@ +--- +additionalProperties: false +properties: + apiVersion: + type: string + kind: + enum: + - component + type: string + metadata: + properties: + description: + type: + - string + group: + properties: + maintainers: + items: + type: string + type: array + product_owner: + type: string + project_owner: + type: string + type: object + links: + items: + properties: + icon: + type: + - string + name: + type: string + url: + type: string + type: object + type: + - array + name: + type: string + label: + type: string + owner: + type: string + tags: + items: + type: string + type: + - array + type: object + required: + - name + - label + - owner + spec: + properties: + analysis: + items: + properties: + exclusions: + items: + type: string + type: array + type: object + type: + - array + environments: + items: + properties: + name: + type: string + dashboard_url: + type: + - string + health_check_url: + type: + - string + service_urls: + items: + type: string + type: array + type: object + type: array + impact: + enum: + - profit + - customers + - employees + type: string + integrations: + properties: + pagerduty_service: + type: string + sentry_project: + type: string + sonarqube_project: + type: string + slack_channel: + type: string + type: object + lifecycle: + enum: + - beta + - production + - deprecated + - discontinued + - fixed + type: string + type: + enum: + - service + type: string + type: object + required: + - type +required: + - kind +type: object diff --git a/zoo/factories.py b/zoo/factories.py index 34843dbc..d218ad2c 100644 --- a/zoo/factories.py +++ b/zoo/factories.py @@ -18,8 +18,11 @@ from zoo.auditing.check_discovery import Kind from zoo.auditing.models import Issue from zoo.datacenters.models import InfraNode +from zoo.entities.models import Entity, Group, Link +from zoo.libraries.models import Library from zoo.repos.models import Repository, RepositoryEnvironment -from zoo.services.models import Environment, Impact, Link, Service, Status, Tier +from zoo.services.constants import Lifecycle +from zoo.services.models import Environment, Impact, Service, Tier class RepositoryFactory(DjangoModelFactory): @@ -70,7 +73,9 @@ class Meta: owner = Faker("user_name") name = Faker("slug") - status = LazyFunction(lambda: choice([item.value for item in Status] + [None])) + lifecycle = LazyFunction( + lambda: choice([item.value for item in Lifecycle] + [None]) + ) impact = LazyFunction(lambda: choice([item.value for item in Impact] + [None])) tier = SubFactory(TierFactory, level=LazyFunction(lambda: randint(1, 4))) repository = SubFactory( @@ -86,6 +91,9 @@ class Meta: name = Faker("domain_word") service = SubFactory(ServiceFactory) + health_check_url = Faker("uri") + dashboard_url = Faker("uri") + service_urls = [Faker("uri"), Faker("uri")] class UserFactory(DjangoModelFactory): @@ -133,7 +141,6 @@ class Meta: name = Faker("domain_word") url = Faker("uri") - service = SubFactory(ServiceFactory) class KindFactory(Factory): @@ -153,3 +160,74 @@ class Meta: kind = Faker("domain_word") value = Faker("slug") + + +class GroupFactory(DjangoModelFactory): + class Meta: + model = Group + + product_owner = Faker("user_name") + project_owner = Faker("user_name") + + +class LibraryFactory(DjangoModelFactory): + class Meta: + model = Library + + owner = Faker("user_name") + name = Faker("domain_word") + lifecycle = LazyFunction( + lambda: choice([item.value for item in Lifecycle] + [None]) + ) + impact = LazyFunction(lambda: choice([item.value for item in Impact] + [None])) + slack_channel = Faker("domain_word") + sonarqube_project = Faker("domain_word") + repository = SubFactory(RepositoryFactory) + + +class ComponentBaseFactory(DjangoModelFactory): + class Meta: + model = Entity + + name = Faker("domain_word") + label = Faker("domain_word") + type = "database" + description = Faker("paragraph") + kind = "component" + owner = Faker("user_name") + source = SubFactory(RepositoryFactory) + service = None + library = None + group = SubFactory(GroupFactory) + + +class ComponentServiceFactory(DjangoModelFactory): + class Meta: + model = Entity + + name = Faker("domain_word") + label = Faker("domain_word") + type = "service" + description = Faker("paragraph") + kind = Faker("domain_word") + owner = Faker("user_name") + source = SubFactory(RepositoryFactory) + service = SubFactory(ServiceFactory) + library = None + group = SubFactory(GroupFactory) + + +class ComponentLibraryFactory(DjangoModelFactory): + class Meta: + model = Entity + + name = Faker("domain_word") + label = Faker("domain_word") + type = Faker("domain_word") + description = Faker("paragraph") + kind = Faker("domain_word") + owner = Faker("user_name") + source = SubFactory(RepositoryFactory) + service = None + library = SubFactory(LibraryFactory) + group = SubFactory(GroupFactory) diff --git a/zoo/libraries/migrations/0004_rename_status_to_lifecycle.py b/zoo/libraries/migrations/0004_rename_status_to_lifecycle.py new file mode 100644 index 00000000..7bbeec67 --- /dev/null +++ b/zoo/libraries/migrations/0004_rename_status_to_lifecycle.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.19 on 2021-07-19 09:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("libraries", "0003_rename_plural"), + ] + + operations = [ + migrations.RenameField( + model_name="library", + old_name="status", + new_name="lifecycle", + ), + ] diff --git a/zoo/libraries/models.py b/zoo/libraries/models.py index ba67f3ef..da1d9242 100644 --- a/zoo/libraries/models.py +++ b/zoo/libraries/models.py @@ -1,5 +1,4 @@ import re -from enum import Enum from django.conf import settings from django.contrib.postgres import fields as pg_fields @@ -8,18 +7,7 @@ from django.urls import reverse from djangoql.schema import DjangoQLSchema - -class Status(Enum): - BETA = "beta" - PRODUCTION = "production" - DEPRECATED = "deprecated" - DISCONTINUED = "discontinued" - - -class Impact(Enum): - PROFIT = "profit" - CUSTOMERS = "customers" - EMPLOYEES = "employees" +from zoo.services.constants import Impact, Lifecycle class Library(models.Model): @@ -29,8 +17,8 @@ class Meta: owner = pg_fields.CICharField(max_length=100) name = pg_fields.CICharField(max_length=100) - status = models.CharField( - choices=((item.value, item.value) for item in Status), + lifecycle = models.CharField( + choices=((item.value, item.value) for item in Lifecycle), null=True, blank=True, max_length=100, @@ -101,5 +89,5 @@ class LibraryQLSchema(DjangoQLSchema): def get_fields(self, model): if isinstance(model, Library): - return ["name", "owner", "status", "impact", "library_url"] + return ["name", "owner", "lifecycle", "impact", "library_url"] return super().get_fields(model) diff --git a/zoo/libraries/templates/libraries/library_detail.html b/zoo/libraries/templates/libraries/library_detail.html index ec43c4a3..fb9d5cc1 100644 --- a/zoo/libraries/templates/libraries/library_detail.html +++ b/zoo/libraries/templates/libraries/library_detail.html @@ -8,12 +8,6 @@ {% endwith %} {% endblock %} -{% block navtitle %} - {% with project_delete_view='library_delete' icon='book' to_url='/libraries' header=object.name|add:' library' project_update_view='library_update' subheader='owned by '|add:object.owner %} - {{ block.super }} - {% endwith %} -{% endblock %} - {% block content %} {% with project_type='libraries' project_type_singular='library' project_checklist_view='library_checklist' %} {{ block.super }} diff --git a/zoo/libraries/templates/libraries/library_form.html b/zoo/libraries/templates/libraries/library_form.html deleted file mode 100644 index 88231158..00000000 --- a/zoo/libraries/templates/libraries/library_form.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'shared/project_form.html' %} -{% load static %} -{% load utils %} - -{% block menu %} - {% with selected='libraries' %} - {{ block.super }} - {% endwith %} -{% endblock %} - -{% block disclaimer %} -{% singleton 'instance.Hints' as hints %} -{% if hints.library_form_explanation %} -

{{ hints.library_form_explanation|safe }}

-{% endif %} -{% endblock %} - -{% block content %} - {% with project=library project_type_singular='library' %} - {{ block.super }} - {% endwith %} -{% endblock %} diff --git a/zoo/libraries/templates/libraries/library_list.html b/zoo/libraries/templates/libraries/library_list.html index 9a237797..510b73a3 100644 --- a/zoo/libraries/templates/libraries/library_list.html +++ b/zoo/libraries/templates/libraries/library_list.html @@ -15,7 +15,7 @@ {% endblock %} {% block content %} - {% with project_type='libraries' project_create_view='library_create' project_links=project_links is_paginated=is_paginated page_obj=page_obj request=request %} - {{ block.super }} - {% endwith %} +{# {% with project_type='libraries' project_create_view='library_create' project_links=project_links is_paginated=is_paginated page_obj=page_obj request=request %}#} +{# {{ block.super }}#} +{# {% endwith %}#} {% endblock %} diff --git a/zoo/libraries/urls.py b/zoo/libraries/urls.py index 5d3434e9..eee4d1bc 100644 --- a/zoo/libraries/urls.py +++ b/zoo/libraries/urls.py @@ -10,15 +10,4 @@ views.LibraryDetail.as_view(), name="library_detail", ), - path( - "//update", - views.LibraryUpdate.as_view(), - name="library_update", - ), - path( - "//delete", - views.LibraryDelete.as_view(), - name="library_delete", - ), - path("new", views.LibraryCreate.as_view(), name="library_create"), ] diff --git a/zoo/libraries/views.py b/zoo/libraries/views.py index e51e66e1..6da18649 100644 --- a/zoo/libraries/views.py +++ b/zoo/libraries/views.py @@ -1,17 +1,15 @@ import re import structlog -from django.core.exceptions import SuspiciousOperation from django.db.models import Q from django.http import Http404 -from django.urls import reverse_lazy from django.views import generic as generic_views from djangoql.exceptions import DjangoQLError from djangoql.queryset import apply_search from ..auditing.models import Issue from ..checklists.steps import STEPS -from . import forms, models +from . import models log = structlog.get_logger() @@ -31,33 +29,6 @@ def get_object(self, queryset=None): raise Http404("Library.DoesNotExist") -class LibraryCreate(generic_views.CreateView): - form_class = forms.LibraryForm - model = form_class.Meta.model - - -class LibraryDelete(generic_views.DeleteView): - model = models.Library - success_url = reverse_lazy("library_list") - - def get_object(self, queryset=None): - owner_slug = self.kwargs.get("owner_slug") - name_slug = self.kwargs.get("name_slug") - - if queryset is None: - queryset = self.get_queryset() - - if owner_slug is None or name_slug is None: - raise SuspiciousOperation( - "LibraryDelete view must be called with owner_slug and name_slug" - ) - - try: - return queryset.get(owner_slug=owner_slug, name_slug=name_slug) - except self.model.DoesNotExist: - raise Http404(f"Library {owner_slug}/{name_slug} does not exist") - - class LibraryDetail(LibraryMixin, generic_views.DetailView): model = models.Library @@ -136,8 +107,3 @@ def get_context_data(self, **kwargs): context["project_links"] = ["Support", "Repository", "Documentation"] return context - - -class LibraryUpdate(LibraryMixin, generic_views.UpdateView): - form_class = forms.LibraryForm - model = form_class.Meta.model diff --git a/zoo/repos/entities_yaml.py b/zoo/repos/entities_yaml.py new file mode 100644 index 00000000..eb3e11bb --- /dev/null +++ b/zoo/repos/entities_yaml.py @@ -0,0 +1,151 @@ +from pathlib import Path +from typing import List, Union + +import structlog +import yaml +from jsonschema import ValidationError +from jsonschema import validate as schema_validate +from yaml import FullLoader, load_all, safe_dump_all + +from zoo.repos.models import Repository +from zoo.repos.utils import delete_none + +log = structlog.get_logger() + + +SCHEMAS = { + "base_component": str(Path(__file__).parents[1]) + + "/entities/yaml_definitions/component_base.yaml", + "service": str(Path(__file__).parents[1]) + + "/entities/yaml_definitions/component_service.yaml", + "library": str(Path(__file__).parents[1]) + + "/entities/yaml_definitions/component_library.yaml", +} + + +def get_schema(entity): + if entity["kind"] == "component": + if entity["spec"]["type"].lower() == "service": + return SCHEMAS["service"] + if entity["spec"]["type"].lower() == "library": + return SCHEMAS["library"] + return SCHEMAS["base_component"] + log.info("repos.sync_entity_yaml.no_schema_defined") + + +def validate(_yaml: str) -> bool: + try: + all_entities = load_all(_yaml, FullLoader) + for entity in all_entities: # list(list) + schema = get_schema(entity) + if not schema: + raise ValidationError + with open(schema, "r") as schema_file: + schema_dict = yaml.load(schema_file, FullLoader) + schema_validate(entity, schema_dict) + except ValidationError as err: + log.info("repos.sync_entity_yml.validation_error", error=err) + return False + else: + return True + + +def parse(_yaml: str) -> Union[List, None]: + return load_all(_yaml, Loader=FullLoader) + + +def generate_component_base(component): + component_base_document = { + "apiVersion": "v1alpha1", # TODO: make it configurable? + "kind": component.kind, + "metadata": { + "name": component.name, + "label": component.label, + "owner": component.owner, + "group": { + "product_owner": component.group.product_owner, + "project_owner": component.group.project_owner, + "maintainers": component.group.maintainers, + }, + "description": getattr(component, "description"), + "tags": getattr(component, "tags", []), + "links": [], + }, + "spec": {"type": component.type}, + } + + for link in component.links.all(): + link_document = {"name": link.name, "url": link.url} + if link.icon: + link_document["icon"] = link.icon + component_base_document["metadata"]["links"].append(link_document) + + return component_base_document + + +def generate_component_library(component_library, component_base_doc): + component_base_doc["spec"] = { + "type": "library", + "lifecycle": component_library.library.lifecycle, + "impact": component_library.library.impact, + "analysis": [], + "integrations": { + "sonarqube_project": component_library.library.sonarqube_project, + "slack_channel": component_library.library.slack_channel, + }, + } + + return component_base_doc + + +def generate_component_service(component_service, component_base_doc): + component_base_doc["spec"] = { + "type": "service", + "lifecycle": component_service.service.lifecycle, + "impact": component_service.service.impact, + "analysis": [], + "environments": [], + "integrations": { + "sonarqube_project": getattr( + component_service.service, "sonarqube_project" + ), + "slack_channel": getattr(component_service.service, "slack_channel"), + "sentry_project": getattr(component_service.service, "sentry_project"), + "pagerduty_service": getattr( + component_service.service, "pagerduty_service" + ), + }, + } + + for environment in component_service.service.environments.all(): + environment_document = { + "name": environment.name, + "dashboard_url": getattr(environment, "dashboard_url"), + "health_check_url": getattr(environment, "health_check_url"), + "service_urls": getattr(environment, "service_urls"), + } + component_base_doc["spec"]["environments"].append(environment_document) + + return component_base_doc + + +def generate(repository: Repository) -> str: + results = [] + for entity in repository.entities.all(): + if entity.kind != "component": + return NotImplemented + component_base_document = generate_component_base(entity) + if entity.type == "library": + component_library_document = generate_component_library( + entity, component_base_document + ) + results.append(delete_none(component_library_document)) + elif entity.type == "service": + component_service_document = generate_component_service( + entity, component_base_document + ) + results.append(delete_none(component_service_document)) + else: + results.append(delete_none(component_base_document)) + + return safe_dump_all(results, sort_keys=False) diff --git a/zoo/repos/tasks.py b/zoo/repos/tasks.py index c9475429..bcfa234c 100644 --- a/zoo/repos/tasks.py +++ b/zoo/repos/tasks.py @@ -2,25 +2,29 @@ import itertools import tempfile from collections import namedtuple -from typing import Dict +from typing import Dict, List import structlog from celery import shared_task from django.conf import settings +from django.db import transaction from ..analytics.tasks import repo_analyzers from ..auditing import runner from ..auditing.check_discovery import CHECKS as AUDITING_CHECKS +from ..entities.builder import EntityBuilder +from ..entities.models import Entity, Link +from ..libraries.models import Library from ..repos.models import Endpoint from ..services.constants import EnviromentType from ..services.models import Environment, Service +from .entities_yaml import parse, validate from .exceptions import MissingFilesError, RepositoryNotFoundError from .github import get_repositories as get_github_repositories from .gitlab import get_project_enviroments from .gitlab import get_repositories as get_gitlab_repositories from .models import Repository, RepositoryEnvironment from .utils import download_repository, get_scm_module, openapi_definition -from .zoo_yml import parse, validate log = structlog.get_logger() @@ -156,69 +160,63 @@ def index_api(repository, repo_path): @shared_task -def sync_zoo_file(): +def sync_entity_file(): for project in itertools.chain( get_github_repositories(), get_gitlab_repositories() ): + log.info("repos.sync_entity_yml.processing", project=project["name"]) if settings.SYNC_REPOS_SKIP_FORKS and project["is_fork"]: + log.info("repos.sync_entity_yml.skipping", project=project["name"]) continue - update_project_from_zoo_file.apply_async(args=project) + update_project_from_entity_file.apply_async(args=[project]) @shared_task -def update_project_from_zoo_file(proj: Dict) -> None: +def update_project_from_entity_file(proj: Dict) -> None: try: - content = get_zoo_file_content(proj) + content = get_entity_file_content(proj) except FileNotFoundError as err: - log.info("repos.sync_zoo_yml.file_not_found", error=err) + log.info("repos.sync_entity_yml.file_not_found", error=err) else: if not validate(content): return - update_or_create_service(parse(content), proj) + update_or_create_components(parse(content), proj) -def update_or_create_service(data: Dict, proj: Dict) -> None: - # Skip processing the zoo file if the repository is not yet synced +def update_or_create_components(data: List, proj: Dict) -> None: + # Skip processing the entity file if the repository is not yet synced + log.info( + "repos.sync_entity_yml.update_or_create_components", + project=proj["name"], + ) try: repository = Repository.objects.get( - remote_id=int(proj["id"]), provider=proj["provider"] + remote_id=int(proj["remote_id"]), provider=proj["provider"] ) except Repository.DoesNotExist: + log.error( + "repos.sync_entity_yml.update_or_create_components.repository_not_found", + remote_id=proj["remote_id"], + ) return - service_defaults = { - "impact": data["impact"], - "status": data["status"], - "repository": repository, - "docs_url": data["docs_url"], - "slack_channel": data["slack_channel"], - "sentry_project": data["sentry_project"], - "sonarqube_project": data["sonarqube_project"], - "pagerduty_service": data["pagerduty_service"], - "tags": data["tags"], - } - - service, _ = Service.objects.update_or_create( - owner=data["owner"], name=data["name"], defaults=service_defaults - ) + def _do_cleanup(): + with transaction.atomic(): + affected_entities = Entity.objects.filter(source=repository) + Link.objects.filter(entity__in=affected_entities).delete() + Service.objects.filter(repository=repository).delete() + Library.objects.filter(repository=repository).delete() + Entity.objects.filter(source=repository).delete() - # Delete all environments as yaml file has precedence - Environment.objects.filter(service=service).delete() + _do_cleanup() + entity_builder = EntityBuilder() + for component in data: + entity_builder.sync_entities(component, repository) - # Add all environments - for env in data["environments"]: - e = Environment(service=service, name=env["name"]) - e.dashboard_url = env["dashboard_url"] - e.service_urls = env["service_urls"] - e.health_check_url = env["health_check_url"] - e.save() - -def get_zoo_file_content(proj: Dict) -> str: +def get_entity_file_content(proj: Dict) -> str: provider = get_scm_module(proj["provider"]) - return provider.get_file_content( - proj["id"], settings.ZOO_YAML_FILE, settings.ZOO_YAML_DEFAULT_REF - ) + return provider.get_file_content(proj["remote_id"], "entities.yaml") def sync_enviroments_from_gitlab(repo: Repository): diff --git a/zoo/repos/utils.py b/zoo/repos/utils.py index cb76b524..b40af5a6 100644 --- a/zoo/repos/utils.py +++ b/zoo/repos/utils.py @@ -157,3 +157,13 @@ def openapi_definition(repository, request=None, repo_path=None): log.info("repos.utils.openapi.done", repo=repository, specs=len(specs)) return list(filter(None, specs)) + + +def delete_none(_dict): + for key, value in list(_dict.items()): + if isinstance(value, dict): + delete_none(value) + elif value is None: + _dict.pop(key) + + return _dict diff --git a/zoo/repos/zoo_yml.py b/zoo/repos/zoo_yml.py deleted file mode 100644 index 148fc3ff..00000000 --- a/zoo/repos/zoo_yml.py +++ /dev/null @@ -1,124 +0,0 @@ -from typing import Dict, Union - -import structlog -from jsonschema import ValidationError -from jsonschema import validate as schema_validate -from yaml import FullLoader, dump, load - -from zoo.services.models import Service - -log = structlog.get_logger() - -ZOO_JSON_SCHEMA = """ - type: object - properties: - type: - type: string - name: - type: string - owner: - type: string - impact: - type: ["string", "null"] - enum: ["profit", "customers", "employees"] - status: - type: ["string", "null"] - enum: ["beta", "production", "deprecated", "discontinued"] - docs_url: - type: ["string", "null"] - slack_channel: - type: ["string", "null"] - sentry_project: - type: ["string", "null"] - sonarqube_project: - type: ["string", "null"] - pagerduty_service: - type: string - tags: - type: array - items: - type: string - environments: - type: array - items: - type: object - properties: - name: - type: string - dashboard_url: - type: ["string", "null"] - service_urls: - type: array - items: - type: string - health_check_url: - type: ["string", "null"] - links: - type: array - items: - type: object - properties: - name: - type: string - url: - type: string - icon: - type: ["string", "null"] - additionalProperties: false - required: - - type - - name - - owner - """ - - -def validate(yml: str) -> bool: - try: - schema_validate(load(yml, Loader=FullLoader), load(ZOO_JSON_SCHEMA, FullLoader)) - except ValidationError as err: - log.info("repos.sync_zoo_yml.validation_error", error=err) - return False - else: - return True - - -def parse(yaml: str) -> Union[Dict, None]: - return load(yaml, Loader=FullLoader) - - -def generate(service: Service) -> str: - result = { - "type": "service", - "name": service.name, - "owner": service.owner, - "impact": service.impact, - "status": service.status, - "docs_url": service.docs_url, - "slack_channel": service.slack_channel, - "sentry_project": service.sentry_project, - "sonarqube_project": service.sonarqube_project, - "pagerduty_service": service.pagerduty_service, - "tags": service.tags, - "environments": [], - "links": [], - } - - for env in service.environments.all(): - environ = { - "name": env.name, - "dashboard_url": env.dashboard_url, - "health_check_url": env.health_check_url, - "service_urls": env.service_urls, - } - result["environments"].append(environ) - - for link in service.links.all(): - result["links"].append( - { - "name": link.name, - "url": link.url, - "icon": link.icon, - } - ) - - return dump(result, sort_keys=False) diff --git a/zoo/services/assets/js/service_detail.js b/zoo/services/assets/js/service_detail.js index 4779ecbc..647b73a1 100644 --- a/zoo/services/assets/js/service_detail.js +++ b/zoo/services/assets/js/service_detail.js @@ -1,5 +1,4 @@ import '../style/service_detail.less' -import jQuery from 'jquery/src/jquery' import 'fomantic-ui-css/semantic.min.js' import Mustache from 'mustache' import Vue from 'vue' diff --git a/zoo/services/assets/js/service_form.js b/zoo/services/assets/js/service_form.js deleted file mode 100644 index 3a0fc9eb..00000000 --- a/zoo/services/assets/js/service_form.js +++ /dev/null @@ -1,101 +0,0 @@ -import '../style/service_form.less' - -$(document).ready(function () { - $(".field.service-urls").each(function () { - var previous = null; - var previousEmpty = false; - $(this).find("input").each(function () { - if (previous !== null) { - $(previous).keyup((function (next, current) { - return function () { - if ($(current).val()) { - $(next).show(); - } else { - $(next).hide(); - } - } - })(this, previous)); - } - var isEmpty = $(this).val() == ""; - if (isEmpty && previousEmpty) { - $(this).hide(); - } - previous = this; - previousEmpty = isEmpty; - }); - }); - - $(".field.name input:visible").prop("required", true); -}); - -$("button.remove-environment").click(function () { - var checkbox = $("#" + $(this).data("checkbox")); - checkbox.val(true); - - var uiSegment = checkbox.closest(".ui.segment"); - uiSegment.hide(); - uiSegment.find(".field.name input").prop("required", false); - uiSegment.find(".field.type input").val(""); - - var hidden = $(".ui.segment.environment:hidden"); - if (hidden.length > 0) { - $("button.add-environment").prop("disabled", false); - } -}); - -$("button.remove-link").click(function () { - var checkbox = $("#" + $(this).data("checkbox")); - checkbox.val(true); - - var uiSegment = checkbox.closest(".ui.segment"); - uiSegment.hide(); - uiSegment.find(".field.name input").prop("required", false); - - var hidden = $(".ui.segment.link:hidden"); - if (hidden.length > 0) { - $("button.add-link").prop("disabled", false); - } -}); - -$("button.add-environment").click(function () { - $(this).blur(); - let hidden = $(".ui.segment.environment:hidden"); - if (hidden.length > 0) { - if (hidden.length == 1) { - $(this).prop("disabled", true); - } - - let firstSegment = hidden.eq(0); - firstSegment.find(":not(.no-reset) input").val(""); - firstSegment.find(".field.name input").prop("required", true); - firstSegment.show(); - } -}); -$("button.add-link").click(function () { - $(this).blur(); - var hidden = $(".ui.segment.link:hidden"); - if (hidden.length > 0) { - if (hidden.length == 1) { - $(this).prop("disabled", true); - } - - var firstSegment = hidden.eq(0); - firstSegment.find(":not(.no-reset) input").val(""); - firstSegment.find(".field.name input").prop("required", true); - firstSegment.show(); - } -}); - -$('.icon-hint .label').popup({ - transition: 'fade down', - exclusive: true, - hoverable: true, - variation: 'inverted', - position: 'left center' -}); - -$(".ui.green.button").on('submit', function(e) { - e.preventDefault(); - $("#gitlab_envs input").prop('disabled', true) - $(this).submit(); -}); diff --git a/zoo/services/constants.py b/zoo/services/constants.py index 3c346bc5..37a65775 100644 --- a/zoo/services/constants.py +++ b/zoo/services/constants.py @@ -1,7 +1,7 @@ from enum import Enum -class Status(Enum): +class Lifecycle(Enum): BETA = "beta" PRODUCTION = "production" DEPRECATED = "deprecated" diff --git a/zoo/services/forms.py b/zoo/services/forms.py deleted file mode 100644 index 4fc572dc..00000000 --- a/zoo/services/forms.py +++ /dev/null @@ -1,218 +0,0 @@ -import itertools - -from django import forms -from django.conf import settings -from django.contrib.postgres import forms as pg_forms -from django.core.exceptions import ValidationError -from django.forms import widgets - -from ..base.forms import ( - PagerdutyServiceInput, - SentryProjectInput, - SlackChannelInput, - WidgetAttrsMixin, -) -from ..checklists.forms import TagInput -from ..repos.forms import RepoInput -from ..repos.github import get_namespaces as get_github_namespaces -from ..repos.gitlab import get_namespaces as get_gitlab_namespaces -from ..repos.models import Provider -from . import models - - -class SplitArrayField(pg_forms.SplitArrayField): - def has_changed(self, initial, data): - try: - python_data = self.to_python(data) - except ValidationError: - pass - else: - if self.remove_trailing_nulls: - null_index = None - for i, value in reversed(list(enumerate(python_data))): - if value in self.base_field.empty_values: - null_index = i - else: - break - if null_index is not None: - data = python_data[:null_index] - - if initial in self.empty_values and data in self.empty_values: - return False - return super().has_changed(initial, data) - - -class EnvironmentForm(forms.ModelForm): - service_urls = SplitArrayField( - forms.URLField(required=False), - size=5, - remove_trailing_nulls=True, - label="Service URLs", - required=False, - ) - - class Meta: - model = models.Environment - fields = [ - "name", - "dashboard_url", - "logs_url", - "service_urls", - "open_api_url", - ] - labels = { - "dashboard_url": "Dashboard URL", - "logs_url": "Logs URL", - "open_api_url": "OpenAPI URL", - } - - -class LinkForm(forms.ModelForm): - class Meta: - model = models.Link - fields = ["name", "url", "icon"] - - labels = {"name": "Name", "url": "URL", "icon": "Icon"} - - -class ServiceForm(WidgetAttrsMixin, forms.ModelForm): - - namespace = "service_" - sections = [ - { - "title": "General", - "subtitle": "Basic service information", - "icon": "paperclip", - "fields": [ - "owner", - "name", - "description", - "impact", - "status", - "tier", - "docs_url", - "tags", - "exclusions", - ], - }, - { - "title": "Integrations", - "subtitle": "Third party solutions", - "icon": "bolt", - "fields": [ - "repository", - "slack_channel", - "sentry_project", - "sonarqube_project", - "pagerduty_service", - ], - }, - ] - exclusions = forms.CharField( - max_length=500, - label="Repository check exclusions", - help_text="Comma separated paths to exclude", - required=False, - ) - - class Meta: - model = models.Service - fields = [ - "owner", - "name", - "description", - "impact", - "status", - "tier", - "slack_channel", - "sentry_project", - "sonarqube_project", - "repository", - "pagerduty_service", - "docs_url", - "tags", - ] - labels = { - "docs_url": "Documentation URL", - "sonarqube_project": "Sonarqube project key", - } - widgets = { - "owner": widgets.Select( - attrs={"class": "ui fluid search selection dropdown"}, - choices=( - sorted( - [("", "---------")] # empty select - + [ - (namespace["name"], namespace["name"]) - for namespace in itertools.chain( - get_github_namespaces(), get_gitlab_namespaces() - ) - ], - key=lambda k: k[1].lower(), - ) - ), - ) - if settings.REMOTE_DATA_OWNERS - else widgets.TextInput(), - "repository": RepoInput(), - "pagerduty_service": PagerdutyServiceInput(), - "sentry_project": SentryProjectInput(), - "slack_channel": SlackChannelInput(), - "tags": TagInput(), - } - - def __init__(self, *args, **kwargs): - instance = kwargs.get("instance") - exclusions = ( - instance.repository.exclusions if instance and instance.repository else "" - ) - kwargs.update({"initial": {"exclusions": exclusions}}) - super().__init__(*args, **kwargs) - - def save(self, commit=True): - try: - repository = self.instance.repository - if repository: - exclusions = self.data["exclusions"] - repository.exclusions = exclusions - repository.save() - except (KeyError, AttributeError): - pass - - service = super().save() - self.create_gitlab_envs(service) - return service - - def create_gitlab_envs(self, service): - if not service.repository: - return - - if service.repository.provider == Provider.GITHUB: - return - - for env in service.repository.repository_environments.all(): - models.Environment.objects.update_or_create( - type=models.EnviromentType.GITLAB, - name=env.name, - dashboard_url=env.external_url, - service_id=service.id, - ) - - -ServiceEnvironmentsFormSet = forms.inlineformset_factory( - models.Service, - models.Environment, - form=EnvironmentForm, - extra=5, - max_num=5, - can_delete=True, -) - -ServiceLinksFormSet = forms.inlineformset_factory( - models.Service, - models.Link, - form=LinkForm, - extra=5, - max_num=5, - can_delete=True, -) diff --git a/zoo/services/migrations/0028_rename_status_remove_link_alter_pds.py b/zoo/services/migrations/0028_rename_status_remove_link_alter_pds.py new file mode 100644 index 00000000..47cde13f --- /dev/null +++ b/zoo/services/migrations/0028_rename_status_remove_link_alter_pds.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.19 on 2021-07-27 08:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("services", "0027_auto_20210509_1900"), + ] + + operations = [ + migrations.RenameField( + model_name="service", + old_name="status", + new_name="lifecycle", + ), + migrations.AlterField( + model_name="service", + name="pagerduty_service", + field=models.CharField(blank=True, max_length=80, null=True), + ), + migrations.DeleteModel( + name="Link", + ), + ] diff --git a/zoo/services/models.py b/zoo/services/models.py index f2803231..c3de307e 100644 --- a/zoo/services/models.py +++ b/zoo/services/models.py @@ -10,7 +10,7 @@ from djangoql.schema import DjangoQLSchema from . import ratings -from .constants import EnviromentType, Impact, SentryIssueCategory, Status +from .constants import EnviromentType, Impact, Lifecycle, SentryIssueCategory from .managers import SentryIssueManager @@ -26,8 +26,8 @@ class Meta: blank=True, help_text="Short description of this service", ) - status = models.CharField( - choices=((item.value, item.value) for item in Status), + lifecycle = models.CharField( + choices=((item.value, item.value) for item in Lifecycle), null=True, blank=True, max_length=100, @@ -57,7 +57,7 @@ class Meta: blank=True, related_name="services", ) - pagerduty_service = models.CharField(max_length=80, default="", blank=True) + pagerduty_service = models.CharField(max_length=80, null=True, blank=True) docs_url = models.URLField(max_length=500, null=True, blank=True) owner_slug = models.SlugField(max_length=140) name_slug = models.SlugField(max_length=140) @@ -249,23 +249,6 @@ class Meta: ) -class Link(models.Model): - name = models.CharField(max_length=32) - icon = models.CharField( - max_length=16, - null=True, - blank=True, - help_text="https://fomantic-ui.com/elements/icon.html", - ) - url = models.URLField() - service = models.ForeignKey( - Service, - on_delete=models.CASCADE, - related_name="links", - related_query_name="link", - ) - - class ServiceQLSchema(DjangoQLSchema): include = (Service, Environment) diff --git a/zoo/services/templates/services/service_detail.html b/zoo/services/templates/services/service_detail.html index 73b2538a..afb9c0ed 100644 --- a/zoo/services/templates/services/service_detail.html +++ b/zoo/services/templates/services/service_detail.html @@ -14,9 +14,7 @@ {% endblock %} {% block navtitle %} - {% with project_delete_view='service_delete' icon='cogs' to_url='/services' header=object.name project_update_view='service_update' subheader='owned by '|add:object.owner %} {{ block.super }} - {% endwith %} {% endblock %} {% block content %} @@ -236,7 +234,7 @@

{% include "datacenters/infra_overview.html" with datacenters=object.datacenters.all only %} {% endif %} - {% if object.links %} + {% if object.links.all %}

diff --git a/zoo/services/templates/services/service_form.html b/zoo/services/templates/services/service_form.html deleted file mode 100644 index b973fcef..00000000 --- a/zoo/services/templates/services/service_form.html +++ /dev/null @@ -1,222 +0,0 @@ -{% extends 'shared/project_form.html' %} -{% load static %} -{% load utils %} - -{% block stylesheets %} - {{ block.super }} - -{% endblock %} - -{% block menu %} - {% with selected='services' %} - {{ block.super }} - {% endwith %} -{% endblock %} - -{% block disclaimer %} - {% singleton 'instance.Hints' as hints %} - {% if hints.service_form_explanation %} -

{{ hints.service_form_explanation|safe }}

- {% endif %} -{% endblock %} - -{% block content %} - {% with project=service project_type_singular='service' %} - {{ block.super }} - {% endwith %} -{% endblock %} - -{% block extrafields %} -
-
-

Environments

- -
- {{ envs_formset.management_form }} - {% for form in envs_formset %} -
1 %}style="display: none"{% endif %}> - {% if forloop.counter > 1 %} -
- -
- {% endif %} - {% if form.errors %} -
-
Please correct the following errors
-
    - {% for field, messages in form.errors.items %} -
  • {{ field }} - {{ messages|join:" " }}
  • - {% endfor %} -
-
- {% endif %} -
- {% for field in form %} - {% with field.field.widget.input_type as type %} - {% if type == "hidden" %} - {{ field }} - {% endif %} - {% endwith %} - {% endfor %} -
- {{ form.DELETE.as_hidden }} -
-
- - {{ form.name }} -
-
- - {{ form.dashboard_url }} -
-
-
- - {{ form.logs_url }} -
-
- - {{ form.service_urls }} -
-
- - {{ form.open_api_url }} -
-
- {% endfor %} -
- {% if envs_gitlab %} - {% for env in envs_gitlab %} -
-
- gitlab -
- -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- {% endfor %} - {% endif %} -
- -
- -{% endblock %} - -{% block scripts %} - {{ block.super }} - -{% endblock %} diff --git a/zoo/services/templates/services/service_list.html b/zoo/services/templates/services/service_list.html index 6ded4c67..aed70a04 100644 --- a/zoo/services/templates/services/service_list.html +++ b/zoo/services/templates/services/service_list.html @@ -14,8 +14,8 @@ {% endwith %} {% endblock %} -{% block content %} - {% with project_type='services' project_create_view='service_create' project_links=project_links is_paginated=is_paginated page_obj=page_obj request=request %} - {{ block.super }} - {% endwith %} -{% endblock %} +{#{% block content %}#} +{# {% with project_type='services' project_create_view='service_create' project_links=project_links is_paginated=is_paginated page_obj=page_obj request=request %}#} +{# {{ block.super }}#} +{# {% endwith %}#} +{#{% endblock %}#} diff --git a/zoo/services/urls.py b/zoo/services/urls.py index bd4ce55e..0daac80e 100644 --- a/zoo/services/urls.py +++ b/zoo/services/urls.py @@ -15,15 +15,4 @@ views.ServiceOpenApiDefinition.as_view(), name="service_openapi", ), - path( - "//update", - views.ServiceUpdate.as_view(), - name="service_update", - ), - path( - "//delete", - views.ServiceDelete.as_view(), - name="service_delete", - ), - path("new", views.ServiceCreate.as_view(), name="service_create"), ] diff --git a/zoo/services/views.py b/zoo/services/views.py index 14c1534a..239ee79d 100644 --- a/zoo/services/views.py +++ b/zoo/services/views.py @@ -3,11 +3,8 @@ import requests import structlog -from django.core.exceptions import SuspiciousOperation -from django.db import transaction from django.db.models import Q from django.http import Http404, JsonResponse -from django.urls import reverse_lazy from django.views import generic as generic_views from djangoql.exceptions import DjangoQLError from djangoql.queryset import apply_search @@ -15,8 +12,8 @@ from ..auditing.models import Issue from ..checklists.steps import STEPS from ..repos.utils import openapi_definition -from . import forms, models -from .models import EnviromentType, Environment, Service +from . import models +from .models import EnviromentType, Service log = structlog.get_logger() @@ -36,76 +33,6 @@ def get_object(self, queryset=None): raise Http404("Service.DoesNotExist") -class ServiceEnvironmentMixin: - def form_valid(self, form): - context = self.get_context_data() - envs_formset = context["envs_formset"] - with transaction.atomic(): - self.object = form.save() - log.info(form.data) - if envs_formset.is_valid(): - envs_formset.instance = self.object - envs_formset.save() - else: - return self.form_invalid(form) - return super().form_valid(form) - - -class ServiceLinkMixin: - def form_valid(self, form): - context = self.get_context_data() - links_formset = context["links_formset"] - with transaction.atomic(): - self.object = form.save() - log.info(form.data) - - if links_formset.is_valid(): - links_formset.instance = self.object - links_formset.save() - else: - return self.form_invalid(form) - return super().form_valid(form) - - -class ServiceCreate( - ServiceEnvironmentMixin, ServiceLinkMixin, generic_views.CreateView -): - form_class = forms.ServiceForm - model = form_class.Meta.model - - def get_context_data(self, **kwargs): - data = super().get_context_data(**kwargs) - if self.request.POST: - data["envs_formset"] = forms.ServiceEnvironmentsFormSet(self.request.POST) - data["links_formset"] = forms.ServiceLinksFormSet(self.request.POST) - else: - data["envs_formset"] = forms.ServiceEnvironmentsFormSet() - data["links_formset"] = forms.ServiceLinksFormSet() - return data - - -class ServiceDelete(generic_views.DeleteView): - model = models.Service - success_url = reverse_lazy("service_list") - - def get_object(self, queryset=None): - owner_slug = self.kwargs.get("owner_slug") - name_slug = self.kwargs.get("name_slug") - - if queryset is None: - queryset = self.get_queryset() - - if owner_slug is None or name_slug is None: - raise SuspiciousOperation( - "ServiceDelete view must be called with owner_slug and name_slug" - ) - - try: - return queryset.get(owner_slug=owner_slug, name_slug=name_slug) - except self.model.DoesNotExist: - raise Http404(f"Service {owner_slug}/{name_slug} does not exist") - - class ServiceDetail(ServiceMixin, generic_views.DetailView): model = models.Service @@ -141,7 +68,7 @@ def get_sentry_context(self): } def get_checklists_context(self): - if self.object.status != models.Status.BETA.value: + if self.object.lifecycle != models.Lifecycle.BETA.value: return None return { @@ -186,7 +113,7 @@ def get_queryset(self): queryset = queryset.filter( Q(name__icontains=queryterm) | Q(owner__icontains=queryterm) - | Q(status__icontains=queryterm) + | Q(lifecycle__icontains=queryterm) | Q(impact__icontains=queryterm) ) @@ -219,31 +146,6 @@ def get_context_data(self, **kwargs): return context -class ServiceUpdate( - ServiceEnvironmentMixin, ServiceLinkMixin, ServiceMixin, generic_views.UpdateView -): - form_class = forms.ServiceForm - model = form_class.Meta.model - - def get_context_data(self, **kwargs): - data = super().get_context_data(**kwargs) - if self.request.POST: - data["envs_formset"] = forms.ServiceEnvironmentsFormSet( - self.request.POST, instance=self.object - ) - data["links_formset"] = forms.ServiceLinksFormSet( - self.request.POST, instance=self.object - ) - else: - data["envs_gitlab"] = Environment.objects.filter(type=EnviromentType.GITLAB) - data["envs_formset"] = forms.ServiceEnvironmentsFormSet( - instance=self.object, - queryset=Environment.objects.filter(type=EnviromentType.ZOO), - ) - data["links_formset"] = forms.ServiceLinksFormSet(instance=self.object) - return data - - class ServiceOpenApiDefinition(ServiceMixin, generic_views.View): def dispatch(self, request, *args, **kwargs): if request.method != "GET":