├── tests ├── __init__.py └── integration │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── test_extra_vars.py │ ├── test_playbook.py │ └── conftest.py │ ├── db │ ├── __init__.py │ └── sql │ │ ├── __init__.py │ │ ├── base │ │ └── __init__.py │ │ └── rulebook │ │ └── __init__.py │ ├── utils │ ├── __init__.py │ ├── app.py │ └── db.py │ └── test_root_handlers.py ├── src └── eda_server │ ├── __init__.py │ ├── db │ ├── __init__.py │ ├── sql │ │ ├── __init__.py │ │ ├── playbook │ │ │ └── __init__.py │ │ ├── extra_var │ │ │ └── __init__.py │ │ └── inventory │ │ │ └── __init__.py │ ├── utils │ │ ├── __init__.py │ │ └── common.py │ ├── migrations │ │ ├── README │ │ ├── versions │ │ │ ├── 202210261523_f592f6ef1e3a_new_rbac_enum_values.py │ │ │ ├── 202209101031_873701d68c86_uuid_ossp.py │ │ │ ├── 202209162048_9f6b44aa0df8_project_name_unique.py │ │ │ ├── 202211171209_202cf90fc4b0_add_role_is_default.py │ │ │ ├── 202210040316_ce5cc33ecdb6_add_sources_to_rulesets.py │ │ │ ├── 202209131944_34bca36e407d_change_job_instance_event_table_to_add_.py │ │ │ ├── 202209291853_25bcfbe12475_project_name_not_null.py │ │ │ ├── 202208242257_61c61bfd1f7b_add_ee_and_wd_to_activation_instance.py │ │ │ ├── 202210041309_12c3a0edc032_activation_project.py │ │ │ ├── 202209262002_bc14081ad342_add_ruleset_created_at_modified_at.py │ │ │ ├── 202209221449_d4df045eb3ce_add_job_instance_host_table.py │ │ │ ├── 202210071428_abfdc233b4b0_rulebook_new_fields.py │ │ │ ├── 202208161211_2425525e8124_native_uuid.py │ │ │ ├── 202210171900_c703cd56f6b0_update_job_instance_table_columns.py │ │ │ ├── 202208311823_c1eee0e47fc1_project_table_add_fields.py │ │ │ └── 202209131406_74607f5764f9_update_activation_table_columns.py │ │ └── script.py.mako │ ├── provider.py │ ├── dependency.py │ ├── models │ │ ├── base.py │ │ ├── __init__.py │ │ └── inventory.py │ └── session.py │ ├── utils │ └── __init__.py │ ├── messages.py │ ├── config │ ├── logging.yaml │ └── __init__.py │ ├── schema │ ├── user.py │ ├── playbook.py │ ├── extra_vars.py │ ├── role.py │ ├── inventory.py │ ├── message.py │ ├── project.py │ ├── job.py │ └── audit_rule.py │ ├── api │ ├── ssh.py │ ├── task.py │ ├── auth.py │ └── __init__.py │ ├── main.py │ ├── types.py │ └── key.py ├── ui ├── .prettierignore ├── __mocks__ │ ├── styleMock.js │ └── fileMock.js ├── .storybook │ ├── tsconfig.json │ ├── preview.ts │ ├── main.ts │ └── webpack.config.js ├── .prettierrc ├── src │ ├── favicon.png │ ├── assets │ │ └── images │ │ │ └── favicon.ico │ ├── app │ │ ├── utils │ │ │ ├── EdaCodeEditor.css │ │ │ ├── constants.ts │ │ │ └── useDocumentTitle.ts │ │ ├── Rules │ │ │ ├── rules-table-context.ts │ │ │ └── rules-table-helpers.tsx │ │ ├── types │ │ │ └── assets.d.ts │ │ ├── Activations │ │ │ ├── activations-table-context.ts │ │ │ └── activations-table-helpers.tsx │ │ ├── Inventories │ │ │ ├── inventories-table-context.ts │ │ │ └── inventories-table-helpers.tsx │ │ ├── API │ │ │ ├── Playbook.ts │ │ │ ├── Rule.ts │ │ │ ├── Audit.ts │ │ │ ├── Extravar.ts │ │ │ ├── baseApi.ts │ │ │ ├── Rulebook.ts │ │ │ ├── auth.ts │ │ │ ├── Inventory.ts │ │ │ ├── Job.ts │ │ │ ├── Ruleset.ts │ │ │ ├── Project.ts │ │ │ └── Activation.ts │ │ ├── AppLayout │ │ │ ├── small-logo.tsx │ │ │ ├── about-modal.tsx │ │ │ └── stateful-dropdown.js │ │ ├── Jobs │ │ │ ├── jobs-table-context.ts │ │ │ └── jobs-table-helpers.tsx │ │ ├── Dashboard │ │ │ └── Dashboard.tsx │ │ ├── NewRuleSet │ │ │ └── NewRuleSet.tsx │ │ ├── app.css │ │ ├── InventoriesSelect │ │ │ ├── inventories-select-table-context.ts │ │ │ └── inventories-select-table-helpers.tsx │ │ ├── RuleSet │ │ │ ├── sources-table-helpers.tsx │ │ │ └── rules-table-helpers.tsx │ │ ├── RuleSets │ │ │ └── rule-sets-table-helpers.tsx │ │ ├── Projects │ │ │ ├── projects-table-context.ts │ │ │ └── projects-table-helpers.tsx │ │ ├── RuleBooks │ │ │ └── rulebooks-table-helpers.tsx │ │ ├── Activation │ │ │ ├── jobs-table-helpers.tsx │ │ │ └── activation-stdout.tsx │ │ ├── shared │ │ │ ├── pagination.ts │ │ │ ├── breadcrumbs.tsx │ │ │ ├── app-tabs.tsx │ │ │ ├── table-empty-state.tsx │ │ │ ├── top-toolbar.tsx │ │ │ └── types │ │ │ │ └── common-types.d.ts │ │ ├── RuleBook │ │ │ └── rulesets-table-helpers.tsx │ │ ├── NotFound │ │ │ └── NotFound.tsx │ │ ├── Var │ │ │ └── Var.tsx │ │ ├── Inventory │ │ │ └── InventoryLinks.tsx │ │ ├── AuditView │ │ │ ├── audit-hosts-table-helpers.tsx │ │ │ ├── audit-rules-table-helpers.tsx │ │ │ └── AuditView.tsx │ │ ├── index.tsx │ │ ├── Playbook │ │ │ └── Playbook.tsx │ │ ├── Vars │ │ │ └── Vars.tsx │ │ └── Playbooks │ │ │ └── Playbooks.tsx │ ├── test │ │ ├── __mocks__ │ │ │ └── baseApi.ts │ │ ├── Rules │ │ │ └── Rules.test.tsx │ │ ├── RuleSets │ │ │ └── RuleSets.test.tsx │ │ ├── AuditView │ │ │ ├── audit-rules.test.tsx │ │ │ ├── audit-hosts.test.tsx │ │ │ └── __snapshots__ │ │ │ │ ├── audit-hosts.test.tsx.snap │ │ │ │ └── audit-rules.test.tsx.snap │ │ ├── RuleBooks │ │ │ └── RuleBooks.test.tsx │ │ ├── Inventories │ │ │ └── Inventories.test.tsx │ │ ├── Activations │ │ │ └── Activations.test.tsx │ │ ├── Rule │ │ │ └── rule.test.tsx │ │ ├── NewInventory │ │ │ └── NewInventory.test.tsx │ │ ├── RuleSet │ │ │ └── ruleset.test.tsx │ │ ├── InventoriesSelect │ │ │ └── InventoriesSelect.test.tsx │ │ ├── AppLayout │ │ │ └── AppLayout.test.tsx │ │ ├── Job │ │ │ └── job.test.tsx │ │ └── NewJob │ │ │ └── NewJob.test.tsx │ ├── typings.d.ts │ ├── index.html │ ├── store │ │ └── index.ts │ ├── index.tsx │ └── global-styles.js ├── .gitignore ├── babel.config.json ├── .editorconfig ├── dr-surge.js ├── .github │ └── ISSUE_TEMPLATE │ │ └── bug_report.md ├── stylePaths.js ├── config │ ├── test-setup.js │ ├── webpack.prod.js │ ├── webpack.dev.js │ └── webpack.common.js ├── LICENSE ├── tsconfig.json ├── .eslintrc └── jest.config.js ├── requirements_test.txt ├── tools ├── deploy │ ├── kustomization.yaml │ ├── postgres │ │ ├── kustomization.yaml │ │ ├── pvc.yaml │ │ ├── service.yaml │ │ └── deployment.yaml │ ├── eda-server │ │ ├── service.yaml │ │ ├── kustomization.yaml │ │ └── deployment.yaml │ └── eda-frontend │ │ ├── service.yaml │ │ ├── kustomization.yaml │ │ └── deployment.yaml ├── docker │ ├── postgres │ │ ├── initdb.d │ │ │ └── pg_stat_statements.sql │ │ └── conf.d │ │ │ ├── pg_statistics.conf │ │ │ ├── pg_ssl.conf │ │ │ ├── pg_logging.conf │ │ │ ├── pg_jit.conf │ │ │ └── pg_stat_statements.conf │ ├── nginx │ │ ├── Dockerfile │ │ └── default.conf │ └── Dockerfile └── initial_data.yml ├── .dockerignore ├── MANIFEST.in ├── requirements_dev.txt ├── tox.ini ├── requirements.txt ├── .gitignore ├── requirements_lint.txt ├── pyproject.toml ├── scripts └── common │ ├── utils.sh │ └── logging.sh ├── .github └── workflows │ ├── test-docker-deployment.yml │ ├── ui-pr-test.yml │ ├── linters.yml │ └── tests.yml ├── .pre-commit-config.yaml ├── setup.py └── setup.cfg /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eda_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eda_server/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eda_server/db/sql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eda_server/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eda_server/db/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/db/sql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /tests/integration/db/sql/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/db/sql/rulebook/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /ui/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | faker 2 | pytest 3 | pytest-asyncio 4 | httpx 5 | -------------------------------------------------------------------------------- /ui/.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /ui/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '@patternfly/react-core/dist/styles/base.css'; 2 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /tools/deploy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - ./eda-frontend 3 | - ./eda-server 4 | - ./postgres -------------------------------------------------------------------------------- /tools/docker/postgres/initdb.d/pg_stat_statements.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION pg_stat_statements; 2 | -------------------------------------------------------------------------------- /ui/src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/eda-server-prototype/HEAD/ui/src/favicon.png -------------------------------------------------------------------------------- /ui/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/eda-server-prototype/HEAD/ui/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | */node_modules 3 | */dist 4 | */*/*/temp 5 | 6 | *.egg-info 7 | 8 | *.db 9 | .env 10 | .venv 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/eda_server/config/*.yaml 2 | include src/eda_server/db/migrations/* 3 | include src/eda_server/db/migrations/versions/*.py 4 | -------------------------------------------------------------------------------- /ui/src/app/utils/EdaCodeEditor.css: -------------------------------------------------------------------------------- 1 | .monaco-editor span { 2 | font-family: unset; 3 | } 4 | 5 | .pf-c-code-editor__code { 6 | padding: 0; 7 | } 8 | -------------------------------------------------------------------------------- /tools/deploy/postgres/kustomization.yaml: -------------------------------------------------------------------------------- 1 | commonLabels: 2 | app: eda-postgres 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | - pvc.yaml -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | dist 3 | yarn-error.log 4 | yarn.lock 5 | stats.json 6 | coverage 7 | storybook-static 8 | .DS_Store 9 | .idea 10 | .env 11 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | -r requirements_test.txt 3 | -r requirements_lint.txt 4 | 5 | sqlalchemy[mypy] ~= 1.4 6 | python-dateutil 7 | pre-commit 8 | -------------------------------------------------------------------------------- /ui/src/app/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const APPLICATION_TITLE = 2 | // eslint-disable-next-line no-undef 3 | `${process.env.APPLICATION_NAME || 'Event-Driven Ansible'}`; 4 | -------------------------------------------------------------------------------- /ui/src/test/__mocks__/baseApi.ts: -------------------------------------------------------------------------------- 1 | import MockAdapter from 'axios-mock-adapter'; 2 | import { getAxiosInstance } from '../../app/API/baseApi'; 3 | 4 | export const mockApi = new MockAdapter(getAxiosInstance()); 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py39,py310,py311 3 | isolated_build = True 4 | 5 | [testenv] 6 | setenv = 7 | PYTHONPATH = {toxinidir} 8 | deps = 9 | -r{toxinidir}/requirements_dev.txt 10 | commands = pytest 11 | -------------------------------------------------------------------------------- /ui/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-react", { "runtime": "automatic" }], 4 | "@babel/preset-typescript", 5 | ["@babel/preset-env", { "modules": "auto" }] 6 | ], 7 | "plugins": ["@babel/transform-runtime"] 8 | } 9 | -------------------------------------------------------------------------------- /tools/deploy/postgres/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: postgres-data 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 100Mi 11 | status: {} 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic 2 | asyncpg 3 | asyncpg-lostream 4 | fastapi 5 | fastapi-users[sqlalchemy] 6 | pyyaml 7 | sqlalchemy ~= 1.4 8 | uvicorn 9 | websockets # TODO(cutwater): Check if this is dependency is necessary 10 | environs 11 | aiodocker 12 | ansible-rulebook 13 | -------------------------------------------------------------------------------- /tools/deploy/postgres/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres 5 | spec: 6 | ports: 7 | - name: "5432" 8 | port: 5432 9 | targetPort: 5432 10 | selector: 11 | app: postgres 12 | status: 13 | loadBalancer: {} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python: Byte code 2 | __pycache__/ 3 | 4 | # Python: Packaging 5 | build/ 6 | dist/ 7 | *.egg-info 8 | 9 | 10 | *.db 11 | .env 12 | /venv 13 | /.venv 14 | .tox 15 | 16 | # IDE's 17 | .idea 18 | .vscode 19 | /.venv/ 20 | /.python-version 21 | /tools/deploy/temp/ 22 | -------------------------------------------------------------------------------- /tools/deploy/eda-server/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: eda-server 5 | spec: 6 | ports: 7 | - name: "9000" 8 | port: 9000 9 | targetPort: 9000 10 | selector: 11 | app: eda-server 12 | status: 13 | loadBalancer: {} 14 | -------------------------------------------------------------------------------- /tools/deploy/eda-frontend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: eda-frontend 5 | spec: 6 | ports: 7 | - name: "8080" 8 | port: 8080 9 | targetPort: 8080 10 | selector: 11 | app: eda-frontend 12 | status: 13 | loadBalancer: {} 14 | -------------------------------------------------------------------------------- /tools/deploy/eda-server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | commonLabels: 2 | app: eda-server 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | apiVersion: kustomize.config.k8s.io/v1beta1 7 | kind: Kustomization 8 | images: 9 | - name: eda-server 10 | newName: eda-server 11 | newTag: latest 12 | -------------------------------------------------------------------------------- /tools/deploy/eda-frontend/kustomization.yaml: -------------------------------------------------------------------------------- 1 | commonLabels: 2 | app: eda-frontend 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | apiVersion: kustomize.config.k8s.io/v1beta1 7 | kind: Kustomization 8 | images: 9 | - name: eda-frontend 10 | newName: eda-frontend 11 | newTag: latest 12 | -------------------------------------------------------------------------------- /ui/src/app/Rules/rules-table-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const RulesTableContext = createContext<{ 4 | selectedRules: string[]; 5 | setSelectedRules: ((ids: string[]) => void) | null; 6 | }>({ selectedRules: [], setSelectedRules: null }); 7 | 8 | export default RulesTableContext; 9 | -------------------------------------------------------------------------------- /requirements_lint.txt: -------------------------------------------------------------------------------- 1 | # NOTE(cutwater): Keep list of flake8 plugins in sync with .pre-commit-config.yaml 2 | flake8 3 | flake8-broken-line 4 | flake8-bugbear 5 | flake8-comprehensions 6 | flake8-debugger 7 | flake8-docstrings 8 | flake8-eradicate 9 | flake8-print 10 | flake8-string-format 11 | pep8-naming 12 | black 13 | isort 14 | -------------------------------------------------------------------------------- /ui/src/app/types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React = require('react'); 3 | export const ReactComponent: React.SFC>; 4 | const src: string; 5 | export default src; 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string; 10 | export default content; 11 | } 12 | -------------------------------------------------------------------------------- /tools/docker/postgres/conf.d/pg_statistics.conf: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # PostgreSQL Statistics 3 | #------------------------------------------------------------------------------ 4 | 5 | # Enable tracking of procedural language functions 6 | track_functions = pl 7 | track_activity_query_size = 2048 8 | -------------------------------------------------------------------------------- /ui/src/app/Activations/activations-table-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const ActivationsTableContext = createContext<{ 4 | selectedActivations: string[]; 5 | setSelectedActivations: ((ids: string[]) => void) | null; 6 | }>({ selectedActivations: [], setSelectedActivations: null }); 7 | 8 | export default ActivationsTableContext; 9 | -------------------------------------------------------------------------------- /ui/src/app/Inventories/inventories-table-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const InventoriesTableContext = createContext<{ 4 | selectedInventories: string[]; 5 | setSelectedInventories: ((ids: string[]) => void) | null; 6 | }>({ selectedInventories: [], setSelectedInventories: null }); 7 | 8 | export default InventoriesTableContext; 9 | -------------------------------------------------------------------------------- /ui/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | declare module '*.jpeg'; 4 | declare module '*.gif'; 5 | declare module '*.svg'; 6 | declare module '*.css'; 7 | declare module '*.wav'; 8 | declare module '*.mp3'; 9 | declare module '*.m4a'; 10 | declare module '*.rdf'; 11 | declare module '*.ttl'; 12 | declare module '*.pdf'; 13 | -------------------------------------------------------------------------------- /ui/src/app/API/Playbook.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { AxiosResponse } from 'axios'; 3 | import { getAxiosInstance } from '@app/API/baseApi'; 4 | 5 | const playbooksEndpoint = '/api/playbooks'; 6 | 7 | export const listPlaybooks = (pagination = defaultSettings): Promise => 8 | getAxiosInstance().get(playbooksEndpoint); 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 79 7 | target-version = ["py38", "py39", "py310"] 8 | extend-exclude = "docs" 9 | 10 | [tool.isort] 11 | profile = "black" 12 | combine_as_imports = true 13 | line_length = 79 14 | 15 | [tool.pytest.ini_options] 16 | asyncio_mode = "auto" 17 | -------------------------------------------------------------------------------- /ui/src/app/AppLayout/small-logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SmallLogoImage from '../../assets/images/logo-masthead.svg'; 3 | 4 | interface IProps { 5 | alt: string; 6 | } 7 | 8 | export class SmallLogo extends React.Component { 9 | render() { 10 | return {this.props.alt}; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tools/docker/postgres/conf.d/pg_ssl.conf: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------- 2 | # SSL settings 3 | # -------------------------------------------------------- 4 | 5 | ssl = on 6 | # Provided by docker hub postgres image. Production should use a different cert 7 | ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' 8 | ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' 9 | -------------------------------------------------------------------------------- /ui/src/app/Jobs/jobs-table-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface IJobsTableContext { 4 | selectedJobs: string[]; 5 | setSelectedJobs?: (string) => void; 6 | } 7 | 8 | const defaultState = { 9 | selectedJobs: [], 10 | }; 11 | 12 | const JobsTableContext = createContext({ selectedJobs: [] }); 13 | 14 | export default JobsTableContext; 15 | -------------------------------------------------------------------------------- /ui/src/app/utils/useDocumentTitle.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // a custom hook for setting the page title 4 | export function useDocumentTitle(title: string) { 5 | React.useEffect(() => { 6 | const originalTitle = document.title; 7 | document.title = title; 8 | 9 | return () => { 10 | document.title = originalTitle; 11 | }; 12 | }, [title]); 13 | } 14 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.snap] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /ui/dr-surge.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const indexPath = path.resolve(__dirname, 'dist/index.html'); 4 | const targetFilePath = path.resolve(__dirname, 'dist/200.html'); 5 | // ensure we have bookmarkable url's when publishing to surge 6 | // https://surge.sh/help/adding-a-200-page-for-client-side-routing 7 | fs.createReadStream(indexPath).pipe(fs.createWriteStream(targetFilePath)); 8 | -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.applicationName %> 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ui/src/app/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import React from 'react'; 3 | import { TopToolbar } from '@app/shared/top-toolbar'; 4 | 5 | const Dashboard: React.FunctionComponent = () => { 6 | return ( 7 | 8 | 9 | Dashboard 10 | 11 | 12 | ); 13 | }; 14 | 15 | export { Dashboard }; 16 | -------------------------------------------------------------------------------- /ui/src/app/NewRuleSet/NewRuleSet.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import React from 'react'; 3 | import { TopToolbar } from '@app/shared/top-toolbar'; 4 | 5 | const NewRuleSet: React.FunctionComponent = () => { 6 | return ( 7 | 8 | 9 | New Rule Set 10 | 11 | 12 | ); 13 | }; 14 | 15 | export { NewRuleSet }; 16 | -------------------------------------------------------------------------------- /ui/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | // https://storybook.js.org/blog/storybook-for-webpack-5/ 2 | module.exports = { 3 | // https://gist.github.com/shilman/8856ea1786dcd247139b47b270912324#upgrade 4 | core: { 5 | builder: "webpack5", 6 | }, 7 | stories: ['../stories/*.stories.tsx'], 8 | addons: [ 9 | '@storybook/addon-knobs', 10 | ], 11 | typescript: { 12 | check: false, 13 | checkOptions: {}, 14 | reactDocgen: 'react-docgen-typescript' 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /ui/src/app/app.css: -------------------------------------------------------------------------------- 1 | @import '~@redhat-cloud-services/frontend-components-utilities/index.scss'; 2 | 3 | html, body, #root { 4 | height: 100%; 5 | } 6 | 7 | .pf-c-content { 8 | --pf-c-content--small--Color: red; /* changes all color to red */ 9 | --pf-c-content--blockquote--BorderLeftColor: purple; /* changes all
left border color to purple */ 10 | --pf-c-content--hr--BackgroundColor: lemonchiffon; /* changes a
color to lemonchiffon */ 11 | } 12 | -------------------------------------------------------------------------------- /scripts/common/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | check_vars() { 4 | # Variable validation. 5 | # 6 | # Args: ($@) - 1 or many variable(s) to check 7 | # 8 | local _var_names=("$@") 9 | 10 | for var_name in "${_var_names[@]}"; do 11 | if [[ -z "$var_name" ]]; then 12 | log-debug "${var_name}=${!var_name}" 13 | else 14 | log-err "Environment variable $var_name is not set! Unable to continue." 15 | exit 1 16 | fi 17 | done 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/InventoriesSelect/inventories-select-table-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { InventorySelectType } from '@app/InventoriesSelect/InventoriesSelect'; 3 | 4 | const InventoriesSelectTableContext = createContext<{ 5 | selectedInventory: InventorySelectType | undefined; 6 | setSelectedInventory: ((inventory: InventorySelectType | undefined) => void) | null; 7 | }>({ selectedInventory: undefined, setSelectedInventory: null }); 8 | 9 | export default InventoriesSelectTableContext; 10 | -------------------------------------------------------------------------------- /ui/src/app/RuleSet/sources-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const createRows = (data) => 5 | data.map(({ id, name, type }) => ({ 6 | id, 7 | cells: [ 8 | 9 | 14 | {name || id} 15 | 16 | , 17 | type, 18 | ], 19 | })); 20 | -------------------------------------------------------------------------------- /ui/src/app/RuleSets/rule-sets-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const createRows = (data) => 5 | data.map(({ id, name, rule_count }) => ({ 6 | id, 7 | cells: [ 8 | 9 | 14 | {name} 15 | 16 | , 17 | rule_count, 18 | ], 19 | })); 20 | -------------------------------------------------------------------------------- /ui/src/app/Projects/projects-table-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface IProjectsTableContext { 4 | selectedProjects: string[]; 5 | setSelectedProjects?: (string) => void; 6 | } 7 | 8 | const defaultState = { 9 | selectedProjects: [], 10 | }; 11 | 12 | const ProjectsTableContext = createContext<{ 13 | selectedProjects: string[]; 14 | setSelectedProjects: ((ids: string[]) => void) | null; 15 | }>({ selectedProjects: [], setSelectedProjects: null }); 16 | 17 | export default ProjectsTableContext; 18 | -------------------------------------------------------------------------------- /tools/docker/postgres/conf.d/pg_logging.conf: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # Reporting and Logging 3 | #------------------------------------------------------------------------------ 4 | 5 | # Log statement duration if it exceeds 1s 6 | log_min_duration_statement = 1000 7 | # See: https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-LINE-PREFIX 8 | log_line_prefix = '%m:%b:[%p]: ' 9 | # Log DDL and DML statements 10 | log_statement = 'mod' 11 | log_timezone = 'Etc/UTC' 12 | -------------------------------------------------------------------------------- /ui/src/app/API/Rule.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { AxiosResponse } from 'axios'; 3 | import { getAxiosInstance } from '@app/API/baseApi'; 4 | 5 | const rulesEndpoint = '/api/rules'; 6 | 7 | export const fetchRule = (ruleId: string | number, pagination = defaultSettings): Promise => { 8 | return getAxiosInstance().get(`${rulesEndpoint}/${ruleId}`); 9 | }; 10 | 11 | export const listRules = (pagination = defaultSettings): Promise => 12 | getAxiosInstance().get(rulesEndpoint); 13 | -------------------------------------------------------------------------------- /ui/src/app/RuleBooks/rulebooks-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const createRows = (data) => 5 | data.map(({ id, name, ruleset_count, fire_count }) => ({ 6 | id, 7 | cells: [ 8 | 9 | 14 | {name} 15 | 16 | , 17 | ruleset_count, 18 | fire_count, 19 | ], 20 | })); 21 | -------------------------------------------------------------------------------- /ui/src/app/API/Audit.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { getAxiosInstance } from '@app/API/baseApi'; 3 | import { AxiosResponse } from 'axios'; 4 | 5 | const auditRulesEndpoint = '/api/audit/rules_fired'; 6 | const auditHostsEndpoint = '/api/audit/hosts_changed'; 7 | 8 | export const listAuditRules = (pagination = defaultSettings): Promise => 9 | getAxiosInstance().get(auditRulesEndpoint); 10 | 11 | export const listAuditHosts = (pagination = defaultSettings): Promise => 12 | getAxiosInstance().get(auditHostsEndpoint); 13 | -------------------------------------------------------------------------------- /tools/deploy/eda-frontend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: eda-frontend 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: eda-frontend 10 | strategy: {} 11 | template: 12 | metadata: 13 | labels: 14 | app: eda-frontend 15 | spec: 16 | containers: 17 | - image: eda-frontend 18 | imagePullPolicy: Never 19 | name: eda-frontend 20 | ports: 21 | - containerPort: 8080 22 | resources: {} 23 | restartPolicy: Always 24 | status: {} 25 | -------------------------------------------------------------------------------- /ui/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import ReducerRegistry from '@redhat-cloud-services/frontend-components-utilities/ReducerRegistry'; 2 | import { notificationsReducer } from '@redhat-cloud-services/frontend-components-notifications/redux'; 3 | import { Store } from 'redux'; 4 | 5 | const registerReducers = (registry: ReducerRegistry): void => { 6 | registry.register({ 7 | notifications: notificationsReducer, 8 | }); 9 | }; 10 | 11 | export default (): Store => { 12 | const registry = new ReducerRegistry({}, [...[]]); 13 | registerReducers(registry); 14 | return registry.getStore() as Store; 15 | }; 16 | -------------------------------------------------------------------------------- /tools/docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build eda-frontend 2 | FROM docker.io/node:16-alpine AS ui-builder 3 | WORKDIR /app 4 | 5 | COPY ui/package.json /app/ui/ 6 | RUN cd ui && npm install 7 | 8 | COPY ui /app/ui 9 | 10 | RUN cd ui && npm run build 11 | 12 | FROM docker.io/nginx 13 | ARG NGINX_CONF=tools/docker/nginx/default.conf 14 | ARG NGINX_CONFIGURATION_PATH=/etc/nginx/conf.d/ 15 | 16 | ENV DIST_UI="/opt/app-root/ui/eda" 17 | 18 | ADD ${NGINX_CONF} ${NGINX_CONFIGURATION_PATH} 19 | 20 | # Copy dist dir to final location 21 | RUN mkdir -p ${DIST_UI}/ 22 | COPY --from=ui-builder /app/ui/dist/ ${DIST_UI} 23 | 24 | -------------------------------------------------------------------------------- /ui/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the react seed 4 | title: '' 5 | labels: needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '@app/index'; 4 | import '@patternfly/react-core/dist/styles/base.css' 5 | 6 | if (process.env.NODE_ENV !== 'production') { 7 | const config = { 8 | rules: [ 9 | { 10 | id: 'color-contrast', 11 | enabled: false, 12 | }, 13 | ], 14 | }; 15 | // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef 16 | const axe = require('react-axe'); 17 | axe(React, ReactDOM, 1000, config); 18 | } 19 | 20 | ReactDOM.render(, document.getElementById('root') as HTMLElement); 21 | -------------------------------------------------------------------------------- /tools/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build eda-server 2 | FROM registry.access.redhat.com/ubi8/python-39 3 | ARG USER_ID=${USER_ID:-1001} 4 | WORKDIR $HOME 5 | 6 | USER 0 7 | RUN dnf install -y java-17-openjdk-devel maven 8 | ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk 9 | 10 | RUN pip install ansible && \ 11 | ansible-galaxy collection install git+https://github.com/ansible/event-driven-ansible.git 12 | 13 | COPY requirements.txt /tmp/requirements.txt 14 | RUN pip install -r /tmp/requirements.txt 15 | 16 | COPY . $WORKDIR 17 | RUN chown -R $USER_ID ./ 18 | 19 | USER $USER_ID 20 | RUN pip install --no-deps -e . 21 | 22 | CMD ["eda-server"] 23 | -------------------------------------------------------------------------------- /ui/src/app/API/Extravar.ts: -------------------------------------------------------------------------------- 1 | import { getAxiosInstance } from '@app/API/baseApi'; 2 | import { AxiosResponse } from 'axios'; 3 | 4 | const extravarsEndpoint = '/api/extra_vars'; 5 | const extravarEndpoint = '/api/extra_var'; 6 | 7 | export const listExtraVars = (): Promise => getAxiosInstance().get(extravarsEndpoint); 8 | 9 | export const fetchRuleVars = (varname: string): Promise => { 10 | return getAxiosInstance().get(`${extravarEndpoint}/${varname}`); 11 | }; 12 | 13 | export const fetchExtraVar = (id: string | number): Promise => 14 | getAxiosInstance().get(`${extravarEndpoint}/${id}`); 15 | -------------------------------------------------------------------------------- /ui/src/app/Activation/jobs-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const createRows = (data) => 5 | data.map(({ job_instance_id, name, status, rule, last_fired_at }) => ({ 6 | job_instance_id, 7 | cells: [ 8 | 9 | 14 | {name || job_instance_id} 15 | 16 | , 17 | status, 18 | rule, 19 | last_fired_at, 20 | ], 21 | })); 22 | -------------------------------------------------------------------------------- /tools/docker/postgres/conf.d/pg_jit.conf: -------------------------------------------------------------------------------- 1 | # Disable PostgreSQL JIT compilation 2 | # NOTE(cutwater): This is a workaround for database introspection long execution time. 3 | # Background: For native PostgreSQL enum types, asyncpg connector library executes 4 | # an introspection query [1] once per connection. Disabling JIT significantly reduces 5 | # execution time of the query: 6 | # - JIT enabled: ~600ms 7 | # - JIT disabled ~2.4-2.8ms 8 | # References: 9 | # - 1. https://gist.github.com/cutwater/e83c0eb55448d78965f087ae44c7d4e2 10 | # See also: 11 | # - https://github.com/MagicStack/asyncpg/issues/530 12 | jit = off 13 | -------------------------------------------------------------------------------- /.github/workflows/test-docker-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Test docker deployment 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | test-docker-deployment: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Deploy docker-compose and test it 17 | working-directory: tools/docker 18 | run: | 19 | docker-compose up -d --build 20 | curl -v -q --fail -o /dev/null http://localhost:9000/ping 21 | curl -v -q --fail -o /dev/null http://localhost:8080/eda/index.html 22 | -------------------------------------------------------------------------------- /ui/src/app/RuleSet/rules-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const createRows = (data) => { 5 | return data.map(({ id, name, condition, action, fire_count, last_fired_at }) => ({ 6 | id, 7 | cells: [ 8 | 9 | 14 | {name || id} 15 | 16 | , 17 | condition, 18 | `${Object.keys(action)}`, 19 | fire_count, 20 | last_fired_at, 21 | ], 22 | })); 23 | }; 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/pagination.ts: -------------------------------------------------------------------------------- 1 | import { SortByDirection } from '@patternfly/react-table'; 2 | import { PaginationProps } from '@patternfly/react-core'; 3 | 4 | export interface PaginationConfiguration extends PaginationProps { 5 | limit: number; 6 | count: number; 7 | filter?: string; 8 | sortDirection?: SortByDirection; 9 | } 10 | 11 | export const defaultSettings: PaginationConfiguration = { 12 | limit: 50, 13 | offset: 0, 14 | count: 0, 15 | filter: '', 16 | }; 17 | 18 | export const getCurrentPage = (limit = 1, offset = 0): number => Math.floor(offset / limit) + 1; 19 | 20 | export const getNewPage = (page = 1, offset = 0): number => (page - 1) * offset; 21 | -------------------------------------------------------------------------------- /.github/workflows/ui-pr-test.yml: -------------------------------------------------------------------------------- 1 | name: UI PR test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | pull-request: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./ui 15 | concurrency: 16 | group: ${{ github.head_ref || github.run_id }} 17 | cancel-in-progress: true 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: '16' 23 | cache: 'npm' 24 | cache-dependency-path: ui/package-lock.json 25 | - run: npm ci 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/psf/black 4 | rev: 22.10.0 5 | hooks: 6 | - id: black 7 | - repo: https://github.com/pycqa/isort 8 | rev: 5.10.1 9 | hooks: 10 | - id: isort 11 | - repo: https://github.com/pycqa/flake8 12 | rev: 5.0.4 13 | hooks: 14 | - id: flake8 15 | additional_dependencies: 16 | - flake8-broken-line 17 | - flake8-bugbear 18 | - flake8-comprehensions 19 | - flake8-debugger 20 | - flake8-docstrings 21 | - flake8-eradicate 22 | - flake8-print 23 | - flake8-string-format 24 | - pep8-naming 25 | -------------------------------------------------------------------------------- /ui/src/app/RuleBook/rulesets-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | //TODO - replace the link to the ruleset when the endpoint is updated with that data 5 | export const createRows = (data) => { 6 | return data.map(({ id, name, rule_count, fired_stats }) => ({ 7 | id, 8 | cells: [ 9 | 10 | 15 | {name || id} 16 | 17 | , 18 | rule_count, 19 | fired_stats?.fired_count, 20 | fired_stats?.last_fired_at, 21 | ], 22 | })); 23 | }; 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from setuptools import setup 16 | 17 | if __name__ == "__main__": 18 | setup() 19 | -------------------------------------------------------------------------------- /ui/src/app/API/baseApi.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; 2 | import { stringify } from 'qs'; 3 | 4 | export interface ErrorResponse { 5 | headers?: Headers; 6 | } 7 | 8 | export interface ServerError { 9 | response?: ErrorResponse; 10 | status?: 403 | 404 | 401 | 400 | 429 | 500 | 200; // not a complete list, replace by library with complete interface 11 | config?: AxiosRequestConfig; 12 | } 13 | 14 | const createAxiosInstance = () => { 15 | return axios.create({ 16 | paramsSerializer: (params) => stringify(params), 17 | }); 18 | }; 19 | 20 | const axiosInstance: AxiosInstance = createAxiosInstance(); 21 | 22 | export function getAxiosInstance(): AxiosInstance { 23 | return axiosInstance; 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/app/Rules/rules-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const createRows = (data) => 5 | data.map(({ id, name, ruleset, action, last_fired_at }) => ({ 6 | id, 7 | cells: [ 8 | 9 | 14 | {name} 15 | 16 | , 17 | 18 | 23 | {ruleset?.name || `Ruleset ${ruleset?.id}`} 24 | 25 | , 26 | action, 27 | last_fired_at, 28 | ], 29 | })); 30 | -------------------------------------------------------------------------------- /tools/docker/postgres/conf.d/pg_stat_statements.conf: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------- 2 | # Configuration for library and extension 'pg_stat_statements' 3 | # -------------------------------------------------------- 4 | 5 | # See: https://www.postgresql.org/docs/14/runtime-config-client.html#GUC-SHARED-PRELOAD-LIBRARIES 6 | # The pg_stat_statements module requires a library to be loaded at engine start. 7 | shared_preload_libraries = 'pg_stat_statements' 8 | 9 | # PostgreSQL pg_stat_statements configuration 10 | # See https://www.postgresql.org/docs/current/pgstatstatements.html#id-1.11.7.39.9 11 | 12 | # Maximum number of unique statements to track 13 | pg_stat_statements.max=2000 14 | # Save between restarts 15 | pg_stat_statements.save='off' 16 | # Track DDL 17 | pg_stat_statements.track_utility='off' 18 | -------------------------------------------------------------------------------- /ui/src/app/API/Rulebook.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { AxiosResponse } from 'axios'; 3 | import { getAxiosInstance } from '@app/API/baseApi'; 4 | 5 | const rulebooksEndpoint = '/api/rulebooks'; 6 | 7 | export const listRulebooks = (): Promise => getAxiosInstance().get(rulebooksEndpoint); 8 | 9 | export const fetchRulebook = (id: string | number): Promise => 10 | getAxiosInstance().get(`${rulebooksEndpoint}/${id}`); 11 | 12 | export const fetchRulebookRuleSets = (id: string | number, pagination = defaultSettings): Promise => { 13 | return getAxiosInstance() 14 | .get(`${rulebooksEndpoint}/${id}/rulesets`) 15 | }; 16 | 17 | export const listRuleBooks = (pagination = defaultSettings): Promise => 18 | getAxiosInstance().get(rulebooksEndpoint); 19 | -------------------------------------------------------------------------------- /scripts/common/logging.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # colors 4 | ERR=$(tput setaf 1) 5 | INFO=$(tput setaf 178) 6 | WARN=$(tput setaf 165) 7 | TRACE=$(tput setaf 27) 8 | TS=$(tput setaf 2) 9 | TAG=$(tput setaf 10) 10 | RESET=$(tput sgr0) 11 | 12 | # timestamp 13 | TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") 14 | 15 | log(){ 16 | local _tag_name=${1} 17 | local _msg=${@:2} 18 | 19 | printf "${TS}${TIMESTAMP} ${TAG}[${_tag_name}\t] ${_msg}\n" 20 | printf ${RESET} 21 | } 22 | 23 | log-info() { 24 | log "INFO" "${INFO} $@" 25 | } 26 | 27 | log-warn() { 28 | log "WARNING" "${WARN} $@" 29 | } 30 | 31 | log-err() { 32 | log "ERROR" "${ERR} $@" 33 | } 34 | 35 | log-debug() { 36 | local _debug=$(tr '[:upper:]' '[:lower:]' <<<"$DEBUG") 37 | if [[ ! -z "${DEBUG}" && ${_debug} == true ]];then 38 | log "DEBUG" "${TRACE} $@" 39 | fi 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/app/API/auth.ts: -------------------------------------------------------------------------------- 1 | import { getAxiosInstance } from '@app/API/baseApi'; 2 | import { AxiosResponse } from 'axios'; 3 | 4 | const loginEndpoint = '/api/auth/jwt/login'; 5 | const getUserEndpoint = '/api/users/me'; 6 | const logoutEndpoint = '/api/auth/jwt/logout'; 7 | 8 | export const loginUser = (loginData: string): Promise => 9 | getAxiosInstance().post(loginEndpoint, loginData, { 10 | headers: { 11 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 12 | }, 13 | }); 14 | 15 | export const getUser = (): Promise => { 16 | return getAxiosInstance().get(getUserEndpoint); 17 | }; 18 | 19 | export const logoutUser = (): Promise => 20 | getAxiosInstance().post(logoutEndpoint, '', { 21 | headers: { 22 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/eda_server/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import NamedTuple 16 | 17 | from pydantic import BaseModel 18 | 19 | 20 | class JobEnd(NamedTuple): 21 | job_id: str 22 | 23 | 24 | class ActivationErrorMessage(BaseModel): 25 | message: str 26 | detail: str 27 | -------------------------------------------------------------------------------- /tools/deploy/eda-server/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: eda-server 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: eda-server 10 | strategy: {} 11 | template: 12 | metadata: 13 | labels: 14 | app: eda-server 15 | spec: 16 | containers: 17 | - args: 18 | - /bin/bash 19 | - -c 20 | - alembic upgrade head && eda-server 21 | env: 22 | - name: EDA_DATABASE_URL 23 | value: postgresql+asyncpg://postgres:secret@postgres/eda_server 24 | - name: EDA_HOST 25 | value: 0.0.0.0 26 | image: eda-server 27 | imagePullPolicy: Never 28 | name: eda-server 29 | ports: 30 | - containerPort: 8080 31 | resources: {} 32 | restartPolicy: Always 33 | status: {} 34 | -------------------------------------------------------------------------------- /src/eda_server/config/logging.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | disable_existing_loggers: False 4 | formatters: 5 | default: 6 | '()': 'uvicorn.logging.DefaultFormatter' 7 | fmt: '[%(process)d] %(asctime)s %(levelname)-8s %(message)s' 8 | access: 9 | '()': 'uvicorn.logging.AccessFormatter' 10 | fmt: '[%(process)d] %(asctime)s %(levelname)-8s %(client_addr)s - "%(request_line)s" %(status_code)s' 11 | handlers: 12 | default: 13 | formatter: 'default' 14 | class: 'logging.StreamHandler' 15 | stream: 'ext://sys.stderr' 16 | access: 17 | formatter: 'access' 18 | class: 'logging.StreamHandler' 19 | stream: 'ext://sys.stdout' 20 | root: 21 | handlers: ['default'] 22 | loggers: 23 | eda_server: 24 | level: 'DEBUG' 25 | uvicorn: 26 | level: 'DEBUG' 27 | uvicorn.error: 28 | level: 'DEBUG' 29 | uvicorn.access: 30 | level: 'DEBUG' 31 | handlers: ['access'] 32 | propagate: false 33 | -------------------------------------------------------------------------------- /ui/stylePaths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stylePaths: [ 4 | path.resolve(__dirname, 'src'), 5 | path.resolve(__dirname, 'node_modules/patternfly'), 6 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), 7 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), 8 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), 9 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), 10 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), 11 | path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), 12 | path.resolve( 13 | __dirname, 14 | 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css' 15 | ), 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202210261523_f592f6ef1e3a_new_rbac_enum_values.py: -------------------------------------------------------------------------------- 1 | """New RBAC enum values. 2 | 3 | Revision ID: f592f6ef1e3a 4 | Revises: c703cd56f6b0 5 | Create Date: 2022-10-26 15:23:06.016126+00:00 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "f592f6ef1e3a" 13 | down_revision = "ce5cc33ecdb6" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | RESOURCE_TYPES = [ 19 | "activation", 20 | "activation_instance", 21 | "audit_rule", 22 | "job", 23 | "user", 24 | "task", 25 | ] 26 | ADD_RESOURCE_TYPE_QUERY = """\ 27 | ALTER TYPE "resource_type_enum" ADD VALUE IF NOT EXISTS '{value}' 28 | """ 29 | 30 | 31 | def upgrade() -> None: 32 | for value in RESOURCE_TYPES: 33 | op.execute(sa.text(ADD_RESOURCE_TYPE_QUERY.format(value=value))) 34 | 35 | 36 | def downgrade() -> None: 37 | pass 38 | -------------------------------------------------------------------------------- /src/eda_server/schema/user.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import uuid 16 | 17 | from fastapi_users import schemas 18 | 19 | 20 | class UserRead(schemas.BaseUser[uuid.UUID]): 21 | pass 22 | 23 | 24 | class UserCreate(schemas.BaseUserCreate): 25 | pass 26 | 27 | 28 | class UserUpdate(schemas.BaseUserUpdate): 29 | pass 30 | -------------------------------------------------------------------------------- /ui/src/global-styles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | /** 4 | * Use direct css imports for FCE components 5 | * This will save some bundle size 6 | */ 7 | const GlobalStyle = createGlobalStyle` 8 | .disabled-link { 9 | pointer-events: none 10 | } 11 | 12 | h2.pf-c-nav__section-title { 13 | font-size: 18px; 14 | font-weight: var(--pf-global--FontWeight--semi-bold); 15 | } 16 | 17 | .icon-danger-fill { 18 | fill: var(--pf-global--danger-color--100) 19 | } 20 | 21 | .bottom-pagination-container { 22 | width: 100% 23 | } 24 | 25 | .global-primary-background { 26 | background-color: var(--pf-global--BackgroundColor--100) 27 | } 28 | 29 | .full-height { 30 | min-height: 100%; 31 | } 32 | 33 | .content-layout { 34 | display: flex; 35 | flex-direction: column; 36 | } 37 | 38 | .banner-container { 39 | padding-top: 0px; 40 | padding-bottom: 0px; 41 | } 42 | `; 43 | 44 | export default GlobalStyle; 45 | -------------------------------------------------------------------------------- /tests/integration/api/test_extra_vars.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from fastapi import status as status_codes 16 | from httpx import AsyncClient 17 | 18 | 19 | async def test_read_extra_var_not_found(client: AsyncClient): 20 | response = await client.get("/api/extra_var/42") 21 | 22 | assert response.status_code == status_codes.HTTP_404_NOT_FOUND 23 | -------------------------------------------------------------------------------- /src/eda_server/schema/playbook.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional 16 | 17 | from pydantic import BaseModel, StrictStr 18 | 19 | 20 | class PlaybookRead(BaseModel): 21 | id: int 22 | name: StrictStr 23 | playbook: StrictStr 24 | project_id: int 25 | 26 | 27 | class PlaybookRef(BaseModel): 28 | id: Optional[int] 29 | name: StrictStr 30 | -------------------------------------------------------------------------------- /tools/deploy/postgres/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: postgres 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: postgres 10 | strategy: 11 | type: Recreate 12 | template: 13 | metadata: 14 | labels: 15 | app: postgres 16 | spec: 17 | containers: 18 | - env: 19 | - name: POSTGRES_DB 20 | value: eda_server 21 | - name: POSTGRES_PASSWORD 22 | value: secret 23 | image: docker.io/library/postgres:13 24 | name: postgres 25 | ports: 26 | - containerPort: 5432 27 | resources: {} 28 | volumeMounts: 29 | - mountPath: /var/lib/postgresql/data 30 | name: postgres-data 31 | restartPolicy: Always 32 | volumes: 33 | - name: postgres-data 34 | persistentVolumeClaim: 35 | claimName: postgres-data 36 | status: {} 37 | -------------------------------------------------------------------------------- /ui/src/app/shared/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink } from 'react-router-dom'; 4 | import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; 5 | 6 | const Breadcrumbs = ({ breadcrumbs }) => { 7 | return breadcrumbs ? ( 8 | 9 | {breadcrumbs.map(({ to, id, title }, idx) => ( 10 | 11 | {(to && ( 12 | false} exact to={to}> 13 | {title} 14 | 15 | )) || 16 | title} 17 | 18 | ))} 19 | 20 | ) : null; 21 | }; 22 | 23 | Breadcrumbs.propTypes = { 24 | breadcrumbs: PropTypes.arrayOf( 25 | PropTypes.shape({ 26 | title: PropTypes.string.isRequired, 27 | to: PropTypes.string, 28 | }) 29 | ), 30 | }; 31 | 32 | export default Breadcrumbs; 33 | -------------------------------------------------------------------------------- /tests/integration/api/test_playbook.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # TODO(cutwater): Add unit tests for API endpoints 16 | from fastapi import status as status_codes 17 | from httpx import AsyncClient 18 | 19 | 20 | async def test_read_playbook_not_found(client: AsyncClient): 21 | response = await client.get("/api/playbook/42") 22 | 23 | assert response.status_code == status_codes.HTTP_404_NOT_FOUND 24 | -------------------------------------------------------------------------------- /src/eda_server/schema/extra_vars.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional 16 | 17 | from pydantic import BaseModel, StrictStr 18 | 19 | 20 | class ExtraVarsCreate(BaseModel): 21 | name: StrictStr 22 | extra_var: StrictStr 23 | 24 | 25 | class ExtraVarsRead(ExtraVarsCreate): 26 | id: int 27 | 28 | 29 | class ExtraVarsRef(BaseModel): 30 | name: StrictStr 31 | id: Optional[int] 32 | -------------------------------------------------------------------------------- /ui/src/app/API/Inventory.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { getAxiosInstance } from '@app/API/baseApi'; 3 | import { AxiosResponse } from 'axios'; 4 | 5 | export interface NewInventoryType { 6 | name: string; 7 | description?: string; 8 | inventory?: string; 9 | } 10 | 11 | const inventoriesEndpoint = '/api/inventories'; 12 | const inventoryEndpoint = '/api/inventory'; 13 | 14 | export const listInventories = (pagination = defaultSettings): Promise => 15 | getAxiosInstance().get(inventoriesEndpoint); 16 | 17 | export const fetchInventory = (id: string | number | undefined): Promise => 18 | getAxiosInstance().get(`${inventoryEndpoint}/${id}`); 19 | 20 | export const addInventory = (data: NewInventoryType): Promise => 21 | getAxiosInstance().post(inventoryEndpoint, data); 22 | export const removeInventory = (inventoryId: string | number): Promise => 23 | getAxiosInstance().delete(`${inventoryEndpoint}/${inventoryId}`); 24 | -------------------------------------------------------------------------------- /ui/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | const appConfig = require('../webpack.common'); 4 | const { stylePaths } = require("../stylePaths"); 5 | 6 | module.exports = ({ config, mode }) => { 7 | config.module.rules = []; 8 | config.module.rules.push(...appConfig(mode).module.rules); 9 | config.module.rules.push({ 10 | test: /\.css$/, 11 | include: [ 12 | path.resolve(__dirname, '../node_modules/@storybook'), 13 | ...stylePaths 14 | ], 15 | use: ["style-loader", "css-loader"] 16 | }); 17 | config.module.rules.push({ 18 | test: /\.tsx?$/, 19 | include: path.resolve(__dirname, '../src'), 20 | use: [ 21 | require.resolve('react-docgen-typescript-loader'), 22 | ], 23 | }) 24 | config.resolve.plugins = [ 25 | new TsconfigPathsPlugin({ 26 | configFile: path.resolve(__dirname, "../tsconfig.json") 27 | }) 28 | ]; 29 | config.resolve.extensions.push('.ts', '.tsx'); 30 | return config; 31 | }; 32 | -------------------------------------------------------------------------------- /ui/src/app/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ExclamationTriangleIcon } from '@patternfly/react-icons'; 3 | import { PageSection, Title, Button, EmptyState, EmptyStateIcon, EmptyStateBody } from '@patternfly/react-core'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | const NotFound: React.FunctionComponent = () => { 7 | function GoHomeBtn() { 8 | const history = useHistory(); 9 | function handleClick() { 10 | history.push('/'); 11 | } 12 | return ; 13 | } 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 404 Page not found 21 | 22 | We didn't find a page that matches the address you navigated to. 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export { NotFound }; 30 | -------------------------------------------------------------------------------- /ui/src/app/Var/Var.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import { useParams } from 'react-router-dom'; 3 | import React, { useState, useEffect } from 'react'; 4 | import { CodeBlock, CodeBlockCode } from '@patternfly/react-core'; 5 | import { TopToolbar } from '@app/shared/top-toolbar'; 6 | import { ExtraVarType } from '@app/Vars/Vars'; 7 | import {fetchExtraVar} from "@app/API/Extravar"; 8 | 9 | const Var: React.FunctionComponent = () => { 10 | const [extraVar, setVar] = useState(undefined); 11 | 12 | const { id } = useParams(); 13 | 14 | useEffect(() => { 15 | fetchExtraVar(id) 16 | .then((data) => setVar(data.data)); 17 | }, []); 18 | 19 | return ( 20 | 21 | 22 | {`Var ${extraVar?.name}`} 23 | 24 | 25 | {extraVar?.extra_var} 26 | 27 | 28 | ); 29 | }; 30 | 31 | export { Var }; 32 | -------------------------------------------------------------------------------- /src/eda_server/db/provider.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from sqlalchemy.ext.asyncio import create_async_engine 16 | 17 | from eda_server.db.session import create_session_factory 18 | 19 | 20 | class DatabaseProvider: 21 | def __init__(self, database_url: str): 22 | self.engine = create_async_engine(database_url) 23 | self.session_factory = create_session_factory(self.engine) 24 | 25 | async def close(self) -> None: 26 | await self.engine.dispose() 27 | -------------------------------------------------------------------------------- /ui/config/test-setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import React from 'react'; 5 | import { configure, mount, render, shallow } from 'enzyme'; 6 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 7 | import fetchMock from 'jest-fetch-mock'; 8 | 9 | /** 10 | * mock fetch 11 | */ 12 | import 'whatwg-fetch'; 13 | 14 | import reactIntl from 'react-intl'; 15 | import 'jest-canvas-mock'; 16 | 17 | /** 18 | * mock react-intl in tests 19 | * 20 | */ 21 | fetchMock.enableMocks(); 22 | 23 | configure({ adapter: new Adapter() }); 24 | 25 | global.shallow = shallow; 26 | global.render = render; 27 | global.mount = mount; 28 | global.React = React; 29 | global.fetchMock = fetchMock; 30 | 31 | /** 32 | * Setup JSDOM 33 | */ 34 | global.SVGPathElement = function () {}; 35 | 36 | global.MutationObserver = class { 37 | constructor(callback) {} 38 | disconnect() {} 39 | observe(element, initObject) {} 40 | }; 41 | 42 | // prepare root element 43 | const root = document.createElement('div'); 44 | root.id = 'root'; 45 | document.body.appendChild(root); 46 | Element.prototype.scrollTo = () => {}; 47 | -------------------------------------------------------------------------------- /ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Red Hat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | const { stylePaths } = require('../stylePaths'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 7 | const TerserJSPlugin = require('terser-webpack-plugin'); 8 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 9 | 10 | module.exports = merge(common('production'), { 11 | mode: 'production', 12 | devtool: 'source-map', 13 | optimization: { 14 | minimizer: [new TerserJSPlugin({})], 15 | }, 16 | plugins: [ 17 | new MiniCssExtractPlugin({ 18 | filename: '[name].css', 19 | chunkFilename: '[name].bundle.css', 20 | }), 21 | ], 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.s?[ac]ss$/, 26 | use: [ 27 | MiniCssExtractPlugin.loader, 28 | 'css-loader', 29 | { 30 | loader: 'sass-loader', 31 | }, 32 | 'sass-loader', 33 | ], 34 | }, 35 | ], 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /ui/src/app/Jobs/jobs-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Checkbox } from '@patternfly/react-core'; 4 | import JobsTableContext from './jobs-table-context'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | export const SelectBox = ({ id }) => { 8 | const { selectedJobs, setSelectedJobs } = useContext(JobsTableContext); 9 | 10 | return ( 11 | (setSelectedJobs ? setSelectedJobs(id) : '')} 15 | /> 16 | ); 17 | }; 18 | 19 | SelectBox.propTypes = { 20 | id: PropTypes.string.isRequired, 21 | }; 22 | 23 | export const createRows = (data) => 24 | data.map(({ id }) => ({ 25 | id, 26 | cells: [ 27 | 28 | 29 | , 30 | 31 | 36 | {id} 37 | 38 | , 39 | ], 40 | })); 41 | -------------------------------------------------------------------------------- /ui/src/app/API/Job.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { AxiosResponse } from 'axios'; 3 | import { getAxiosInstance } from '@app/API/baseApi'; 4 | 5 | export interface NewJobType { 6 | name?: string; 7 | playbook_id: string; 8 | inventory_id: string; 9 | extra_var_id: string; 10 | } 11 | 12 | const jobEndpoint = '/api/job_instance'; 13 | const jobsEndpoint = '/api/job_instances'; 14 | const eventsEndpoint = '/api/job_instance_events'; 15 | 16 | export const fetchJob = (id: string | number | undefined): Promise => 17 | getAxiosInstance().get(`${jobEndpoint}/${id}`); 18 | 19 | export const fetchJobEvents = (id: string | number): Promise => 20 | getAxiosInstance().get(`${eventsEndpoint}/${id}`); 21 | 22 | export const addJob = (jobData: NewJobType): Promise => getAxiosInstance().post(jobEndpoint, jobData); 23 | 24 | export const listJobs = (pagination = defaultSettings): Promise => getAxiosInstance().get(jobsEndpoint); 25 | 26 | export const removeJob = (jobId: string | number): Promise => 27 | getAxiosInstance().delete(`${jobEndpoint}/${jobId}`); 28 | -------------------------------------------------------------------------------- /ui/src/app/Inventory/InventoryLinks.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import { useParams } from 'react-router-dom'; 3 | import React, { useState, useEffect } from 'react'; 4 | import { CodeBlock, CodeBlockCode } from '@patternfly/react-core'; 5 | import { TopToolbar } from '@app/shared/top-toolbar'; 6 | import { InventoryType } from '@app/RuleSets/RuleSets'; 7 | import { fetchInventory } from '@app/API/Inventory'; 8 | 9 | const Inventory: React.FunctionComponent = () => { 10 | const [inventory, setInventory] = useState(undefined); 11 | 12 | const { id } = useParams>(); 13 | 14 | useEffect(() => { 15 | if (!id) { 16 | return; 17 | } 18 | fetchInventory(id).then((data) => setInventory(data.data)); 19 | }, [id]); 20 | 21 | return ( 22 | 23 | 24 | Inventory 25 | 26 | 27 | {inventory?.inventory} 28 | 29 | 30 | ); 31 | }; 32 | 33 | export { Inventory }; 34 | -------------------------------------------------------------------------------- /ui/src/app/AuditView/audit-hosts-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Text, TextVariants } from '@patternfly/react-core'; 4 | 5 | export const createRows = (data) => 6 | data.map(({ host, rule, ruleset, fired_date }) => ({ 7 | cells: [ 8 | host, 9 | 10 | 15 | {rule?.name || rule?.id} 16 | 17 | , 18 | 19 | 24 | {ruleset?.name || ruleset?.id} 25 | 26 | , 27 | 28 | 29 | {new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeStyle: 'long' }).format( 30 | new Date(fired_date || 0) 31 | )} 32 | 33 | , 34 | ], 35 | })); 36 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "sourceMap": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "target": "es5", 10 | "lib": ["es6", "dom"], 11 | "skipLibCheck": true, 12 | "jsx": "react", 13 | "moduleResolution": "node", 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noImplicitAny": false, 18 | "allowJs": true, 19 | "esModuleInterop": true, 20 | "allowSyntheticDefaultImports": true, 21 | "noErrorTruncation": true, 22 | "strict": true, // Never set this rule to false. Setting strict to false basically turns off any type check and we could not use TS at all. 23 | "paths": { 24 | "@app/*": ["src/app/*"], 25 | "@assets/*": ["node_modules/@patternfly/react-core/dist/styles/assets/*"] 26 | }, 27 | "typeRoots": [ 28 | "./node_modules/@types", 29 | "./src/types/" 30 | ] 31 | }, 32 | "include": [ 33 | "**/*.ts", 34 | "**/*.tsx", 35 | "**/*.jsx", 36 | "**/*.js" 37 | ], 38 | "exclude": ["./dist", "node_modules"] 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import { IntlProvider } from 'react-intl'; 4 | import '@patternfly/react-core/dist/styles/base.css'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | import { AppLayout } from '@app/AppLayout/AppLayout'; 7 | import { AppRoutes } from '@app/routes'; 8 | import { Login } from '@app/Login/Login'; 9 | import '@app/app.css'; 10 | import GlobalStyle from '../global-styles'; 11 | import { Provider } from 'react-redux'; 12 | import store from '../store'; 13 | 14 | const App: React.FunctionComponent = () => ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /ui/src/app/InventoriesSelect/inventories-select-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import InventoriesSelectTableContext from '@app/InventoriesSelect/inventories-select-table-context'; 3 | import { Radio } from '@patternfly/react-core'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export const SelectRadio = ({ id, label, description }) => { 7 | const { selectedInventory, setSelectedInventory } = useContext(InventoriesSelectTableContext); 8 | return ( 9 | (setSelectedInventory ? setSelectedInventory({ id: id, name: label }) : undefined)} 16 | /> 17 | ); 18 | }; 19 | 20 | SelectRadio.propTypes = { 21 | id: PropTypes.string.isRequired, 22 | }; 23 | 24 | export const createRows = (data) => { 25 | return data.map(({ id, name }) => ({ 26 | id, 27 | cells: [ 28 | 29 | 30 | , 31 | ], 32 | })); 33 | }; 34 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push: 5 | branches: [ 'main' ] 6 | pull_request: 7 | branches: [ 'main' ] 8 | 9 | jobs: 10 | flake8: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.10' 19 | - name: Install dependencies 20 | run: python -m pip install -r requirements_lint.txt 21 | - name: Lint with flake8 22 | run: flake8 . --count --show-source --statistics 23 | 24 | isort: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: '3.10' 33 | - name: Install dependencies 34 | run: python -m pip install isort 35 | - name: Lint with isort 36 | run: isort . --check --diff 37 | 38 | black: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | - name: black 44 | uses: psf/black@stable 45 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209101031_873701d68c86_uuid_ossp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Enable uuid-ossp PostgreSQL extension. 16 | 17 | Revision ID: 873701d68c86 18 | Revises: bc14081ad342 19 | Create Date: 2022-09-10 10:31:35.781229+00:00 20 | """ 21 | 22 | from alembic import op 23 | 24 | # revision identifiers, used by Alembic. 25 | revision = "873701d68c86" 26 | down_revision = "bc14081ad342" 27 | branch_labels = None 28 | depends_on = None 29 | 30 | 31 | def upgrade() -> None: 32 | op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"') 33 | 34 | 35 | def downgrade() -> None: 36 | pass 37 | -------------------------------------------------------------------------------- /tools/docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server_tokens off; 2 | server { 3 | listen 8080 default_server; 4 | listen [::]:8080; 5 | server_name _; 6 | 7 | location ~ ^/api/ws[0-9a-z-]+ { 8 | proxy_pass http://eda-server:9000; 9 | proxy_set_header X-Forwarded-Proto $scheme; 10 | proxy_set_header X-Forwarded-Port $server_port; 11 | proxy_set_header Host $http_host; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | 14 | proxy_http_version 1.1; 15 | proxy_set_header Upgrade $http_upgrade; 16 | proxy_set_header Connection "Upgrade"; 17 | } 18 | 19 | location ~ ^/api/ { 20 | proxy_pass http://eda-server:9000; 21 | proxy_set_header Host $http_host; 22 | proxy_set_header X-Forwarded-Host $host; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | proxy_set_header X-Forwarded-Port $server_port; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | } 27 | 28 | location /eda { 29 | alias /opt/app-root/ui/eda; 30 | try_files $uri $uri/ /index.html =404; 31 | } 32 | 33 | location = / { 34 | return 301 http://$http_host/eda; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = eda-server 3 | version = 0.1.0 4 | author = Red Hat, Inc. 5 | author_email = info@ansible.com 6 | url = https://github.com/ansible/eda-server 7 | license = Apache-2.0 8 | 9 | [options] 10 | zip_safe = False 11 | include_package_data = True 12 | packages = find: 13 | package_dir = 14 | =src 15 | 16 | python_requires = >=3.9 17 | install_requires = file:requirements.txt 18 | 19 | [options.packages.find] 20 | where = src 21 | 22 | [options.entry_points] 23 | console_scripts = 24 | eda-server = eda_server.main:main 25 | 26 | 27 | [flake8] 28 | extend-exclude = docs, ui, ./.venv 29 | # show-source = True 30 | # Flake8 default ignore list: 31 | # ['W504', 'B904', 'B901', 'E24', 'W503', 'B950', 'E123', 'E704', 'B903', 'E121', 'B902', 'E226', 'E126'] 32 | extend-ignore = 33 | E203, # Whitespace before ':' (false positive in slices, handled by black. 34 | # see: https://github.com/psf/black/issues/315) 35 | D1, # Missing docstrings errors 36 | extend-immutable-calls = 37 | Depends 38 | # Fix pep8-naming: False positive N805 when running against `pydantic.validator`. 39 | # See https://github.com/PyCQA/pep8-naming/issues/169 40 | classmethod-decorators = 41 | classmethod 42 | validator 43 | -------------------------------------------------------------------------------- /tests/integration/test_root_handlers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import yaml 16 | from fastapi import status as status_codes 17 | from httpx import AsyncClient 18 | 19 | 20 | async def test_ping(client: AsyncClient): 21 | response = await client.get("/ping") 22 | assert response.status_code == status_codes.HTTP_200_OK 23 | assert response.json() == {"ping": "pong!"} 24 | 25 | 26 | async def test_openapi_yaml(client: AsyncClient): 27 | response = await client.get("/api/openapi.yml") 28 | assert response.status_code == status_codes.HTTP_200_OK 29 | data = yaml.safe_load(response.text) 30 | assert data["info"]["title"] == "Ansible Events API" 31 | -------------------------------------------------------------------------------- /src/eda_server/db/dependency.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import AsyncGenerator 16 | 17 | import sqlalchemy.orm 18 | from fastapi import Depends 19 | from sqlalchemy.ext.asyncio import AsyncSession 20 | 21 | 22 | def get_db_session_factory() -> sqlalchemy.orm.sessionmaker: 23 | """Database sessionmaker dependency stub.""" 24 | raise NotImplementedError 25 | 26 | 27 | async def get_db_session( 28 | session_factory: sqlalchemy.orm.sessionmaker = Depends( 29 | get_db_session_factory 30 | ), 31 | ) -> AsyncGenerator[AsyncSession, None]: 32 | """Database session dependency.""" 33 | async with session_factory() as session: 34 | yield session 35 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | # Copyright ${create_date.year} Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """${message} 16 | 17 | Revision ID: ${up_revision} 18 | Revises: ${down_revision | comma,n} 19 | Create Date: ${create_date} 20 | """ 21 | 22 | from alembic import op 23 | import sqlalchemy as sa 24 | ${imports if imports else ""} 25 | 26 | # revision identifiers, used by Alembic. 27 | revision = ${repr(up_revision)} 28 | down_revision = ${repr(down_revision)} 29 | branch_labels = ${repr(branch_labels)} 30 | depends_on = ${repr(depends_on)} 31 | 32 | 33 | def upgrade() -> None: 34 | ${upgrades if upgrades else "pass"} 35 | 36 | 37 | def downgrade() -> None: 38 | ${downgrades if downgrades else "pass"} 39 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209162048_9f6b44aa0df8_project_name_unique.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """project name unique. 16 | 17 | Revision ID: 9f6b44aa0df8 18 | Revises: 34bca36e407d 19 | Create Date: 2022-09-16 20:48:01.987096+00:00 20 | """ 21 | 22 | from alembic import op 23 | 24 | # revision identifiers, used by Alembic. 25 | revision = "9f6b44aa0df8" 26 | down_revision = "34bca36e407d" 27 | branch_labels = None 28 | depends_on = None 29 | 30 | 31 | def upgrade() -> None: 32 | op.create_unique_constraint(op.f("uq_project_name"), "project", ["name"]) 33 | 34 | 35 | def downgrade() -> None: 36 | op.drop_constraint(op.f("uq_project_name"), "project", type_="unique") 37 | -------------------------------------------------------------------------------- /ui/src/app/Inventories/inventories-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import InventoriesTableContext from '@app/Inventories/inventories-table-context'; 4 | import { Checkbox } from '@patternfly/react-core'; 5 | import PropTypes from 'prop-types'; 6 | 7 | export const SelectBox = ({ id }) => { 8 | const { selectedInventories, setSelectedInventories } = useContext(InventoriesTableContext); 9 | 10 | return ( 11 | (setSelectedInventories ? setSelectedInventories(id) : '')} 15 | /> 16 | ); 17 | }; 18 | 19 | SelectBox.propTypes = { 20 | id: PropTypes.string.isRequired, 21 | }; 22 | 23 | export const createRows = (data) => 24 | data.map(({ id, name, source }) => ({ 25 | id, 26 | cells: [ 27 | 28 | 29 | , 30 | 31 | 36 | {name} 37 | 38 | , 39 | source, 40 | ], 41 | })); 42 | -------------------------------------------------------------------------------- /src/eda_server/schema/role.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import uuid 16 | 17 | from pydantic import BaseModel, StrictBool 18 | 19 | from eda_server.types import Action, ResourceType 20 | 21 | 22 | class RoleRead(BaseModel): 23 | id: uuid.UUID 24 | name: str 25 | description: str 26 | is_default: StrictBool 27 | 28 | 29 | class RoleCreate(BaseModel): 30 | name: str 31 | description: str = "" 32 | is_default: StrictBool = False 33 | 34 | 35 | class RolePermissionRead(BaseModel): 36 | id: uuid.UUID 37 | resource_type: ResourceType 38 | action: Action 39 | 40 | 41 | class RolePermissionCreate(BaseModel): 42 | resource_type: ResourceType 43 | action: Action 44 | -------------------------------------------------------------------------------- /src/eda_server/db/models/base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sqlalchemy 16 | from sqlalchemy.orm import declarative_base 17 | 18 | __all__ = ( 19 | "Base", 20 | "metadata", 21 | ) 22 | 23 | NAMING_CONVENTION = { 24 | # Index 25 | "ix": "ix_%(table_name)s_%(column_0_N_name)s", 26 | # Unique constraint 27 | "uq": "uq_%(table_name)s_%(column_0_N_name)s", 28 | # Check 29 | "ck": "ck_%(table_name)s_%(constraint_name)s", 30 | # Foreign key 31 | "fk": "fk_%(table_name)s_%(column_0_N_name)s", 32 | # Primary key 33 | "pk": "pk_%(table_name)s", 34 | } 35 | metadata = sqlalchemy.MetaData(naming_convention=NAMING_CONVENTION) 36 | Base = declarative_base(metadata=metadata) 37 | -------------------------------------------------------------------------------- /ui/src/app/Playbook/Playbook.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import { useParams } from 'react-router-dom'; 3 | import React, { useState, useEffect } from 'react'; 4 | import { CodeBlock, CodeBlockCode } from '@patternfly/react-core'; 5 | import { getServer } from '@app/utils/utils'; 6 | import { TopToolbar } from '@app/shared/top-toolbar'; 7 | import { PlaybookType } from '@app/RuleSets/RuleSets'; 8 | 9 | const endpoint = 'http://' + getServer() + '/api/playbook/'; 10 | 11 | const Playbook: React.FunctionComponent = () => { 12 | const [playbook, setPlaybook] = useState(undefined); 13 | 14 | const { id } = useParams<{ id: string }>(); 15 | 16 | useEffect(() => { 17 | fetch(endpoint + id, { 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | }) 22 | .then((response) => response.json()) 23 | .then((data) => setPlaybook(data)); 24 | }, []); 25 | 26 | return ( 27 | 28 | 29 | {`Playbook ${playbook?.name}`} 30 | 31 | 32 | {playbook?.playbook} 33 | 34 | 35 | ); 36 | }; 37 | 38 | export { Playbook }; 39 | -------------------------------------------------------------------------------- /ui/config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | const { stylePaths } = require('../stylePaths'); 5 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 6 | const HOST = process.env.HOST || 'localhost'; 7 | const PORT = process.env.PORT || '8080'; 8 | 9 | module.exports = merge(common('development'), { 10 | mode: 'development', 11 | devtool: 'eval-source-map', 12 | output: { 13 | publicPath: '/' 14 | }, 15 | devServer: { 16 | static: './dist', 17 | host: HOST, 18 | port: PORT, 19 | compress: true, 20 | historyApiFallback: true, 21 | open: true, 22 | headers: { 23 | 'Access-Control-Allow-Origin': '*', 24 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 25 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 26 | }, 27 | proxy: { 28 | '/api': { 29 | target: 'http://localhost:9000', 30 | changeOrigin: true, 31 | ws: true, 32 | }, 33 | }, 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.(sa|sc|c)ss$/i, 39 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], 40 | }, 41 | ], 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /ui/src/app/shared/app-tabs.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react'; 3 | import { Tabs, Tab } from '@patternfly/react-core'; 4 | import { useHistory, useLocation } from 'react-router-dom'; 5 | 6 | export interface AppTabsProps { 7 | tabItems: { 8 | name: string; 9 | eventKey: number; 10 | title: React.ReactNode; 11 | }[]; 12 | defaultActive?: number; 13 | } 14 | const AppTabs: React.ComponentType = ({ tabItems, defaultActive = 1 }) => { 15 | const { push } = useHistory(); 16 | const { pathname, search } = useLocation(); 17 | const activeTab = tabItems.find(({ name }) => name.split('/').pop() === pathname.split('/').pop()); 18 | const handleTabClick = (_event: React.MouseEvent, tabIndex: number | string) => 19 | push({ pathname: tabItems[tabIndex as number].name, search }); 20 | 21 | return ( 22 | 29 | {tabItems.map((item) => ( 30 | 31 | ))} 32 | 33 | ); 34 | }; 35 | 36 | export default AppTabs; 37 | -------------------------------------------------------------------------------- /tests/integration/api/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | from fastapi import FastAPI 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from eda_server.auth import check_permission 9 | from eda_server.db import models 10 | from eda_server.users import UserDatabase, current_active_user 11 | from tests.integration.utils.app import override_dependencies 12 | 13 | 14 | @pytest_asyncio.fixture 15 | async def admin_user(db: AsyncSession): 16 | user = await UserDatabase(db).create( 17 | { 18 | "email": "admin@example.com", 19 | "hashed_password": "", 20 | "is_superuser": True, 21 | } 22 | ) 23 | # NOTE(cutwater): Commit transaction implicitly opened by 24 | # UserDatabase as a side effect. Need better API for creating users. 25 | await db.commit() 26 | return user 27 | 28 | 29 | @pytest.fixture 30 | def app(app: FastAPI, admin_user: models.User): 31 | dependencies = { 32 | current_active_user: lambda: admin_user, 33 | } 34 | with override_dependencies(app, dependencies): 35 | yield app 36 | 37 | 38 | @pytest.fixture 39 | def check_permission_spy(): 40 | m = mock.AsyncMock(wraps=check_permission) 41 | with mock.patch("eda_server.auth.check_permission", m): 42 | yield m 43 | -------------------------------------------------------------------------------- /ui/src/app/API/Ruleset.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { getAxiosInstance } from '@app/API/baseApi'; 3 | import { AxiosResponse } from 'axios'; 4 | 5 | const rulesetsEndpoint = '/api/rulesets'; 6 | const rulesEndpoint = '/api/rulebook_json'; 7 | const sourcesEndpoint = '/api/rulebook_json'; 8 | 9 | export const listRulesets = (pagination = defaultSettings): Promise => { 10 | return getAxiosInstance().get(`${rulesetsEndpoint}`); 11 | }; 12 | 13 | export const fetchRuleset = (id: string | number): Promise => { 14 | return getAxiosInstance().get(`${rulesetsEndpoint}/${id}`); 15 | }; 16 | 17 | export const fetchRulesetRules = (id: string | number, pagination = defaultSettings): Promise => { 18 | return getAxiosInstance() 19 | .get(`${rulesEndpoint}/${id}`) 20 | .then((data) => 21 | data?.data && data.data.rulesets && data.data.rulesets.length > 0 ? data.data.rulesets[0].rules : [] 22 | ); 23 | }; 24 | 25 | export const fetchRulesetSources = (id: string | number, pagination = defaultSettings): Promise => { 26 | return getAxiosInstance() 27 | .get(`${sourcesEndpoint}/${id}`) 28 | .then((data) => 29 | data?.data && data.data.rulesets && data.data.rulesets.length > 0 ? data.data.rulesets[0].sources : [] 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/eda_server/api/ssh.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from fastapi import APIRouter 16 | 17 | from eda_server.key import generate_ssh_keys 18 | from eda_server.managers import secretsmanager 19 | 20 | router = APIRouter() 21 | 22 | 23 | @router.get("/api/ssh-public-key") 24 | async def ssh_public_key(): 25 | if secretsmanager.has_secret("ssh-public-key"): 26 | return {"public_key": secretsmanager.get_secret("ssh-public-key")} 27 | else: 28 | ssh_private_key, ssh_public_key = await generate_ssh_keys() 29 | secretsmanager.set_secret("ssh-public-key", ssh_public_key) 30 | secretsmanager.set_secret("ssh-private-key", ssh_private_key) 31 | return {"public_key": secretsmanager.get_secret("ssh-public-key")} 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ 'main' ] 6 | pull_request: 7 | branches: [ 'main' ] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | postgres: 15 | image: 'postgres:13' 16 | env: 17 | POSTGRES_PASSWORD: secret 18 | options: >- 19 | --health-cmd pg_isready 20 | --health-interval 10s 21 | --health-timeout 5s 22 | --health-retries 5 23 | ports: 24 | - '5432:5432' 25 | 26 | strategy: 27 | matrix: 28 | python-version: 29 | - "3.11" 30 | - "3.10" 31 | - "3.9" 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v4 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - name: Install package 43 | run: python -m pip install -e . 44 | 45 | - name: Install dependencies 46 | run: python -m pip install -r requirements_test.txt 47 | 48 | - name: Run tests 49 | run: pytest 50 | env: 51 | EDA_DATABASE_URL: 'postgresql+asyncpg://postgres:secret@localhost:5432/postgres' 52 | EDA_DEPLOYMENT_TYPE: local 53 | -------------------------------------------------------------------------------- /src/eda_server/api/task.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from fastapi import APIRouter, Depends 16 | 17 | from eda_server.auth import requires_permission 18 | from eda_server.managers import taskmanager 19 | from eda_server.types import Action, ResourceType 20 | 21 | router = APIRouter(prefix="/api/tasks", tags=["tasks"]) 22 | 23 | 24 | @router.get( 25 | "", 26 | operation_id="list_tasks", 27 | dependencies=[ 28 | Depends(requires_permission(ResourceType.TASK, Action.READ)) 29 | ], 30 | ) 31 | async def list_tasks(): 32 | tasks = [ 33 | { 34 | "name": task.get_name(), 35 | "done": task.done(), 36 | "cancelled": task.cancelled(), 37 | } 38 | for task in taskmanager.tasks 39 | ] 40 | return tasks 41 | -------------------------------------------------------------------------------- /ui/src/app/Projects/projects-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Checkbox } from '@patternfly/react-core'; 4 | import ProjectsTableContext from './projects-table-context'; 5 | import { Link } from 'react-router-dom'; 6 | import { ProjectType } from '@app/shared/types/common-types'; 7 | 8 | export const SelectBox = ({ id }) => { 9 | const { selectedProjects, setSelectedProjects } = useContext(ProjectsTableContext); 10 | 11 | return ( 12 | (setSelectedProjects ? setSelectedProjects(id) : '')} 16 | /> 17 | ); 18 | }; 19 | 20 | SelectBox.propTypes = { 21 | id: PropTypes.string.isRequired, 22 | }; 23 | 24 | export const createRows = (data: ProjectType[]) => 25 | data.map(({ id, name, status, type, revision }) => ({ 26 | id, 27 | cells: [ 28 | 29 | 30 | , 31 | 32 | 37 | {name} 38 | 39 | , 40 | status, 41 | type, 42 | revision || '000000', 43 | ], 44 | })); 45 | -------------------------------------------------------------------------------- /ui/src/test/Rules/Rules.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { Rules } from '@app/Rules/Rules'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('Rules', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the Rules component', async () => { 24 | mockApi.onGet(`/api/rules`).replyOnce(200, [ 25 | { id: '1', name: 'Rule 1' }, 26 | { id: '2', name: 'Rule 2' }, 27 | { id: '3', name: 'Rule 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | await act(async () => { 32 | wrapper = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | await act(async () => { 41 | wrapper.update(); 42 | }); 43 | expect(wrapper).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /ui/src/app/API/Project.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { AxiosResponse } from 'axios'; 3 | import { getAxiosInstance } from '@app/API/baseApi'; 4 | 5 | const projectsEndpoint = '/api/projects'; 6 | 7 | export interface NewProjectType { 8 | name: string; 9 | description?: string; 10 | scm_type?: string; 11 | scm_token?: string; 12 | url?: string; 13 | } 14 | 15 | export interface ProjectUpdateType { 16 | id: string; 17 | name?: string; 18 | description?: string; 19 | scm_type?: string; 20 | scm_token?: string; 21 | url?: string; 22 | } 23 | 24 | export const listProjects = (pagination = defaultSettings): Promise => 25 | getAxiosInstance().get(projectsEndpoint); 26 | 27 | export const fetchProject = (id: string | number | undefined): Promise => 28 | getAxiosInstance().get(`${projectsEndpoint}/${id}`); 29 | 30 | export const removeProject = (projectId: string | number): Promise => 31 | getAxiosInstance().delete(`${projectsEndpoint}/${projectId}`); 32 | 33 | export const updateProject = (project: ProjectUpdateType): Promise => 34 | getAxiosInstance().patch(`${projectsEndpoint}/${project.id}`, { 35 | name: project.name, 36 | description: project.description, 37 | }); 38 | 39 | export const addProject = (projectData: NewProjectType): Promise => 40 | getAxiosInstance().post(projectsEndpoint, projectData); 41 | -------------------------------------------------------------------------------- /ui/src/app/Activations/activations-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import ActivationsTableContext from '@app/Activations/activations-table-context'; 4 | import { Checkbox } from '@patternfly/react-core'; 5 | import PropTypes from 'prop-types'; 6 | import { ActivationType } from '@app/Activations/Activations'; 7 | 8 | export const SelectBox = ({ id }) => { 9 | const { selectedActivations, setSelectedActivations } = useContext(ActivationsTableContext); 10 | 11 | return ( 12 | (setSelectedActivations ? setSelectedActivations(id) : '')} 16 | /> 17 | ); 18 | }; 19 | 20 | SelectBox.propTypes = { 21 | id: PropTypes.string.isRequired, 22 | }; 23 | 24 | export const createRows = (data: ActivationType[]) => 25 | data.map(({ id, name, status, number_of_rules, fire_count }) => ({ 26 | id, 27 | cells: [ 28 | 29 | 30 | , 31 | 32 | 37 | {name} 38 | 39 | , 40 | status, 41 | number_of_rules, 42 | fire_count, 43 | ], 44 | })); 45 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202211171209_202cf90fc4b0_add_role_is_default.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Add is_default column to roles table. 16 | 17 | Revision ID: 202cf90fc4b0 18 | Revises: f592f6ef1e3a 19 | Create Date: 2022-11-17 12:09:13.258802+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "202cf90fc4b0" 27 | down_revision = "f592f6ef1e3a" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column( 34 | "role", 35 | sa.Column( 36 | "is_default", 37 | sa.Boolean(), 38 | server_default=sa.false(), 39 | nullable=True, 40 | ), 41 | ) 42 | 43 | 44 | def downgrade() -> None: 45 | op.drop_column("role", "is_default") 46 | -------------------------------------------------------------------------------- /src/eda_server/db/sql/playbook/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sqlalchemy as sa 16 | from sqlalchemy.ext.asyncio import AsyncSession 17 | 18 | from eda_server.db import models 19 | 20 | 21 | async def import_playbook_file( 22 | db: AsyncSession, filename: str, file_content: str, project_id: int 23 | ): 24 | await db.execute( 25 | sa.insert(models.playbooks).values( 26 | name=filename, 27 | playbook=file_content, 28 | project_id=project_id, 29 | ) 30 | ) 31 | 32 | 33 | async def update_playbook( 34 | db: AsyncSession, file_content: str, playbook_id: int 35 | ): 36 | await db.execute( 37 | sa.update(models.playbooks) 38 | .where(models.playbooks.c.id == playbook_id) 39 | .values( 40 | playbook=file_content, 41 | ) 42 | ) 43 | -------------------------------------------------------------------------------- /ui/src/app/AuditView/audit-rules-table-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Text, TextVariants } from '@patternfly/react-core'; 4 | 5 | export const createRows = (data) => 6 | data.map(({ rule, job, status, ruleset, fired_date }) => ({ 7 | rule, 8 | cells: [ 9 | 10 | 15 | {rule?.name || rule?.id} 16 | 17 | , 18 | 19 | 24 | {job?.name || job?.id} 25 | 26 | , 27 | status, 28 | 29 | 34 | {ruleset?.name || ruleset?.id} 35 | 36 | , 37 | 38 | 39 | {new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeStyle: 'long' }).format( 40 | new Date(fired_date || 0) 41 | )} 42 | 43 | , 44 | ], 45 | })); 46 | -------------------------------------------------------------------------------- /src/eda_server/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import argparse 16 | 17 | import uvicorn 18 | 19 | from .config import default_log_config, load_settings 20 | 21 | 22 | def parse_args() -> argparse.Namespace: 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument( 25 | "-r", 26 | "--reload", 27 | action="store_true", 28 | help="Enable Auto-reload", 29 | default=False, 30 | ) 31 | return parser.parse_args() 32 | 33 | 34 | def main(): 35 | settings = load_settings() 36 | args = parse_args() 37 | uvicorn.run( 38 | "eda_server.app:create_app", 39 | reload=args.reload, 40 | factory=True, 41 | host=settings.host, 42 | port=settings.port, 43 | log_config=default_log_config(), 44 | log_level=settings.log_level.lower(), 45 | ) 46 | -------------------------------------------------------------------------------- /src/eda_server/db/session.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import contextlib 16 | 17 | from sqlalchemy.ext.asyncio import ( 18 | AsyncEngine, 19 | AsyncSession, 20 | create_async_engine, 21 | ) 22 | from sqlalchemy.orm import sessionmaker 23 | 24 | from eda_server.config import Settings 25 | 26 | 27 | def engine_from_config(config: Settings, **kwargs) -> AsyncEngine: 28 | return create_async_engine(url=config.database_url, **kwargs) 29 | 30 | 31 | def create_session_factory(engine: AsyncEngine) -> sessionmaker: 32 | return sessionmaker( 33 | bind=engine, class_=AsyncSession, expire_on_commit=False 34 | ) 35 | 36 | 37 | @contextlib.asynccontextmanager 38 | async def dispose_context(engine: AsyncEngine): 39 | """Dispose engine at exit.""" 40 | try: 41 | yield engine 42 | finally: 43 | await engine.dispose() 44 | -------------------------------------------------------------------------------- /src/eda_server/db/sql/extra_var/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sqlalchemy as sa 16 | from sqlalchemy.ext.asyncio import AsyncSession 17 | 18 | from eda_server.db import models 19 | 20 | 21 | async def import_extra_var_file( 22 | db: AsyncSession, filename: str, file_content: str, project_id: int 23 | ): 24 | await db.execute( 25 | sa.insert(models.extra_vars).values( 26 | name=filename, 27 | extra_var=file_content, 28 | project_id=project_id, 29 | ) 30 | ) 31 | 32 | 33 | async def update_extra_var( 34 | db: AsyncSession, file_content: str, extra_var_id: int 35 | ): 36 | await db.execute( 37 | sa.update(models.extra_vars) 38 | .where(models.extra_vars.c.id == extra_var_id) 39 | .values( 40 | extra_var=file_content, 41 | ) 42 | ) 43 | -------------------------------------------------------------------------------- /ui/src/test/RuleSets/RuleSets.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { RuleSets } from '@app/RuleSets/RuleSets'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('RuleSets', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the RuleSets component', async () => { 24 | mockApi.onGet(`/api/rulesets`).replyOnce(200, [ 25 | { id: '1', name: 'RuleSet 1' }, 26 | { id: '2', name: 'RuleSet 2' }, 27 | { id: '3', name: 'RuleSet 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | await act(async () => { 32 | wrapper = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | await act(async () => { 41 | wrapper.update(); 42 | }); 43 | expect(wrapper).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/eda_server/schema/inventory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime 16 | from typing import Optional 17 | 18 | from pydantic import BaseModel, StrictStr 19 | 20 | from eda_server.types import InventorySource 21 | 22 | 23 | class InventoryCreate(BaseModel): 24 | name: StrictStr 25 | description: StrictStr = "" 26 | inventory: Optional[StrictStr] = "" 27 | inventory_source: InventorySource = InventorySource.USER_DEFINED 28 | 29 | 30 | class InventoryRead(InventoryCreate): 31 | id: int 32 | project_id: Optional[int] 33 | created_at: datetime 34 | modified_at: datetime 35 | 36 | 37 | class InventoryUpdate(BaseModel): 38 | name: StrictStr 39 | description: StrictStr = "" 40 | inventory: Optional[StrictStr] = "" 41 | 42 | 43 | class InventoryRef(BaseModel): 44 | name: StrictStr 45 | id: Optional[int] 46 | -------------------------------------------------------------------------------- /ui/src/test/AuditView/audit-rules.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { AuditHosts } from '@app/AuditView/audit-hosts'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('AuditHosts', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the AuditView component', async () => { 24 | mockApi.onGet(`/api/audit/hosts_changed`).replyOnce(200, [ 25 | { id: '1', name: 'Host 1' }, 26 | { id: '2', name: 'Host 2' }, 27 | { id: '3', name: 'Host 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | await act(async () => { 32 | wrapper = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | await act(async () => { 41 | wrapper.update(); 42 | }); 43 | expect(wrapper).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /ui/src/test/RuleBooks/RuleBooks.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { RuleBooks } from '@app/RuleBooks/RuleBooks'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('RuleBooks', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the RuleBooks component', async () => { 24 | mockApi.onGet(`/api/rulebooks`).replyOnce(200, [ 25 | { id: '1', name: 'RuleBook 1' }, 26 | { id: '2', name: 'RuleBook 2' }, 27 | { id: '3', name: 'RuleBook 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | await act(async () => { 32 | wrapper = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | await act(async () => { 41 | wrapper.update(); 42 | }); 43 | expect(wrapper).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /ui/src/test/Inventories/Inventories.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { Inventories } from '@app/Inventories/Inventories'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('Inventories', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the Inventories component', async () => { 24 | mockApi.onGet(`/api/inventories`).replyOnce(200, [ 25 | { id: '1', name: 'Inventory 1' }, 26 | { id: '2', name: 'Inventory 2' }, 27 | { id: '3', name: 'Inventory 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | await act(async () => { 32 | wrapper = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | await act(async () => { 41 | wrapper.update(); 42 | }); 43 | expect(wrapper).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /ui/src/test/Activations/Activations.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { Activations } from '@app/Activations/Activations'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('Activations', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the Activations component', async () => { 24 | mockApi.onGet(`/api/activation_instances`).replyOnce(200, [ 25 | { id: '1', name: 'Activation 1' }, 26 | { id: '2', name: 'Activation 2' }, 27 | { id: '3', name: 'Activation 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | await act(async () => { 32 | wrapper = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | await act(async () => { 41 | wrapper.update(); 42 | }); 43 | expect(wrapper).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /ui/src/test/AuditView/audit-hosts.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { AuditHosts } from '@app/AuditView/audit-hosts'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | import {AuditRules} from "@app/AuditView/audit-rules"; 10 | 11 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | 19 | describe('AuditHosts', () => { 20 | beforeAll(() => { 21 | mockApi.reset(); 22 | }); 23 | 24 | it('should render the AuditView component', async () => { 25 | mockApi.onGet(`/api/audit/rules_fired`).replyOnce(200, [ 26 | { id: '1', name: 'Rule 1' }, 27 | { id: '2', name: 'Rule 2' }, 28 | { id: '3', name: 'Rule3 3' }, 29 | ]); 30 | 31 | let wrapper; 32 | await act(async () => { 33 | wrapper = mount( 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }); 41 | await act(async () => { 42 | wrapper.update(); 43 | }); 44 | expect(wrapper).toMatchSnapshot(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/eda_server/db/utils/common.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import enum 16 | from typing import List, Type 17 | 18 | 19 | def enum_names_lower(enum_cls: Type[enum.Enum]) -> List[str]: 20 | """ 21 | Return enum names in lower case. 22 | 23 | This function is to be used as ``values_callable`` argument 24 | in ``sqlalchemy.Enum`` model field types. 25 | """ 26 | return [e.name.lower() for e in enum_cls] 27 | 28 | 29 | def enum_values(enum_cls: Type[enum.Enum]) -> List[str]: 30 | """ 31 | Return enum values. 32 | 33 | This function is to be used as ``values_callable`` argument 34 | in ``sqlalchemy.Enum`` model field types. 35 | 36 | :raises ValueError: If enum value is not a string. 37 | """ 38 | values = [] 39 | for e in enum_cls: 40 | if not isinstance(e.value, str): 41 | raise TypeError("Enum values must be strings.") 42 | values.append(e.value) 43 | return values 44 | -------------------------------------------------------------------------------- /src/eda_server/api/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from fastapi import APIRouter 16 | 17 | from eda_server import schema 18 | from eda_server.users import bearer_backend, cookie_backend, fastapi_users 19 | 20 | router = APIRouter() 21 | router.include_router( 22 | fastapi_users.get_auth_router(cookie_backend), 23 | prefix="/api/auth/jwt", 24 | tags=["auth"], 25 | ) 26 | router.include_router( 27 | fastapi_users.get_auth_router(bearer_backend), 28 | prefix="/api/auth/bearer", 29 | tags=["auth"], 30 | ) 31 | router.include_router( 32 | fastapi_users.get_register_router(schema.UserRead, schema.UserCreate), 33 | prefix="/api/auth", 34 | tags=["auth"], 35 | ) 36 | router.include_router( 37 | fastapi_users.get_reset_password_router(), 38 | prefix="/api/auth", 39 | tags=["auth"], 40 | ) 41 | router.include_router( 42 | fastapi_users.get_verify_router(schema.UserRead), 43 | prefix="/api/auth", 44 | tags=["auth"], 45 | ) 46 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202210040316_ce5cc33ecdb6_add_sources_to_rulesets.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Add sources to rulesets. 16 | 17 | Revision ID: ce5cc33ecdb6 18 | Revises: 25bcfbe12475 19 | Create Date: 2022-10-04 03:16:38.154189+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | from sqlalchemy.dialects import postgresql 25 | 26 | # revision identifiers, used by Alembic. 27 | revision = "ce5cc33ecdb6" 28 | down_revision = "a6c32c91ee3f" 29 | branch_labels = None 30 | depends_on = None 31 | 32 | 33 | def upgrade() -> None: 34 | op.add_column( 35 | "ruleset", 36 | sa.Column( 37 | "sources", 38 | postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), 39 | nullable=True, 40 | comment="Expanded source information from ruleset data.", 41 | ), 42 | ) 43 | 44 | 45 | def downgrade() -> None: 46 | op.drop_column("ruleset", "sources") 47 | -------------------------------------------------------------------------------- /src/eda_server/types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from enum import Enum 16 | 17 | 18 | class ResourceType(Enum): 19 | ACTIVATION = "activation" 20 | ACTIVATION_INSTANCE = "activation_instance" 21 | AUDIT_RULE = "audit_rule" 22 | JOB = "job" 23 | TASK = "task" 24 | USER = "user" 25 | PROJECT = "project" 26 | INVENTORY = "inventory" 27 | EXTRA_VAR = "extra_var" 28 | PLAYBOOK = "playbook" 29 | RULEBOOK = "rulebook" 30 | EXECUTION_ENV = "execution_env" 31 | ROLE = "role" 32 | 33 | def __str__(self): 34 | return self.value 35 | 36 | 37 | class Action(Enum): 38 | CREATE = "create" 39 | READ = "read" 40 | UPDATE = "update" 41 | DELETE = "delete" 42 | 43 | def __str__(self): 44 | return self.value 45 | 46 | 47 | class InventorySource(Enum): 48 | PROJECT = "project" 49 | COLLECTION = "collection" 50 | USER_DEFINED = "user_defined" 51 | EXECUTION_ENV = "execution_env" 52 | -------------------------------------------------------------------------------- /src/eda_server/key.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | import logging 17 | import os 18 | import shutil 19 | import tempfile 20 | 21 | ssh_keygen = shutil.which("ssh-keygen") 22 | logger = logging.getLogger() 23 | 24 | 25 | async def generate_ssh_keys(): 26 | 27 | with tempfile.TemporaryDirectory() as local_working_directory: 28 | cmd = ["-f", "key", "-P", "", "-C", "eda-server"] 29 | logger.debug(ssh_keygen) 30 | logger.debug(cmd) 31 | 32 | proc = await asyncio.create_subprocess_exec( 33 | ssh_keygen, 34 | *cmd, 35 | cwd=local_working_directory, 36 | ) 37 | 38 | await proc.wait() 39 | 40 | with open(os.path.join(local_working_directory, "key")) as f: 41 | ssh_private_key = f.read() 42 | 43 | with open(os.path.join(local_working_directory, "key.pub")) as f: 44 | ssh_public_key = f.read() 45 | return ssh_private_key, ssh_public_key 46 | -------------------------------------------------------------------------------- /ui/src/test/Rule/rule.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { Rule } from '@app/Rule/Rule'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { Tab } from '@patternfly/react-core'; 9 | import { mockApi } from '../__mocks__/baseApi'; 10 | 11 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | 19 | describe('RuleSet', () => { 20 | beforeAll(() => { 21 | mockApi.reset(); 22 | }); 23 | 24 | it('should render the RuleSet component tabs', async () => { 25 | mockApi.onGet(`/api/rules/1`).replyOnce(200, { 26 | name: 'Ruleset 1', 27 | id: '1', 28 | }); 29 | let wrapper; 30 | await act(async () => { 31 | wrapper = mount( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }); 39 | await act(async () => { 40 | wrapper.update(); 41 | }); 42 | expect(wrapper.find(Tab).length).toEqual(2); 43 | expect(wrapper.find(Tab).at(0).props().title.props.children).toContain('Back to Rules'); 44 | expect(wrapper.find(Tab).at(1).props().title).toEqual('Details'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209131944_34bca36e407d_change_job_instance_event_table_to_add_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Change job instance event table to add type and timestamp. 16 | 17 | Revision ID: 34bca36e407d 18 | Revises: 6dab32136502 19 | Create Date: 2022-09-13 19:44:30.295067+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "34bca36e407d" 27 | down_revision = "6dab32136502" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column( 34 | "job_instance_event", sa.Column("type", sa.String(), nullable=True) 35 | ) 36 | op.add_column( 37 | "job_instance_event", 38 | sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), 39 | ) 40 | 41 | 42 | def downgrade() -> None: 43 | op.drop_column("job_instance_event", "created_at") 44 | op.drop_column("job_instance_event", "type") 45 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209291853_25bcfbe12475_project_name_not_null.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """project_name_not_null. 16 | 17 | Revision ID: 25bcfbe12475 18 | Revises: bc14081ad342 19 | Create Date: 2022-09-29 18:53:34.799615+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "25bcfbe12475" 27 | down_revision = "9cdc437024a1" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.alter_column( 34 | "project", "name", existing_type=sa.VARCHAR(), nullable=False 35 | ) 36 | 37 | op.create_check_constraint( 38 | "ck_project_name_not_empty", 39 | "project", 40 | sa.sql.column("name") != "", 41 | ) 42 | 43 | 44 | def downgrade() -> None: 45 | op.alter_column( 46 | "project", "name", existing_type=sa.VARCHAR(), nullable=True 47 | ) 48 | 49 | op.drop_constraint("ck_project_name_not_empty") 50 | -------------------------------------------------------------------------------- /ui/src/app/shared/table-empty-state.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | EmptyStateIcon, 5 | EmptyStateBody, 6 | EmptyStateSecondaryActions, 7 | Text, 8 | TextContent, 9 | TextVariants, 10 | EmptyState, 11 | } from '@patternfly/react-core'; 12 | import { EmptyTable } from '@redhat-cloud-services/frontend-components/EmptyTable'; 13 | 14 | export interface TableEmptyState { 15 | title: ReactNode; 16 | Icon: React.ComponentType; 17 | description: ReactNode; 18 | PrimaryAction?: React.ElementType; 19 | renderDescription?: () => ReactNode; 20 | } 21 | 22 | const TableEmptyState: React.ComponentType = ({ 23 | title, 24 | Icon, 25 | description, 26 | PrimaryAction, 27 | renderDescription, 28 | }) => ( 29 | 30 | 31 | 32 | 33 | {title} 34 | 35 | 36 | {description} 37 | {renderDescription && renderDescription()} 38 | 39 | {PrimaryAction && } 40 | 41 | 42 | ); 43 | 44 | TableEmptyState.propTypes = { 45 | title: PropTypes.string.isRequired, 46 | Icon: PropTypes.any.isRequired, 47 | description: PropTypes.string.isRequired, 48 | PrimaryAction: PropTypes.any, 49 | renderDescription: PropTypes.func, 50 | }; 51 | 52 | export default TableEmptyState; 53 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202208242257_61c61bfd1f7b_add_ee_and_wd_to_activation_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Add execution environment and working directory to activation instance. 16 | 17 | Revision ID: 61c61bfd1f7b 18 | Revises: d3abe0785cfd 19 | Create Date: 2022-08-24 22:57:20.017549+00:00 20 | """ 21 | import sqlalchemy as sa 22 | from alembic import op 23 | 24 | # revision identifiers, used by Alembic. 25 | revision = "61c61bfd1f7b" 26 | down_revision = "c1eee0e47fc1" 27 | branch_labels = None 28 | depends_on = None 29 | 30 | 31 | def upgrade() -> None: 32 | op.add_column( 33 | "activation_instance", 34 | sa.Column("execution_environment", sa.String(), nullable=True), 35 | ) 36 | op.add_column( 37 | "activation_instance", 38 | sa.Column("working_directory", sa.String(), nullable=True), 39 | ) 40 | 41 | 42 | def downgrade() -> None: 43 | op.drop_column("activation_instance", "execution_environment") 44 | op.drop_column("activation_instance", "working_directory") 45 | -------------------------------------------------------------------------------- /src/eda_server/db/sql/inventory/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | 17 | import sqlalchemy as sa 18 | from sqlalchemy.ext.asyncio import AsyncSession 19 | 20 | from eda_server.db import models 21 | from eda_server.types import InventorySource 22 | 23 | logger = logging.getLogger("eda_server") 24 | 25 | 26 | async def import_inventory_file( 27 | db: AsyncSession, filename: str, file_content: str, project_id: int 28 | ): 29 | await db.execute( 30 | sa.insert(models.inventories).values( 31 | name=filename, 32 | inventory=file_content, 33 | project_id=project_id, 34 | inventory_source=InventorySource.PROJECT, 35 | ) 36 | ) 37 | 38 | 39 | async def update_inventory( 40 | db: AsyncSession, file_content: str, inventory_id: int 41 | ): 42 | await db.execute( 43 | sa.update(models.inventories) 44 | .where(models.inventories.c.id == inventory_id) 45 | .values( 46 | inventory=file_content, 47 | ) 48 | ) 49 | -------------------------------------------------------------------------------- /tests/integration/utils/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import contextlib 16 | from typing import Any, Callable, Dict 17 | 18 | from fastapi import FastAPI 19 | 20 | from eda_server.app import setup_cors, setup_routes 21 | from eda_server.db.dependency import get_db_session_factory 22 | 23 | 24 | async def create_test_app(settings, session): 25 | app = FastAPI(title="Ansible Events API") 26 | app.state.settings = settings 27 | 28 | setup_cors(app) 29 | setup_routes(app) 30 | 31 | app.dependency_overrides[get_db_session_factory] = lambda: lambda: session 32 | 33 | return app 34 | 35 | 36 | @contextlib.contextmanager 37 | def override_dependencies( 38 | app: FastAPI, 39 | dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]], 40 | ): 41 | existing_dependencies = app.dependency_overrides 42 | app.dependency_overrides = { 43 | **existing_dependencies, 44 | **dependency_overrides, 45 | } 46 | try: 47 | yield 48 | finally: 49 | app.dependency_overrides = existing_dependencies 50 | -------------------------------------------------------------------------------- /ui/src/test/NewInventory/NewInventory.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount, render, shallow } from 'enzyme'; 3 | import { MemoryRouter } from 'react-router'; 4 | import { act } from 'react-dom/test-utils'; 5 | import { IntlProvider } from 'react-intl'; 6 | import { Route } from 'react-router-dom'; 7 | import { Button, TextInput } from '@patternfly/react-core'; 8 | import store from '../../store'; 9 | import { Provider } from 'react-redux'; 10 | import { NewInventory } from '@app/NewInventory/NewInventory'; 11 | import { mockApi } from '../__mocks__/baseApi'; 12 | 13 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | ); 22 | 23 | describe('NewInventory', () => { 24 | beforeAll(() => { 25 | mockApi.reset(); 26 | window.HTMLCanvasElement.prototype.getContext = () => ({} as any); 27 | }); 28 | 29 | it('should render the NewInventory form', async () => { 30 | mockApi.onPost(`/api/inventories`).replyOnce(200, { 31 | name: 'Inventory 1', 32 | id: 1, 33 | inventory: 'inventory', 34 | }); 35 | 36 | let wrapper; 37 | await act(async () => { 38 | wrapper = render( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }); 46 | 47 | expect(wrapper).toMatchSnapshot(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /ui/src/app/shared/top-toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Level, 5 | LevelItem, 6 | PageSection, 7 | PageSectionVariants, 8 | Text, 9 | TextContent, 10 | TextVariants, 11 | Title, 12 | } from '@patternfly/react-core'; 13 | import Breadcrumbs from './breadcrumbs'; 14 | 15 | export const TopToolbar = ({ children, breadcrumbs }) => ( 16 | 17 | {breadcrumbs && ( 18 | 19 | 20 | 21 | 22 | 23 | )} 24 | {children} 25 | 26 | ); 27 | 28 | TopToolbar.propTypes = { 29 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, 30 | breadcrumbs: PropTypes.array, 31 | paddingBottom: PropTypes.bool, 32 | className: PropTypes.string, 33 | }; 34 | 35 | TopToolbar.defaultProps = { 36 | paddingBottom: false, 37 | }; 38 | 39 | export const TopToolbarTitle = ({ title, description, children }) => ( 40 | 41 | 42 | 43 | {`${title}`} 44 | {description && ( 45 | 46 | {description} 47 | 48 | )} 49 | 50 | {children} 51 | 52 | 53 | ); 54 | 55 | TopToolbarTitle.propTypes = { 56 | title: PropTypes.string, 57 | description: PropTypes.string, 58 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), 59 | }; 60 | -------------------------------------------------------------------------------- /src/eda_server/schema/message.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import uuid 16 | from datetime import datetime 17 | 18 | from pydantic import BaseModel, StrictStr, confloat, validator 19 | 20 | 21 | class ProducerMessage(BaseModel): 22 | name: StrictStr 23 | message_id: StrictStr = "" 24 | timestamp: StrictStr = "" 25 | lat: confloat(gt=-90, lt=90) 26 | lon: confloat(gt=-180, lt=180) 27 | 28 | @validator("message_id", pre=True, always=True) 29 | def set_id_from_name_uuid(cls, v, values): 30 | if "name" in values: 31 | return f"{values['name']}_{uuid.uuid4()}" 32 | else: 33 | raise ValueError("name not set") 34 | 35 | @validator("timestamp", pre=True, always=True) 36 | def set_datetime_utcnow(cls, v): 37 | return str(datetime.utcnow()) 38 | 39 | 40 | class ProducerResponse(BaseModel): 41 | name: StrictStr 42 | message_id: StrictStr 43 | topic: StrictStr 44 | timestamp: StrictStr = "" 45 | 46 | @validator("timestamp", pre=True, always=True) 47 | def set_datetime_utcnow(cls, v): 48 | return str(datetime.utcnow()) 49 | -------------------------------------------------------------------------------- /ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // tells eslint to use the TypeScript parser 3 | "parser": "@typescript-eslint/parser", 4 | // tell the TypeScript parser that we want to use JSX syntax 5 | "parserOptions": { 6 | "tsx": true, 7 | "jsx": true, 8 | "js": true, 9 | "useJSXTextNode": true, 10 | "project": "./tsconfig.json", 11 | "tsconfigRootDir": ".", 12 | "ecmaVersion": 7, 13 | "sourceType": "module" 14 | }, 15 | // we want to use the recommended rules provided from the typescript plugin 16 | "extends": [ 17 | "plugin:prettier/recommended", 18 | "eslint:recommended", 19 | "plugin:react/recommended", 20 | "plugin:@typescript-eslint/eslint-recommended", 21 | "plugin:@typescript-eslint/recommended" 22 | ], 23 | "plugins": [ 24 | "formatjs", 25 | "@typescript-eslint", 26 | "react-hooks", 27 | "eslint-plugin-react-hooks" 28 | ], 29 | "globals": { 30 | "window": "readonly", 31 | "describe": "readonly", 32 | "test": "readonly", 33 | "expect": "readonly", 34 | "it": "readonly", 35 | "process": "readonly", 36 | "document": "readonly" 37 | }, 38 | "settings": { 39 | "react": { 40 | "version": "^16.11.0" 41 | } 42 | }, 43 | "rules": { 44 | "@typescript-eslint/explicit-function-return-type": "off", 45 | "react-hooks/rules-of-hooks": "error", 46 | "react-hooks/exhaustive-deps": "warn", 47 | "@typescript-eslint/interface-name-prefix": "off", 48 | "@typescript-eslint/no-empty-function": "warn", 49 | "prettier/prettier": "warn", 50 | "import/no-unresolved": "off", 51 | "import/extensions": "off", 52 | "react/prop-types": "off" 53 | }, 54 | "env": { 55 | "browser": true, 56 | "node": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/test/RuleSet/ruleset.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { RuleSet } from '@app/RuleSet/ruleset'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { Tab } from '@patternfly/react-core'; 9 | import { mockApi } from '../__mocks__/baseApi'; 10 | 11 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | 19 | describe('RuleSet', () => { 20 | beforeAll(() => { 21 | mockApi.reset(); 22 | }); 23 | 24 | it('should render the RuleSet component tabs', async () => { 25 | mockApi.onGet(`/api/rulesets/1`).replyOnce(200, { 26 | name: 'RuleSet 1', 27 | id: 1, 28 | }); 29 | let wrapper; 30 | await act(async () => { 31 | wrapper = mount( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }); 39 | await act(async () => { 40 | wrapper.update(); 41 | }); 42 | expect(wrapper.find(Tab).length).toEqual(4); 43 | expect(wrapper.find(Tab).at(0).props().title.props.children).toContain('Back to Rule Sets'); 44 | expect(wrapper.find(Tab).at(1).props().title).toEqual('Details'); 45 | expect(wrapper.find(Tab).at(2).props().title).toEqual('Rules'); 46 | expect(wrapper.find(Tab).at(3).props().title).toEqual('Sources'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202210041309_12c3a0edc032_activation_project.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Adds project to activation. 16 | 17 | Revision ID: 12c3a0edc032 18 | Revises: 454d8d247eec 19 | Create Date: 2022-10-04 13:09:50.026149+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "12c3a0edc032" 27 | down_revision = "454d8d247eec" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column( 34 | "activation_instance", 35 | sa.Column("project_id", sa.Integer(), nullable=True), 36 | ) 37 | op.create_foreign_key( 38 | op.f("fk_activation_instance_project_id"), 39 | "activation_instance", 40 | "project", 41 | ["project_id"], 42 | ["id"], 43 | ondelete="CASCADE", 44 | ) 45 | 46 | 47 | def downgrade() -> None: 48 | op.drop_constraint( 49 | op.f("fk_activation_instance_project_id"), 50 | "activation_instance", 51 | type_="foreignkey", 52 | ) 53 | op.drop_column("activation_instance", "project_id") 54 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209262002_bc14081ad342_add_ruleset_created_at_modified_at.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Add ruleset created_at, modified_at. 16 | 17 | Revision ID: bc14081ad342 18 | Revises: f282d526b775 19 | Create Date: 2022-09-21 20:02:54.926616+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "bc14081ad342" 27 | down_revision = "f282d526b775" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column( 34 | "ruleset", 35 | sa.Column( 36 | "created_at", 37 | sa.DateTime(timezone=True), 38 | server_default=sa.text("now()"), 39 | nullable=False, 40 | ), 41 | ) 42 | op.add_column( 43 | "ruleset", 44 | sa.Column( 45 | "modified_at", 46 | sa.DateTime(timezone=True), 47 | server_default=sa.text("now()"), 48 | nullable=False, 49 | ), 50 | ) 51 | 52 | 53 | def downgrade() -> None: 54 | op.drop_column("ruleset", "modified_at") 55 | op.drop_column("ruleset", "created_at") 56 | -------------------------------------------------------------------------------- /src/eda_server/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from importlib import resources 16 | 17 | import yaml 18 | from fastapi.requests import Request 19 | from pydantic import BaseSettings 20 | 21 | 22 | class Settings(BaseSettings): 23 | """Application settings.""" 24 | 25 | secret: str = "secret" 26 | 27 | log_level: str = "info" 28 | 29 | host: str = "127.0.0.1" 30 | port: int = 9000 31 | 32 | database_url: str = ( 33 | "postgresql+asyncpg://postgres:secret@localhost:5432/eda_server" 34 | ) 35 | 36 | deployment_type: str = "docker" 37 | server_name: str = "localhost" 38 | 39 | class Config: 40 | env_prefix = "EDA_" 41 | env_nested_delimiter = "__" 42 | 43 | 44 | def load_settings() -> Settings: 45 | """Load and validate application settings.""" 46 | return Settings() 47 | 48 | 49 | def default_log_config(): 50 | with resources.open_text(__name__, "logging.yaml") as fp: 51 | return yaml.safe_load(fp) 52 | 53 | 54 | # TODO(cutwater): Drop in favor of dependency_overrides 55 | def get_settings(request: Request) -> Settings: 56 | """Application settings dependency.""" 57 | return request.app.state.settings 58 | -------------------------------------------------------------------------------- /src/eda_server/schema/project.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime 16 | from typing import List, Optional 17 | 18 | from pydantic import BaseModel, StrictStr, constr 19 | 20 | from .extra_vars import ExtraVarsRef 21 | from .inventory import InventoryRef 22 | from .playbook import PlaybookRef 23 | from .rulebook import RuleRulesetRef 24 | 25 | 26 | def strict_not_empty_str(): 27 | return constr(strict=True, strip_whitespace=True, min_length=1) 28 | 29 | 30 | class ProjectCreate(BaseModel): 31 | url: StrictStr 32 | name: strict_not_empty_str() 33 | description: Optional[StrictStr] = "" 34 | 35 | 36 | class ProjectRead(ProjectCreate): 37 | id: int 38 | git_hash: Optional[StrictStr] 39 | created_at: datetime 40 | modified_at: datetime 41 | 42 | 43 | class ProjectDetail(ProjectRead): 44 | rulesets: List[RuleRulesetRef] 45 | inventories: List[InventoryRef] 46 | vars: List[ExtraVarsRef] 47 | playbooks: List[PlaybookRef] 48 | 49 | 50 | class ProjectList(BaseModel): 51 | id: int 52 | url: StrictStr 53 | name: StrictStr 54 | 55 | 56 | class ProjectUpdate(BaseModel): 57 | name: Optional[strict_not_empty_str()] 58 | description: Optional[StrictStr] 59 | -------------------------------------------------------------------------------- /ui/src/app/shared/types/common-types.d.ts: -------------------------------------------------------------------------------- 1 | import { SortByDirection } from '@patternfly/react-table'; 2 | 3 | export interface StringObject { 4 | [key: string]: string; 5 | } 6 | 7 | export interface AnyObject { 8 | [key: string]: any; 9 | } 10 | 11 | export interface ApiMetadata extends AnyObject { 12 | count?: number; 13 | limit?: number; 14 | offset?: number; 15 | } 16 | 17 | export interface ApiCollectionResponse { 18 | data: T[]; 19 | meta: ApiMetadata; 20 | } 21 | 22 | export interface ActionNotification { 23 | fulfilled?: AnyObject; 24 | pending?: AnyObject; 25 | rejected?: AnyObject; 26 | } 27 | 28 | export type NotificationPayload = 29 | | { 30 | type: string; 31 | payload: { 32 | dismissable: boolean; 33 | variant: string; 34 | title: string; 35 | description: string; 36 | }; 37 | } 38 | | { 39 | type: string; 40 | payload: any; 41 | }; 42 | 43 | export interface SortBy { 44 | index: number; 45 | property: string; 46 | direction: SortByDirection; 47 | } 48 | 49 | export type User = { 50 | first_name?: string; 51 | last_name?: string; 52 | email: string; 53 | }; 54 | 55 | export interface ProjectType { 56 | id: string; 57 | name?: string; 58 | description?: string; 59 | scm_type?: string; 60 | scm_token?: string; 61 | created_at?: string; 62 | modified_at?: string; 63 | url?: string; 64 | status?: string; 65 | type?: string; 66 | revision?: string; 67 | vars?: [{ id: string; name: string }]; 68 | rulesets?: [{ id: string; name: string }]; 69 | inventories?: [{ id: string; name: string }]; 70 | playbooks?: [{ id: string; name: string }]; 71 | } 72 | 73 | export interface TabItemType { 74 | eventKey: number; 75 | title: string | JSXElement; 76 | name: string; 77 | } 78 | -------------------------------------------------------------------------------- /ui/src/test/InventoriesSelect/InventoriesSelect.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { InventoriesSelect } from '@app/InventoriesSelect/InventoriesSelect'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | 10 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | describe('InventoriesSelect', () => { 19 | beforeAll(() => { 20 | mockApi.reset(); 21 | }); 22 | 23 | it('should render the InventoriesSelect component', async () => { 24 | mockApi.onGet(`/api/InventoriesSelect`).replyOnce(200, [ 25 | { id: '1', name: 'Inventory 1' }, 26 | { id: '2', name: 'Inventory 2' }, 27 | { id: '3', name: 'Inventory 3' }, 28 | ]); 29 | 30 | let wrapper; 31 | const inventory = undefined; 32 | const setInventory = (inventory) => undefined; 33 | await act(async () => { 34 | wrapper = mount( 35 | 36 | 37 | true} 42 | /> 43 | 44 | 45 | ); 46 | }); 47 | await act(async () => { 48 | wrapper.update(); 49 | }); 50 | expect(wrapper).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /ui/src/app/AppLayout/about-modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | AboutModal, 4 | TextContent, 5 | TextList, 6 | TextListItem, 7 | TextListItemVariants, 8 | TextListVariants, 9 | } from '@patternfly/react-core'; 10 | import Logo from '../../assets/images/logo-masthead.svg'; 11 | import { detect } from 'detect-browser'; 12 | import { User } from '@app/shared/types/common-types'; 13 | 14 | interface IProps { 15 | isOpen: boolean; 16 | user?: User; 17 | trademark: string; 18 | brandImageSrc: string; 19 | onClose: () => void; 20 | brandImageAlt: string; 21 | productName: string; 22 | } 23 | 24 | interface IState { 25 | applicationInfo: { server_version: string }; 26 | } 27 | 28 | export const AboutModalWindow = (props: IProps) => { 29 | const { isOpen, onClose, user, brandImageAlt, productName } = props; 30 | const browser = detect(); 31 | return ( 32 | 40 | 41 | 42 | {`User`} 43 | {user?.email} 44 | {`Browser Version`} 45 | {browser?.name + ' ' + browser?.version} 46 | {`Browser OS`} 47 | {browser?.os} 48 | 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/eda_server/schema/job.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime 16 | from typing import Optional 17 | 18 | from pydantic import BaseModel, StrictStr 19 | 20 | 21 | class JobInstanceCreate(BaseModel): 22 | id: Optional[int] 23 | name: Optional[StrictStr] 24 | action: Optional[StrictStr] 25 | ruleset: Optional[StrictStr] 26 | rule: Optional[StrictStr] 27 | hosts: Optional[StrictStr] 28 | playbook_id: Optional[int] 29 | inventory_id: Optional[int] 30 | extra_var_id: Optional[int] 31 | 32 | 33 | class JobInstanceBaseRead(JobInstanceCreate): 34 | uuid: StrictStr 35 | 36 | 37 | class JobRef(BaseModel): 38 | id: int 39 | name: Optional[str] 40 | 41 | 42 | class JobInstanceRead(BaseModel): 43 | id: int 44 | uuid: StrictStr 45 | action: Optional[StrictStr] 46 | name: StrictStr 47 | ruleset: Optional[StrictStr] 48 | rule: Optional[StrictStr] 49 | hosts: Optional[StrictStr] 50 | status: Optional[StrictStr] 51 | fired_date: Optional[datetime] 52 | 53 | 54 | class JobInstanceEventsRead(BaseModel): 55 | id: int 56 | job_uuid: StrictStr 57 | counter: int 58 | stdout: StrictStr 59 | type: Optional[StrictStr] 60 | created_at: Optional[StrictStr] 61 | -------------------------------------------------------------------------------- /ui/src/test/AuditView/__snapshots__/audit-hosts.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AuditHosts should render the AuditView component 1`] = ` 4 | 11 | 22 | 31 | 63 | 66 | 67 | 68 | 69 | 70 | `; 71 | -------------------------------------------------------------------------------- /ui/src/test/AuditView/__snapshots__/audit-rules.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AuditHosts should render the AuditView component 1`] = ` 4 | 11 | 22 | 31 | 63 | 66 | 67 | 68 | 69 | 70 | `; 71 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209221449_d4df045eb3ce_add_job_instance_host_table.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Add job_instance_host table. 16 | 17 | Revision ID: d4df045eb3ce 18 | Revises: 1285eea03d23 19 | Create Date: 2022-09-22 14:49:17.873228+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | from sqlalchemy.dialects import postgresql 25 | 26 | # revision identifiers, used by Alembic. 27 | revision = "d4df045eb3ce" 28 | down_revision = "1285eea03d23" 29 | branch_labels = None 30 | depends_on = None 31 | 32 | 33 | def upgrade() -> None: 34 | op.create_table( 35 | "job_instance_host", 36 | sa.Column( 37 | "id", sa.Integer(), sa.Identity(always=True), nullable=False 38 | ), 39 | sa.Column("host", sa.String(), nullable=True), 40 | sa.Column("job_uuid", postgresql.UUID(), nullable=True), 41 | sa.Column("playbook", sa.String(), nullable=True), 42 | sa.Column("play", sa.String(), nullable=True), 43 | sa.Column("task", sa.String(), nullable=True), 44 | sa.Column("status", sa.String(), nullable=True), 45 | sa.PrimaryKeyConstraint("id", name=op.f("pk_job_instance_host")), 46 | ) 47 | 48 | 49 | def downgrade() -> None: 50 | op.drop_table("job_instance_host") 51 | -------------------------------------------------------------------------------- /src/eda_server/db/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .activation import ( 16 | activation_instance_logs, 17 | activation_instances, 18 | activations, 19 | ) 20 | from .auth import User, role_permissions, roles, user_roles 21 | from .base import Base, metadata 22 | from .inventory import inventories 23 | from .job import ( 24 | activation_instance_job_instances, 25 | job_instance_events, 26 | job_instance_hosts, 27 | job_instances, 28 | jobs, 29 | ) 30 | from .project import extra_vars, playbooks, projects 31 | from .rulebook import audit_rules, rulebooks, rules, rulesets 32 | 33 | __all__ = ( 34 | # base 35 | "Base", 36 | "metadata", 37 | # auth 38 | "User", 39 | "roles", 40 | "user_roles", 41 | "role_permissions", 42 | # activation 43 | "activations", 44 | "activation_instances", 45 | "activation_instance_logs", 46 | # inventory 47 | "inventories", 48 | # job 49 | "activation_instance_job_instances", 50 | "job_instance_events", 51 | "job_instance_hosts", 52 | "job_instances", 53 | "jobs", 54 | # project 55 | "extra_vars", 56 | "playbooks", 57 | "projects", 58 | # rulebook 59 | "rulebooks", 60 | "rules", 61 | "audit_rules", 62 | "rulesets", 63 | ) 64 | -------------------------------------------------------------------------------- /ui/src/app/Vars/Vars.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Title } from '@patternfly/react-core'; 3 | import { Link } from 'react-router-dom'; 4 | import { 5 | Card, 6 | CardBody as PFCardBody, 7 | CardTitle, 8 | SimpleList as PFSimpleList, 9 | SimpleListItem, 10 | Stack, 11 | StackItem, 12 | } from '@patternfly/react-core'; 13 | import styled from 'styled-components'; 14 | import { TopToolbar } from '@app/shared/top-toolbar'; 15 | import {listExtraVars} from "@app/API/Extravar"; 16 | 17 | export interface ExtraVarType { 18 | id: string; 19 | name: string; 20 | extra_var: string; 21 | } 22 | 23 | const CardBody = styled(PFCardBody)` 24 | white-space: pre-wrap; 25 | `; 26 | const SimpleList = styled(PFSimpleList)` 27 | white-space: pre-wrap; 28 | `; 29 | 30 | const Vars: React.FunctionComponent = () => { 31 | const [extraVars, setVars] = useState([]); 32 | 33 | useEffect(() => { 34 | listExtraVars() 35 | .then((data) => setVars(data.data)); 36 | }, []); 37 | 38 | return ( 39 | 40 | 41 | Extra vars 42 | 43 | 44 | 45 | 46 | Extra Vars 47 | 48 | {extraVars.length !== 0 && ( 49 | 50 | {extraVars.map((item, i) => ( 51 | 52 | {item.name} 53 | 54 | ))} 55 | 56 | )} 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export { Vars }; 66 | -------------------------------------------------------------------------------- /tests/integration/utils/db.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pathlib 16 | from typing import Callable 17 | 18 | import alembic.command 19 | import alembic.config 20 | import sqlalchemy as sa 21 | import sqlalchemy.future 22 | from sqlalchemy.ext.asyncio import AsyncConnection 23 | 24 | BASE_DIR = pathlib.Path(__file__).parents[3] 25 | ALEMBIC_INI = BASE_DIR / "alembic.ini" 26 | 27 | 28 | async def drop_database(connection: AsyncConnection, name: str): 29 | await connection.execute(sa.text(f'DROP DATABASE IF EXISTS "{name}"')) 30 | 31 | 32 | async def create_database(connection: AsyncConnection, name: str): 33 | await drop_database(connection, name) 34 | await connection.execute(sa.text(f'CREATE DATABASE "{name}"')) 35 | 36 | 37 | def alembic_command_wrapper( 38 | connection: sqlalchemy.future.Engine, 39 | command: Callable[[alembic.config.Config, ...], None], 40 | config: alembic.config.Config, 41 | *args, 42 | **kwargs, 43 | ): 44 | config.attributes["connection"] = connection 45 | command(config, *args, **kwargs) 46 | 47 | 48 | async def upgrade_database(connection): 49 | await connection.run_sync( 50 | alembic_command_wrapper, 51 | alembic.command.upgrade, 52 | alembic.config.Config(str(ALEMBIC_INI)), 53 | "head", 54 | ) 55 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202210071428_abfdc233b4b0_rulebook_new_fields.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """rulebook new fields. 16 | 17 | Revision ID: abfdc233b4b0 18 | Revises: 12c3a0edc032 19 | Create Date: 2022-10-07 14:28:05.385484+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "abfdc233b4b0" 27 | down_revision = "12c3a0edc032" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column( 34 | "rulebook", sa.Column("description", sa.String(), nullable=True) 35 | ) 36 | op.add_column( 37 | "rulebook", 38 | sa.Column( 39 | "created_at", 40 | sa.DateTime(timezone=True), 41 | server_default=sa.text("now()"), 42 | nullable=False, 43 | ), 44 | ) 45 | op.add_column( 46 | "rulebook", 47 | sa.Column( 48 | "modified_at", 49 | sa.DateTime(timezone=True), 50 | server_default=sa.text("now()"), 51 | nullable=False, 52 | ), 53 | ) 54 | 55 | 56 | def downgrade() -> None: 57 | op.drop_column("rulebook", "modified_at") 58 | op.drop_column("rulebook", "created_at") 59 | op.drop_column("rulebook", "description") 60 | -------------------------------------------------------------------------------- /ui/src/app/API/Activation.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@app/shared/pagination'; 2 | import { getAxiosInstance } from '@app/API/baseApi'; 3 | import { AxiosResponse } from 'axios'; 4 | 5 | export interface NewActivationType { 6 | name: string; 7 | rulebook_id: string; 8 | inventory_id: string; 9 | extra_var_id: string; 10 | project_id: string; 11 | working_directory?: string; 12 | execution_environment?: string; 13 | } 14 | 15 | export interface IRemoveActivation { 16 | ids?: Array; 17 | fetchData?: any; 18 | pagination?: PaginationConfiguration; 19 | resetSelectedActivations?: any; 20 | } 21 | 22 | const activationEndpoint = '/api/activation_instance'; 23 | const activationsEndpoint = '/api/activation_instances'; 24 | const activationJobsEndpoint = '/api/activation_instance_job_instances'; 25 | const activationLogsEndpoint = '/api/activation_instance_logs/?activation_instance_id='; 26 | 27 | export const listActivationJobs = ( 28 | activationId: string | number, 29 | pagination = defaultSettings 30 | ): Promise => { 31 | return getAxiosInstance().get(`${activationJobsEndpoint}/${activationId}`); 32 | }; 33 | 34 | export const fetchActivation = (id: string | number | undefined): Promise => 35 | getAxiosInstance().get(`${activationEndpoint}/${id}`); 36 | 37 | export const listActivations = (pagination = defaultSettings): Promise => 38 | getAxiosInstance().get(activationsEndpoint); 39 | 40 | export const addRulebookActivation = (activationData: NewActivationType) => 41 | getAxiosInstance().post(activationEndpoint, activationData); 42 | 43 | export const removeActivation = (activationId: string | number): Promise => 44 | getAxiosInstance().delete(`${activationEndpoint}/${activationId}`); 45 | 46 | export const fetchActivationOutput = (id: string): Promise => 47 | getAxiosInstance().get(`${activationLogsEndpoint}${id}`); 48 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202208161211_2425525e8124_native_uuid.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Use native PostgreSQL UUID type. 16 | 17 | Revision ID: 2425525e8124 18 | Revises: 3412abd6396d 19 | Create Date: 2022-08-16 14:11:36.307266 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | from sqlalchemy.dialects import postgresql 25 | 26 | # revision identifiers, used by Alembic. 27 | revision = "2425525e8124" 28 | down_revision = "3412abd6396d" 29 | branch_labels = None 30 | depends_on = None 31 | 32 | 33 | def upgrade() -> None: 34 | op.alter_column( 35 | "job", 36 | "uuid", 37 | type_=postgresql.UUID(as_uuid=False), 38 | postgresql_using="uuid::uuid", 39 | ) 40 | op.alter_column( 41 | "job_instance", 42 | "uuid", 43 | type_=postgresql.UUID(as_uuid=False), 44 | postgresql_using="uuid::uuid", 45 | ) 46 | op.alter_column( 47 | "job_instance_event", 48 | "job_uuid", 49 | type_=postgresql.UUID(as_uuid=False), 50 | postgresql_using="job_uuid::uuid", 51 | ) 52 | 53 | 54 | def downgrade() -> None: 55 | op.alter_column("job", "uuid", type_=sa.String()) 56 | op.alter_column("job_instance", "uuid", type_=sa.String()) 57 | op.alter_column("job_instance_event", "job_uuid", type_=sa.String()) 58 | -------------------------------------------------------------------------------- /ui/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // Automatically clear mock calls and instances between every test 6 | clearMocks: true, 7 | 8 | // Indicates whether the coverage information should be collected while executing the test 9 | collectCoverage: false, 10 | 11 | // The directory where Jest should output its coverage files 12 | coverageDirectory: 'coverage', 13 | 14 | // An array of directory names to be searched recursively up from the requiring module's location 15 | moduleDirectories: ['node_modules', '/src'], 16 | 17 | // A map from regular expressions to module names that allow to stub out resources with a single module 18 | moduleNameMapper: { 19 | '\\.(css|less)$': '/__mocks__/styleMock.js', 20 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 21 | '/__mocks__/fileMock.js', 22 | 'monaco-editor': '/node_modules/react-monaco-editor', 23 | '@app/(.*)': '/src/app/$1', 24 | }, 25 | transform: { 26 | '^.+\\.jsx?$': 'babel-jest', 27 | '^.+\\.tsx?$': 'ts-jest', 28 | }, 29 | transformIgnorePatterns: ['node_modules/(?!react-monaco-editor|@react-hook/*|@patternfly/react-tokens)'], 30 | // A preset that is used as a base for Jest's configuration 31 | preset: 'ts-jest/presets/js-with-ts', 32 | 33 | // The path to a module that runs some code to configure or set up the testing framework before each test 34 | setupFilesAfterEnv: ['/config/test-setup.js'], 35 | 36 | // The test environment that will be used for testing. 37 | testEnvironment: 'jsdom', 38 | 39 | testEnvironmentOptions: { url: 'http://localhost:8080' }, 40 | 41 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 42 | snapshotSerializers: ['enzyme-to-json/serializer'], 43 | }; 44 | -------------------------------------------------------------------------------- /src/eda_server/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import yaml 16 | from fastapi import APIRouter, Request, Response 17 | 18 | from . import ( 19 | activation, 20 | audit_rule, 21 | auth, 22 | inventory, 23 | job, 24 | project, 25 | role, 26 | rulebook, 27 | ssh, 28 | task, 29 | user, 30 | websocket, 31 | ) 32 | 33 | # TODO(cutwater): Will be updated in follow up PR: Refactor routers to 34 | # avoid route duplication. 35 | router = APIRouter() 36 | router.include_router(activation.router) 37 | router.include_router(audit_rule.router) 38 | router.include_router(auth.router) 39 | router.include_router(inventory.router) 40 | router.include_router(job.router) 41 | router.include_router(project.router) 42 | router.include_router(role.router) 43 | router.include_router(rulebook.router) 44 | router.include_router(task.router) 45 | router.include_router(user.router) 46 | router.include_router(ssh.router) 47 | router.include_router(websocket.router) 48 | 49 | 50 | OPENAPI_YAML_CACHE = None 51 | 52 | 53 | @router.get("/api/openapi.yml", include_in_schema=False) 54 | async def openapi_yaml(request: Request) -> Response: 55 | global OPENAPI_YAML_CACHE 56 | if OPENAPI_YAML_CACHE is None: 57 | OPENAPI_YAML_CACHE = yaml.dump(request.app.openapi()) 58 | 59 | return Response(OPENAPI_YAML_CACHE, media_type="text/yaml") 60 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202210171900_c703cd56f6b0_update_job_instance_table_columns.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Update job_instance table columns. 16 | 17 | Revision ID: c703cd56f6b0 18 | Revises: abfdc233b4b0 19 | Create Date: 2022-10-17 19:00:22.804697+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "c703cd56f6b0" 27 | down_revision = "abfdc233b4b0" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column( 34 | "job_instance", sa.Column("action", sa.String(), nullable=True) 35 | ) 36 | op.add_column( 37 | "job_instance", sa.Column("name", sa.String(), nullable=True) 38 | ) 39 | op.add_column( 40 | "job_instance", sa.Column("ruleset", sa.String(), nullable=True) 41 | ) 42 | op.add_column( 43 | "job_instance", sa.Column("rule", sa.String(), nullable=True) 44 | ) 45 | op.add_column( 46 | "job_instance", sa.Column("hosts", sa.String(), nullable=True) 47 | ) 48 | 49 | 50 | def downgrade() -> None: 51 | op.drop_column("job_instance", "hosts") 52 | op.drop_column("job_instance", "rule") 53 | op.drop_column("job_instance", "ruleset") 54 | op.drop_column("job_instance", "name") 55 | op.drop_column("job_instance", "action") 56 | -------------------------------------------------------------------------------- /tools/initial_data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: 3 | - name: 'admin' 4 | description: 'Has full access to all resources' 5 | permissions: 6 | - resources: 7 | - 'project' 8 | - 'inventory' 9 | - 'extra_var' 10 | - 'playbook' 11 | - 'rulebook' 12 | - 'execution_env' 13 | - 'role' 14 | - 'activation' 15 | - 'activation_instance' 16 | - 'audit_rule' 17 | - 'job' 18 | - 'task' 19 | - 'user' 20 | actions: 21 | - 'create' 22 | - 'read' 23 | - 'update' 24 | - 'delete' 25 | 26 | - name: 'manager' 27 | description: 'Can read and update any resources' 28 | permissions: 29 | - resources: 30 | - 'project' 31 | - 'inventory' 32 | - 'extra_var' 33 | - 'playbook' 34 | - 'rulebook' 35 | - 'execution_env' 36 | - 'activation' 37 | - 'activation_instance' 38 | - 'audit_rule' 39 | - 'job' 40 | - 'task' 41 | - 'user' 42 | actions: 43 | - 'read' 44 | - 'update' 45 | 46 | - name: 'default' 47 | description: 'Default role, that is applied to all users' 48 | is_default: true 49 | permissions: 50 | - resources: 51 | - 'project' 52 | - 'inventory' 53 | - 'extra_var' 54 | - 'playbook' 55 | - 'rulebook' 56 | - 'execution_env' 57 | - 'activation' 58 | - 'activation_instance' 59 | - 'job' 60 | - 'task' 61 | actions: 62 | - 'read' 63 | users: 64 | - email: 'root@example.com' 65 | password: 'secret' 66 | is_superuser: true 67 | 68 | - email: 'admin@example.com' 69 | password: 'secret' 70 | roles: ['admin'] 71 | 72 | - email: 'manager@example.com' 73 | password: 'secret' 74 | roles: ['manager'] 75 | 76 | - email: 'bob@example.com' 77 | password: 'secret' 78 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202208311823_c1eee0e47fc1_project_table_add_fields.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """project table add fields. 16 | 17 | Revision ID: c1eee0e47fc1 18 | Revises: 11fc4f933b72 19 | Create Date: 2022-08-31 18:23:12.353334+00:00 20 | """ 21 | 22 | import sqlalchemy as sa 23 | from alembic import op 24 | 25 | # revision identifiers, used by Alembic. 26 | revision = "c1eee0e47fc1" 27 | down_revision = "11fc4f933b72" 28 | branch_labels = None 29 | depends_on = None 30 | 31 | 32 | def upgrade() -> None: 33 | op.add_column("project", sa.Column("name", sa.String(), nullable=True)) 34 | op.add_column( 35 | "project", sa.Column("description", sa.String(), nullable=True) 36 | ) 37 | op.add_column( 38 | "project", 39 | sa.Column( 40 | "created_at", 41 | sa.DateTime(timezone=True), 42 | server_default=sa.text("now()"), 43 | nullable=False, 44 | ), 45 | ) 46 | op.add_column( 47 | "project", 48 | sa.Column( 49 | "modified_at", 50 | sa.DateTime(timezone=True), 51 | server_default=sa.text("now()"), 52 | nullable=False, 53 | ), 54 | ) 55 | 56 | 57 | def downgrade() -> None: 58 | op.drop_column("project", "modified_at") 59 | op.drop_column("project", "created_at") 60 | op.drop_column("project", "description") 61 | op.drop_column("project", "name") 62 | -------------------------------------------------------------------------------- /ui/src/test/AppLayout/AppLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { AppLayout } from '@app/AppLayout/AppLayout'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { mockApi } from '../__mocks__/baseApi'; 9 | import { Dashboard } from '@app/Dashboard/Dashboard'; 10 | import { NotificationsPortal } from '@redhat-cloud-services/frontend-components-notifications'; 11 | import store from '../../store'; 12 | import { Provider } from 'react-redux'; 13 | import {Button, NavExpandable, NavItem} from "@patternfly/react-core"; 14 | 15 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 16 | 17 | 18 | 19 | {children} 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | describe('AppLayout', () => { 27 | beforeAll(() => { 28 | mockApi.reset(); 29 | }); 30 | 31 | it('should render the AppLayout component', async () => { 32 | mockApi 33 | .onGet(`/api/users/me`) 34 | .replyOnce(200, { email: 'test@test.com', id: '1', is_active: true, is_superuser: false, is_verified: false }); 35 | 36 | let wrapper; 37 | await act(async () => { 38 | wrapper = mount( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }); 48 | await act(async () => { 49 | wrapper.update(); 50 | }); 51 | expect(wrapper.find(NavExpandable).length).toEqual(1); 52 | expect(wrapper.find(NavExpandable).at(0).props().title).toEqual('Management'); 53 | expect(wrapper.find(NavItem).length).toEqual(11); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/eda_server/db/models/inventory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sqlalchemy as sa 16 | from sqlalchemy.sql import func 17 | 18 | from eda_server.db.utils.common import enum_values 19 | from eda_server.types import InventorySource 20 | 21 | from .base import metadata 22 | 23 | __all__ = "inventories" 24 | 25 | inventories = sa.Table( 26 | "inventory", 27 | metadata, 28 | sa.Column( 29 | "id", 30 | sa.Integer, 31 | sa.Identity(always=True), 32 | primary_key=True, 33 | ), 34 | sa.Column("name", sa.String, nullable=False), 35 | sa.Column("description", sa.String, server_default="", nullable=True), 36 | sa.Column("inventory", sa.String, nullable=True), 37 | sa.Column( 38 | "project_id", 39 | sa.ForeignKey("project.id", ondelete="CASCADE"), 40 | nullable=True, 41 | ), 42 | sa.Column( 43 | "inventory_source", 44 | sa.Enum( 45 | InventorySource, 46 | name="inventory_source_enum", 47 | values_callable=enum_values, 48 | ), 49 | nullable=False, 50 | ), 51 | sa.Column( 52 | "created_at", 53 | sa.DateTime(timezone=True), 54 | nullable=False, 55 | server_default=func.now(), 56 | ), 57 | sa.Column( 58 | "modified_at", 59 | sa.DateTime(timezone=True), 60 | nullable=False, 61 | server_default=func.now(), 62 | onupdate=func.now(), 63 | ), 64 | ) 65 | -------------------------------------------------------------------------------- /ui/src/app/Playbooks/Playbooks.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import { Link } from 'react-router-dom'; 3 | import React, { useState, useEffect } from 'react'; 4 | import { 5 | Card, 6 | CardBody as PFCardBody, 7 | CardTitle, 8 | SimpleList as PFSimpleList, 9 | SimpleListItem, 10 | Stack, 11 | StackItem, 12 | } from '@patternfly/react-core'; 13 | import styled from 'styled-components'; 14 | import { getServer } from '@app/utils/utils'; 15 | import { TopToolbar } from '@app/shared/top-toolbar'; 16 | import { PlaybookType } from '@app/RuleSets/RuleSets'; 17 | 18 | const CardBody = styled(PFCardBody)` 19 | white-space: pre-wrap; 20 | `; 21 | const SimpleList = styled(PFSimpleList)` 22 | white-space: pre-wrap; 23 | `; 24 | 25 | const endpoint = 'http://' + getServer() + '/api/playbooks'; 26 | 27 | const Playbooks: React.FunctionComponent = () => { 28 | const [playbooks, setPlaybooks] = useState([]); 29 | 30 | useEffect(() => { 31 | fetch(endpoint, { 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | }) 36 | .then((response) => response.json()) 37 | .then((data) => setPlaybooks(data)); 38 | }, []); 39 | 40 | return ( 41 | 42 | 43 | Playbooks 44 | 45 | 46 | 47 | 48 | Playbooks 49 | 50 | {playbooks.length !== 0 && ( 51 | 52 | {playbooks.map((item, i) => ( 53 | 54 | {item?.name} 55 | 56 | ))} 57 | 58 | )} 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export { Playbooks }; 68 | -------------------------------------------------------------------------------- /ui/config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 5 | const Dotenv = require('dotenv-webpack'); 6 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 7 | 8 | const BG_IMAGES_DIRNAME = 'bgimages'; 9 | const ASSET_PATH = process.env.ASSET_PATH || '/eda/'; 10 | module.exports = (env) => { 11 | return { 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(tsx|ts|jsx|js)?$/, 16 | exclude: /node_modules/, 17 | use: [ 18 | { 19 | loader: 'ts-loader', 20 | options: { 21 | transpileOnly: true, 22 | experimentalWatchApi: true, 23 | }, 24 | }, 25 | ], 26 | }, 27 | { 28 | test: /\.(woff(2)?|ttf|jpg|png|eot|gif|svg)(\?v=\d+\.\d+\.\d+)?$/, 29 | type: 'asset/resource', 30 | generator: { 31 | filename: 'fonts/[name][ext]', 32 | }, 33 | }, 34 | ], 35 | }, 36 | output: { 37 | filename: '[name].bundle.js', 38 | path: path.resolve(__dirname, '../dist'), 39 | publicPath: ASSET_PATH, 40 | }, 41 | plugins: [ 42 | new MonacoWebpackPlugin({ 43 | languages: ['yaml'], 44 | }), 45 | new HtmlWebpackPlugin({ 46 | template: path.resolve(__dirname, '../src', 'index.html'), 47 | applicationName: 'Event driven automation', 48 | favicon: 'src/assets/images/favicon.ico', 49 | }), 50 | new Dotenv({ 51 | systemvars: true, 52 | silent: true, 53 | }), 54 | ], 55 | resolve: { 56 | extensions: ['.js', '.ts', '.tsx', '.jsx'], 57 | plugins: [ 58 | new TsconfigPathsPlugin({ 59 | configFile: path.resolve(__dirname, '../tsconfig.json'), 60 | }), 61 | ], 62 | symlinks: false, 63 | cacheWithContext: false, 64 | }, 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/eda_server/schema/audit_rule.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime 16 | from typing import Optional 17 | 18 | from pydantic import BaseModel, StrictStr 19 | 20 | from .job import JobRef 21 | from .rulebook import RuleRef, RuleRulesetRef 22 | 23 | 24 | class AuditRuleActivationRef(BaseModel): 25 | id: int 26 | name: StrictStr 27 | 28 | 29 | class AuditRule(BaseModel): 30 | name: StrictStr 31 | description: Optional[StrictStr] 32 | status: Optional[StrictStr] 33 | activation: AuditRuleActivationRef 34 | ruleset: RuleRulesetRef 35 | created_at: datetime 36 | fired_date: datetime 37 | definition: dict 38 | 39 | 40 | class AuditRuleJobInstance(BaseModel): 41 | id: int 42 | status: Optional[StrictStr] 43 | last_fired_date: datetime 44 | 45 | 46 | class AuditRuleJobInstanceEvent(BaseModel): 47 | id: int 48 | name: Optional[StrictStr] 49 | increment_counter: int 50 | type: Optional[StrictStr] 51 | timestamp: datetime 52 | 53 | 54 | class AuditRuleHost(BaseModel): 55 | id: int 56 | name: Optional[StrictStr] 57 | status: Optional[StrictStr] 58 | 59 | 60 | class AuditFiredRule(BaseModel): 61 | job: JobRef 62 | rule: RuleRef 63 | ruleset: RuleRulesetRef 64 | status: Optional[StrictStr] 65 | fired_date: Optional[datetime] 66 | 67 | 68 | class AuditChangedHost(BaseModel): 69 | host: StrictStr 70 | rule: RuleRef 71 | ruleset: RuleRulesetRef 72 | fired_date: Optional[datetime] 73 | -------------------------------------------------------------------------------- /ui/src/test/Job/job.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { Job } from '@app/Job/Job'; 4 | import { MemoryRouter } from 'react-router'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { Route } from 'react-router-dom'; 8 | import { DropdownItem, KebabToggle, Tab } from '@patternfly/react-core'; 9 | import { mockApi } from '../__mocks__/baseApi'; 10 | 11 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | 19 | describe('Job', () => { 20 | beforeAll(() => { 21 | mockApi.reset(); 22 | }); 23 | 24 | it('has a top toolbar kebab menu', async () => { 25 | mockApi.onGet(`/api/job_instance/1`).replyOnce(200, { 26 | name: 'Job 1', 27 | id: 1, 28 | stdout: [], 29 | }); 30 | 31 | mockApi.onGet(`/api/job_instance_events/1`).replyOnce(200, { 32 | name: 'Event 1', 33 | id: 1, 34 | }); 35 | 36 | let wrapper; 37 | await act(async () => { 38 | wrapper = mount( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }); 46 | await act(async () => { 47 | wrapper.update(); 48 | }); 49 | expect(wrapper.find(KebabToggle).length).toEqual(1); 50 | expect(wrapper.find('.pf-c-dropdown__toggle').length).toEqual(1); 51 | wrapper.find('.pf-c-dropdown__toggle').simulate('click'); 52 | wrapper.update(); 53 | expect(wrapper.find(DropdownItem).length).toEqual(3); 54 | expect(wrapper.find(DropdownItem).at(0).props().component.props.children).toEqual('Edit'); 55 | expect(wrapper.find(DropdownItem).at(1).props().component.props.children).toEqual('Launch'); 56 | expect(wrapper.find(DropdownItem).at(2).props().component.props.children).toEqual('Delete'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/eda_server/db/migrations/versions/202209131406_74607f5764f9_update_activation_table_columns.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Update activation table columns. 16 | 17 | Revision ID: 74607f5764f9 18 | Revises: 93a62b2e768b 19 | Create Date: 2022-09-13 14:06:07.046278+00:00 20 | """ 21 | 22 | from alembic import op 23 | 24 | # revision identifiers, used by Alembic. 25 | revision = "74607f5764f9" 26 | down_revision = "93a62b2e768b" 27 | branch_labels = None 28 | depends_on = None 29 | 30 | 31 | def upgrade() -> None: 32 | op.alter_column( 33 | "activation", 34 | "activation_status", 35 | nullable=True, 36 | new_column_name="status", 37 | ) 38 | op.alter_column( 39 | "activation", 40 | "activation_enabled", 41 | nullable=False, 42 | new_column_name="is_enabled", 43 | ) 44 | op.alter_column( 45 | "activation", 46 | "restarted_count", 47 | nullable=False, 48 | new_column_name="restart_count", 49 | ) 50 | 51 | 52 | def downgrade() -> None: 53 | op.alter_column( 54 | "activation", 55 | "status", 56 | nullable=True, 57 | new_column_name="activation_status", 58 | ) 59 | op.alter_column( 60 | "activation", 61 | "is_enabled", 62 | nullable=False, 63 | new_column_name="activation_enabled", 64 | ) 65 | op.alter_column( 66 | "activation", 67 | "restart_count", 68 | nullable=False, 69 | new_column_name="restarted_count", 70 | ) 71 | -------------------------------------------------------------------------------- /ui/src/app/AppLayout/stateful-dropdown.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Dropdown, DropdownPosition, KebabToggle, DropdownToggle } from '@patternfly/react-core'; 4 | 5 | export class StatefulDropdown extends React.Component { 6 | static defaultProps = { 7 | isPlain: true, 8 | toggleType: 'kebab', 9 | }; 10 | 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | isOpen: false, 15 | selected: undefined, 16 | }; 17 | } 18 | 19 | render() { 20 | const { isOpen } = this.state; 21 | const { items, toggleType, defaultText, position, isPlain, ariaLabel } = this.props; 22 | return ( 23 | this.onSelect(e)} 25 | toggle={this.renderToggle(toggleType, defaultText)} 26 | isOpen={isOpen} 27 | isPlain={isPlain} 28 | dropdownItems={items} 29 | position={position || DropdownPosition.right} 30 | autoFocus={false} 31 | aria-label={ariaLabel} 32 | /> 33 | ); 34 | } 35 | 36 | renderToggle(toggleType, defaultText) { 37 | switch (toggleType) { 38 | case 'dropdown': 39 | return ( 40 | this.onToggle(e)}> 41 | {this.state.selected ? this.state.selected : defaultText || ''} 42 | 43 | ); 44 | case 'icon': 45 | return ( 46 | this.onToggle(e)}> 47 | {this.state.selected ? this.state.selected : defaultText || 'Dropdown'} 48 | 49 | ); 50 | case 'kebab': 51 | return this.onToggle(e)} />; 52 | } 53 | } 54 | 55 | onToggle(isOpen) { 56 | this.setState({ 57 | isOpen, 58 | }); 59 | } 60 | 61 | onSelect(event) { 62 | this.setState( 63 | { 64 | isOpen: !this.state.isOpen, 65 | selected: event.currentTarget.value, 66 | }, 67 | () => { 68 | if (this.props.onSelect) { 69 | this.props.onSelect(event); 70 | } 71 | } 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/src/app/Activation/activation-stdout.tsx: -------------------------------------------------------------------------------- 1 | import { CardBody, PageSection } from '@patternfly/react-core'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { Card, CardTitle, Stack, StackItem } from '@patternfly/react-core'; 4 | import { useIntl } from 'react-intl'; 5 | import { getServer } from '@app/utils/utils'; 6 | import { renderActivationTabs } from '@app/Activation/Activation'; 7 | import { ActivationType } from '@app/Activations/Activations'; 8 | import { fetchActivationOutput } from '@app/API/Activation'; 9 | import { LogViewer } from '@patternfly/react-log-viewer'; 10 | 11 | const ActivationStdout: React.FunctionComponent<{ activation: ActivationType }> = ({ activation }) => { 12 | const intl = useIntl(); 13 | const [stdout, setStdout] = useState([]); 14 | const [newStdout, setNewStdout] = useState(''); 15 | 16 | useEffect(() => { 17 | fetchActivationOutput(activation.id); 18 | }, []); 19 | 20 | const [update_client, setUpdateClient] = useState({}); 21 | useEffect(() => { 22 | const uc = new WebSocket('ws://' + getServer() + '/api/ws-activation/' + activation.id); 23 | setUpdateClient(uc); 24 | uc.onopen = () => { 25 | console.log('Update client connected'); 26 | }; 27 | uc.onmessage = (message) => { 28 | const [messageType, data] = JSON.parse(message.data); 29 | if (messageType === 'Stdout') { 30 | const { stdout: dataStdout } = data; 31 | setNewStdout(dataStdout); 32 | } 33 | }; 34 | }, []); 35 | 36 | useEffect(() => { 37 | setStdout([...stdout, newStdout]); 38 | }, [newStdout]); 39 | 40 | return ( 41 | 42 | {renderActivationTabs(activation.id, intl)} 43 | 44 | 45 | 46 | Standard Out 47 | {stdout && stdout.length > 0 && } 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export { ActivationStdout }; 56 | -------------------------------------------------------------------------------- /ui/src/test/NewJob/NewJob.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { MemoryRouter } from 'react-router'; 4 | import { act } from 'react-dom/test-utils'; 5 | import { IntlProvider } from 'react-intl'; 6 | import { Route } from 'react-router-dom'; 7 | import { Button } from '@patternfly/react-core'; 8 | import store from '../../store'; 9 | import { Provider } from 'react-redux'; 10 | import { NewJob } from '@app/NewJob/NewJob'; 11 | import { mockApi } from '../__mocks__/baseApi'; 12 | 13 | const ComponentWrapper = ({ children, initialEntries = ['/dashboard'] }) => ( 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | ); 22 | 23 | describe('NewJob', () => { 24 | beforeAll(() => { 25 | mockApi.reset(); 26 | }); 27 | 28 | it('should render the New Job form', async () => { 29 | mockApi.onGet(`/api/extra_vars`).replyOnce(200, [ 30 | { id: '1', name: 'Var 1' }, 31 | { id: '2', name: 'Var 2' }, 32 | ]); 33 | mockApi.onGet(`/api/inventories`).replyOnce(200, [ 34 | { id: '1', name: 'Inventory 1' }, 35 | { id: '2', name: 'Inventory 2' }, 36 | ]); 37 | mockApi.onGet(`/api/playbooks`).replyOnce(200, [ 38 | { id: '1', name: 'Playbook 1' }, 39 | { id: '2', name: 'Playbook 2' }, 40 | { id: '3', name: 'Playbook 3' }, 41 | ]); 42 | 43 | let wrapper; 44 | await act(async () => { 45 | wrapper = mount( 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | }); 53 | await act(async () => { 54 | wrapper.update(); 55 | }); 56 | expect(wrapper.find(Button).length).toEqual(2); 57 | wrapper.find(Button).at(0).simulate('click'); 58 | await act(async () => { 59 | wrapper.update(); 60 | }); 61 | expect(wrapper.find('div').at(6).at(0).props().children.at(1)).toEqual('Select a playbook'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /ui/src/app/AuditView/AuditView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Level, LevelItem, Title } from '@patternfly/react-core'; 3 | import { Route, Switch, useLocation } from 'react-router-dom'; 4 | import { useIntl } from 'react-intl'; 5 | import AppTabs from '@app/shared/app-tabs'; 6 | import { TopToolbar } from '@app/shared/top-toolbar'; 7 | import { AnyObject, TabItemType } from '@app/shared/types/common-types'; 8 | import sharedMessages from '../messages/shared.messages'; 9 | import { AuditRules } from '@app/AuditView/audit-rules'; 10 | import { AuditHosts } from '@app/AuditView/audit-hosts'; 11 | 12 | export interface AuditRuleType { 13 | id: string; 14 | name: string; 15 | job: string; 16 | job_name: string; 17 | status: string; 18 | ruleset: string; 19 | ruleset_name: string; 20 | fired_at?: string; 21 | } 22 | 23 | export interface AuditHostType { 24 | rule: { id: string; name: string }; 25 | job?: { id: string; name: string }; 26 | ruleset?: { id: string; name: string }; 27 | status: string; 28 | fired_date?: string; 29 | } 30 | 31 | const buildAuditTabs = (intl: AnyObject): TabItemType[] => [ 32 | { eventKey: 0, title: intl.formatMessage(sharedMessages.audit_rules_title), name: `/audit/rules` }, 33 | { 34 | eventKey: 1, 35 | title: intl.formatMessage(sharedMessages.audit_hosts_title), 36 | name: `/audit/hosts`, 37 | }, 38 | ]; 39 | 40 | export const renderAuditTabs = (intl) => { 41 | const audit_tabs = buildAuditTabs(intl); 42 | return ; 43 | }; 44 | 45 | const AuditView: React.FunctionComponent = () => { 46 | const intl = useIntl(); 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | {intl.formatMessage(sharedMessages.audit_view_title)} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export { AuditView }; 70 | --------------------------------------------------------------------------------