From 563721e67cc17d90e4fa028b245ea97cfb7192bc Mon Sep 17 00:00:00 2001 From: Artem Darius Weber Date: Mon, 18 Nov 2024 16:45:48 +0300 Subject: [PATCH] upd --- app/Console/Commands/SyncAirflowData.php | 112 ++ app/Console/Kernel.php | 1 + app/Livewire/AirFlowTargetTable.php | 16 + app/Livewire/AirflowDataView.php | 28 + app/Livewire/AirflowForm.php | 49 + app/Livewire/BaseTableComponent.php | 17 + app/Livewire/UsersTable.php | 13 + app/Models/TaskInstance.php | 52 + app/Models/airflow.php | 39 + app/Models/dag.php | 82 + app/Models/dag_event.php | 39 + app/Models/dag_historie.php | 38 + app/Models/dag_run.php | 55 + config/jetstream.php | 2 +- config/permission.php | 186 +++ ...024_10_01_211759_create_airflows_table.php | 36 + .../2024_10_01_224955_create_dags_table.php | 35 + ..._10_01_225847_create_permission_tables.php | 140 ++ ...024_10_01_232237_create_dag_runs_table.php | 36 + ...0_01_232313_create_dag_histories_table.php | 31 + ...4_10_01_232612_create_dag_events_table.php | 32 + ..._01_232653_create_task_instances_table.php | 47 + final-instructions.md | 1163 +++++++++++++++ resources/views/dashboard.blade.php | 2 +- resources/views/layouts/app.blade.php | 11 +- .../livewire/airflow-data-view.blade.php | 39 + .../views/livewire/airflow-form.blade.php | 53 + .../livewire/base-table-component.blade.php | 33 + .../views/livewire/users-table.blade.php | 3 + resources/views/navigation-menu.blade.php | 16 + resources/views/pages/admin/airflow.blade.php | 19 + .../views/pages/admin/airflows.blade.php | 19 + resources/views/pages/admin/dags.blade.php | 19 + resources/views/pages/client/dags.blade.php | 0 .../views/pages/client/view-dag.blade.php | 1 + routes/web.php | 12 + tmp-instructions.md | 1315 +++++++++++++++++ 37 files changed, 3788 insertions(+), 3 deletions(-) create mode 100644 app/Console/Commands/SyncAirflowData.php create mode 100644 app/Livewire/AirFlowTargetTable.php create mode 100644 app/Livewire/AirflowDataView.php create mode 100644 app/Livewire/AirflowForm.php create mode 100644 app/Livewire/BaseTableComponent.php create mode 100644 app/Livewire/UsersTable.php create mode 100644 app/Models/TaskInstance.php create mode 100644 app/Models/airflow.php create mode 100644 app/Models/dag.php create mode 100644 app/Models/dag_event.php create mode 100644 app/Models/dag_historie.php create mode 100644 app/Models/dag_run.php create mode 100644 config/permission.php create mode 100644 database/migrations/2024_10_01_211759_create_airflows_table.php create mode 100644 database/migrations/2024_10_01_224955_create_dags_table.php create mode 100644 database/migrations/2024_10_01_225847_create_permission_tables.php create mode 100644 database/migrations/2024_10_01_232237_create_dag_runs_table.php create mode 100644 database/migrations/2024_10_01_232313_create_dag_histories_table.php create mode 100644 database/migrations/2024_10_01_232612_create_dag_events_table.php create mode 100644 database/migrations/2024_10_01_232653_create_task_instances_table.php create mode 100644 final-instructions.md create mode 100644 resources/views/livewire/airflow-data-view.blade.php create mode 100644 resources/views/livewire/airflow-form.blade.php create mode 100644 resources/views/livewire/base-table-component.blade.php create mode 100644 resources/views/livewire/users-table.blade.php create mode 100644 resources/views/pages/admin/airflow.blade.php create mode 100644 resources/views/pages/admin/airflows.blade.php create mode 100644 resources/views/pages/admin/dags.blade.php create mode 100644 resources/views/pages/client/dags.blade.php create mode 100644 resources/views/pages/client/view-dag.blade.php create mode 100644 tmp-instructions.md diff --git a/app/Console/Commands/SyncAirflowData.php b/app/Console/Commands/SyncAirflowData.php new file mode 100644 index 0000000..9392ef1 --- /dev/null +++ b/app/Console/Commands/SyncAirflowData.php @@ -0,0 +1,112 @@ +get(); + + foreach ($airflows as $airflow) { + $this->info("Синхронизация данных для кластера: {$airflow->name}"); + + // Получаем DAG'и, привязанные к этому кластеру + $dags = DAG::where('airflow_id', $airflow->id)->get(); + + foreach ($dags as $dag) { + // Получаем DAG Runs из Airflow + $response = Http::withBasicAuth($airflow->username, $airflow->password) + ->get("{$airflow->url}/dags/{$dag->name}/dagRuns"); + + if ($response->ok()) { + $dagRunsData = $response->json()['dag_runs']; + + foreach ($dagRunsData as $dagRunData) { + // Обновляем или создаем DAGRun + $dagRun = dag_run::updateOrCreate( + ['run_id' => $dagRunData['dag_run_id']], + [ + 'dag_id' => $dag->id, + 'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow + 'status' => $dagRunData['state'], + 'execution_date' => $dagRunData['execution_date'], + 'start_date' => $dagRunData['start_date'] ?? null, + 'end_date' => $dagRunData['end_date'] ?? null, + 'queue' => null, + ] + ); + + // Получаем Task Instances для этого DAGRun + $taskInstancesResponse = Http::withBasicAuth($airflow->username, $airflow->password) + ->get("{$airflow->url}/dags/{$dag->name}/dagRuns/{$dagRun->run_id}/taskInstances"); + + if ($taskInstancesResponse->ok()) { + $taskInstancesData = $taskInstancesResponse->json()['task_instances']; + + foreach ($taskInstancesData as $taskInstanceData) { + // Обновляем или создаем TaskInstance + TaskInstance::updateOrCreate( + [ + 'dag_id' => $dag->id, + 'dag_run_id' => $dagRun->id, + 'task_id' => $taskInstanceData['task_id'], + ], + [ + 'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow + 'state' => $taskInstanceData['state'], + 'execution_date' => $taskInstanceData['execution_date'], + 'start_date' => $taskInstanceData['start_date'] ?? null, + 'end_date' => $taskInstanceData['end_date'] ?? null, + 'metadata' => $taskInstanceData, + ] + ); + + // Логирование событий задач + dag_event::create([ + 'dag_id' => $dag->id, + 'dag_run_id' => $dagRun->id, + 'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow + 'event_type' => 'task_' . $taskInstanceData['state'], + 'message' => "Задача {$taskInstanceData['task_id']} находится в состоянии {$taskInstanceData['state']}", + 'metadata' => $taskInstanceData, + ]); + } + } + + // Логирование событий DAGRun + dag_event::updateOrCreate( + [ + 'dag_id' => $dag->id, + 'dag_run_id' => $dagRun->id, + 'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow + 'event_type' => $dagRunData['state'], + ], + [ + 'message' => "DAGRun {$dagRun->run_id} находится в состоянии {$dagRunData['state']}", + 'metadata' => $dagRunData, + ] + ); + } + } else { + $this->error("Не удалось получить данные DAGRun для DAG {$dag->name} в кластере {$airflow->name}"); + } + } + } + + $this->info('Синхронизация данных Airflow завершена.'); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e6b9960..0661ca8 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule): void { // $schedule->command('inspire')->hourly(); + $schedule->command('airflow:sync-data')->everyFiveMinutes(); // todo: change on producation as every 5 secunds } /** diff --git a/app/Livewire/AirFlowTargetTable.php b/app/Livewire/AirFlowTargetTable.php new file mode 100644 index 0000000..9000345 --- /dev/null +++ b/app/Livewire/AirFlowTargetTable.php @@ -0,0 +1,16 @@ +headers = ['ID', 'Имя', 'URL', 'Username', 'Порт', 'Версия', 'Активный']; + $this->data = Airflow::all(['id', 'name', 'url', 'username', 'port', 'version', 'is_active'])->toArray(); + $this->view_item_route = "airflow.view"; + } +} diff --git a/app/Livewire/AirflowDataView.php b/app/Livewire/AirflowDataView.php new file mode 100644 index 0000000..9252437 --- /dev/null +++ b/app/Livewire/AirflowDataView.php @@ -0,0 +1,28 @@ +airflowId = $airflowId; + $this->loadAirflowData(); + } + + public function loadAirflowData() + { + $this->airflow = Airflow::with('user')->find($this->airflowId); + } + + public function render() + { + return view('livewire.airflow-data-view'); + } +} diff --git a/app/Livewire/AirflowForm.php b/app/Livewire/AirflowForm.php new file mode 100644 index 0000000..8a5e81c --- /dev/null +++ b/app/Livewire/AirflowForm.php @@ -0,0 +1,49 @@ +validate([ + 'name' => 'required|string|max:255', + 'url' => 'required|url', + 'username' => 'required|string|max:255', + 'password' => 'required|string|max:255', + 'api_token' => 'nullable|string|max:255', + 'port' => 'required|integer', + 'version' => 'nullable|string|max:255', + ]); + + Airflow::create([ + 'name' => $this->name, + 'url' => $this->url, + 'username' => $this->username, + 'password' => $this->password, + 'api_token' => $this->api_token, + 'port' => $this->port, + 'version' => $this->version, + 'user_id' => Auth::user()->id, + ]); + + session()->flash('message', 'Airflow cluster created successfully.'); + } + + public function render() + { + return view('livewire.airflow-form'); + } +} diff --git a/app/Livewire/BaseTableComponent.php b/app/Livewire/BaseTableComponent.php new file mode 100644 index 0000000..68046b4 --- /dev/null +++ b/app/Livewire/BaseTableComponent.php @@ -0,0 +1,17 @@ + 'array', + ]; + + /** + * Связь с Airflow кластером. + */ + public function airflow() + { + return $this->belongsTo(Airflow::class); + } + + /** + * Связь с моделью DAG. + */ + public function dag() + { + return $this->belongsTo(dag::class); + } + + /** + * Связь с моделью DAGRun. + */ + public function dagRun() + { + return $this->belongsTo(dag_run::class); + } +} diff --git a/app/Models/airflow.php b/app/Models/airflow.php new file mode 100644 index 0000000..6a09326 --- /dev/null +++ b/app/Models/airflow.php @@ -0,0 +1,39 @@ +belongsTo(User::class); + } + + /** + * Связь с DAG'ами, привязанными к этому кластеру. + */ + public function dags() + { + return $this->hasMany(dag::class); + } +} diff --git a/app/Models/dag.php b/app/Models/dag.php new file mode 100644 index 0000000..80114c0 --- /dev/null +++ b/app/Models/dag.php @@ -0,0 +1,82 @@ +belongsTo(Airflow::class); + } + + /** + * Связь с моделью User. + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Связь с моделью DAGRun. + */ + public function runs() + { + return $this->hasMany(dag_run::class); + } + + protected static function booted() + { + static::updated(function ($dag) { + $changes = $dag->getChanges(); + + // Исключаем системные поля + unset($changes['updated_at']); + + if (!empty($changes)) { + $original = $dag->getOriginal(); + + $changedData = []; + foreach ($changes as $key => $value) { + $changedData[$key] = [ + 'old' => $original[$key] ?? null, + 'new' => $value, + ]; + } + + dag_historie::create([ + 'dag_id' => $dag->id, + 'user_id' => Auth::id() ?? $dag->user_id, // Учитываем случаи, когда нет аутентифицированного пользователя + 'change_description' => 'Обновление DAG', + 'changed_data' => $changedData, + ]); + } + }); + } + + /** + * Связь с историей изменений DAG. + */ + public function histories() + { + return $this->hasMany(dag_historie::class); + } +} diff --git a/app/Models/dag_event.php b/app/Models/dag_event.php new file mode 100644 index 0000000..60d70c0 --- /dev/null +++ b/app/Models/dag_event.php @@ -0,0 +1,39 @@ + 'array', + ]; + + /** + * Связь с моделью DAG. + */ + public function dag() + { + return $this->belongsTo(dag::class); + } + + /** + * Связь с моделью DAGRun. + */ + public function dagRun() + { + return $this->belongsTo(dag_run::class); + } +} diff --git a/app/Models/dag_historie.php b/app/Models/dag_historie.php new file mode 100644 index 0000000..a193a64 --- /dev/null +++ b/app/Models/dag_historie.php @@ -0,0 +1,38 @@ + 'array', + ]; + + /** + * Связь с моделью DAG. + */ + public function dag() + { + return $this->belongsTo(DAG::class); + } + + /** + * Связь с моделью User. + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/dag_run.php b/app/Models/dag_run.php new file mode 100644 index 0000000..0e5bc99 --- /dev/null +++ b/app/Models/dag_run.php @@ -0,0 +1,55 @@ +belongsTo(Airflow::class); + } + + /** + * Связь с моделью DAG. + */ + public function dag() + { + return $this->belongsTo(dag::class); + } + + /** + * Связь с TaskInstance. + */ + public function taskInstances() + { + return $this->hasMany(TaskInstance::class); + } + + /** + * Связь с DAGEvent. + */ + public function events() + { + return $this->hasMany(dag_event::class); + } +} diff --git a/config/jetstream.php b/config/jetstream.php index d5e5f11..ea287a5 100644 --- a/config/jetstream.php +++ b/config/jetstream.php @@ -61,7 +61,7 @@ return [ // Features::termsAndPrivacyPolicy(), // Features::profilePhotos(), // Features::api(), - // Features::teams(['invitations' => true]), +// Features::teams(['invitations' => true]), Features::accountDeletion(), ], diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..2a520f3 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,186 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, //default 'role_id', + 'permission_pivot_key' => null, //default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/database/migrations/2024_10_01_211759_create_airflows_table.php b/database/migrations/2024_10_01_211759_create_airflows_table.php new file mode 100644 index 0000000..0d41e87 --- /dev/null +++ b/database/migrations/2024_10_01_211759_create_airflows_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name'); // Имя кластера + $table->string('url'); // URL или IP кластера Airflow + $table->string('username')->nullable(); // Логин для доступа (если используется) + $table->string('password')->nullable(); // Пароль для доступа (если используется) + $table->string('api_token')->nullable(); // Токен для доступа (если используется) + $table->string('port')->default('8080'); // Порт Airflow API + $table->string('version')->nullable(); // Версия Airflow + $table->boolean('is_active')->default(true); // Статус кластера (активен/неактивен) + $table->foreignId('user_id')->constrained(); // Кто создал/управляет кластером + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('airflows'); + } +}; diff --git a/database/migrations/2024_10_01_224955_create_dags_table.php b/database/migrations/2024_10_01_224955_create_dags_table.php new file mode 100644 index 0000000..0cc94b0 --- /dev/null +++ b/database/migrations/2024_10_01_224955_create_dags_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->foreignId('airflow_id')->constrained('airflows')->onDelete('cascade'); + $table->string('name'); + $table->string('description')->nullable(); + $table->string('file_path'); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('dags'); + } +}; diff --git a/database/migrations/2024_10_01_225847_create_permission_tables.php b/database/migrations/2024_10_01_225847_create_permission_tables.php new file mode 100644 index 0000000..9c7044b --- /dev/null +++ b/database/migrations/2024_10_01_225847_create_permission_tables.php @@ -0,0 +1,140 @@ +engine('InnoDB'); + $table->bigIncrements('id'); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { + //$table->engine('InnoDB'); + $table->bigIncrements('id'); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + if (empty($tableNames)) { + throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + } + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/database/migrations/2024_10_01_232237_create_dag_runs_table.php b/database/migrations/2024_10_01_232237_create_dag_runs_table.php new file mode 100644 index 0000000..181e440 --- /dev/null +++ b/database/migrations/2024_10_01_232237_create_dag_runs_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('dag_id')->constrained()->onDelete('cascade'); // Связь с DAG + $table->string('run_id')->unique(); // Уникальный идентификатор выполнения (из Airflow) + $table->foreignId('airflow_id')->after('dag_id')->constrained('airflows')->onDelete('cascade'); + $table->enum('status', ['queued', 'running', 'success', 'failed'])->default('queued'); // Статус выполнения + $table->timestamp('execution_date')->nullable(); // Дата выполнения + $table->timestamp('start_date')->nullable(); // Время начала + $table->timestamp('end_date')->nullable(); // Время окончания + $table->longText('logs')->nullable(); // Логи выполнения + $table->string('queue')->nullable(); // Название очереди + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('dag_runs'); + } +}; diff --git a/database/migrations/2024_10_01_232313_create_dag_histories_table.php b/database/migrations/2024_10_01_232313_create_dag_histories_table.php new file mode 100644 index 0000000..03598f4 --- /dev/null +++ b/database/migrations/2024_10_01_232313_create_dag_histories_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('dag_id')->constrained()->onDelete('cascade'); // Связь с DAG + $table->foreignId('user_id')->constrained()->onDelete('cascade'); // Пользователь, сделавший изменение + $table->text('change_description')->nullable(); // Описание изменения + $table->json('changed_data')->nullable(); // Данные изменений + $table->timestamps(); // created_at будет временем изменения + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('dag_histories'); + } +}; diff --git a/database/migrations/2024_10_01_232612_create_dag_events_table.php b/database/migrations/2024_10_01_232612_create_dag_events_table.php new file mode 100644 index 0000000..f235332 --- /dev/null +++ b/database/migrations/2024_10_01_232612_create_dag_events_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('dag_id')->constrained()->onDelete('cascade'); + $table->foreignId('dag_run_id')->nullable()->constrained()->onDelete('cascade'); + $table->string('event_type'); // Тип события: 'failure', 'success', 'task_failed', и т.д. + $table->text('message')->nullable(); // Дополнительное сообщение + $table->json('metadata')->nullable(); // Дополнительные данные + $table->timestamps(); // created_at будет временем события + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('dag_events'); + } +}; diff --git a/database/migrations/2024_10_01_232653_create_task_instances_table.php b/database/migrations/2024_10_01_232653_create_task_instances_table.php new file mode 100644 index 0000000..b5e692a --- /dev/null +++ b/database/migrations/2024_10_01_232653_create_task_instances_table.php @@ -0,0 +1,47 @@ +id(); + $table->foreignId('dag_id')->constrained()->onDelete('cascade'); + $table->foreignId('dag_run_id')->constrained()->onDelete('cascade'); + $table->foreignId('airflow_id')->after('dag_id')->constrained('airflows')->onDelete('cascade'); + $table->string('task_id'); // ID задачи в Airflow + $table->enum('state', [ + 'success', + 'running', + 'failed', + 'queued', + 'skipped', + 'up_for_retry', + 'upstream_failed', + 'up_for_reschedule', + 'none', + ])->nullable(); + $table->timestamp('execution_date')->nullable(); + $table->timestamp('start_date')->nullable(); + $table->timestamp('end_date')->nullable(); + $table->longText('log')->nullable(); // Логи выполнения задачи + $table->json('metadata')->nullable(); // Дополнительные данные + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('task_instances'); + } +}; diff --git a/final-instructions.md b/final-instructions.md new file mode 100644 index 0000000..b03d487 --- /dev/null +++ b/final-instructions.md @@ -0,0 +1,1163 @@ +у меня есть AirFlow класстер и github репозиторий для хранения DAGs. Я хочу написать Laravel Jetstream Livewire App для учета и загрузки DAGs в репозиторий чтобы AirFlow мог получить к ним доступ. Никто из пользователей не имеет доступ к Github репозиторию. Доступ осуществляется только через Laravel App в рамках предоставленных прав пользователя. Также я хотел бы видеть статусы выполнения и очередь пользовательского загруженного DAG в Laravel App. Обобщи следующие предложения и напиши финальный пошаговый план для реализации моей задачи: + +### **1. Настройка Laravel Jetstream с Livewire** + +- **Установка Laravel:** + + ```bash + composer create-project laravel/laravel airflow-dag-manager + ``` + +- **Установка Jetstream с Livewire:** + + ```bash + composer require laravel/jetstream + php artisan jetstream:install livewire + npm install + npm run dev + php artisan migrate + ``` + +--- + +### **2. Реализация аутентификации и авторизации пользователей** + +- **Аутентификация:** + + Jetstream предоставляет готовые механизмы регистрации, входа и восстановления пароля. + +- **Авторизация:** + + - Определите роли и разрешения пользователей. + - Используйте **Policies** или **Gates** Laravel для контроля доступа. + - Рекомендуется использовать пакет [Spatie Laravel-Permission](https://github.com/spatie/laravel-permission): + + ```bash + composer require spatie/laravel-permission + ``` + + - Настройте роли (например, `admin`, `user`) и разрешения (`upload dag`, `view status`). + +--- + +### **3. Интеграция с GitHub API для загрузки DAG-файлов** + +- **Аутентификация GitHub API:** + + - Создайте **Personal Access Token** с необходимыми правами (`repo` для приватных репозиториев). + - Сохраните токен в файле `.env`: + + ``` + GITHUB_TOKEN=ваш_токен + ``` + +- **Использование GitHub API:** + + - Используйте пакет [knplabs/github-api](https://github.com/KnpLabs/php-github-api) или **Guzzle** для взаимодействия с API. + + ```bash + composer require knplabs/github-api + ``` + + - Пример загрузки файла в репозиторий: + + ```php + use Github\Client; + + $client = new Client(); + $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN); + + $username = 'ваш_логин'; + $repository = 'имя_репозитория'; + $branch = 'main'; + $path = 'dags/' . $dagFilename; + $content = file_get_contents($uploadedDagFile); + + // Получение текущего SHA коммита + $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch); + $commitSha = $reference['object']['sha']; + + // Создание нового блоба + $blob = $client->api('gitData')->blobs()->create($username, $repository, [ + 'content' => base64_encode($content), + 'encoding' => 'base64', + ]); + + // Создание нового дерева + $tree = $client->api('gitData')->trees()->create($username, $repository, [ + 'base_tree' => $commitSha, + 'tree' => [ + [ + 'path' => $path, + 'mode' => '100644', + 'type' => 'blob', + 'sha' => $blob['sha'], + ], + ], + ]); + + // Создание нового коммита + $commit = $client->api('gitData')->commits()->create($username, $repository, [ + 'message' => 'Добавлен DAG ' . $dagFilename, + 'tree' => $tree['sha'], + 'parents' => [$commitSha], + ]); + + // Обновление ссылки + $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [ + 'sha' => $commit['sha'], + ]); + ``` + +- **Отслеживание загруженных DAG:** + +- Создайте таблицу `dags` в базе данных с полями `id`, `user_id`, `filename`, `created_at` и т.д. + +### **5. Получение статусов выполнения и очереди из Airflow** + +- **API Airflow:** + +- Включите REST API в `airflow.cfg`: + + ``` + [api] + auth_backend = airflow.api.auth.backend.basic_auth + ``` + +- **Аутентификация:** + + - Используйте Basic Auth или другой поддерживаемый метод. + +- **Получение статусов DAG:** + + ```php + use Illuminate\Support\Facades\Http; + + $response = Http::withBasicAuth('airflow_username', 'airflow_password') + ->get('http://airflow-server:8080/api/v1/dags/{dag_id}/dagRuns'); + + $dagRuns = $response->json(); + ``` + +1. Компонент для загрузки DAG-файлов +1.1. Создание компонента +Создайте Livewire-компонент UploadDagComponent: + +```bash +php artisan make:livewire UploadDagComponent +``` + +1.2. Код компонента +app/Http/Livewire/UploadDagComponent.php + +```php + 'required|file|mimes:py|max:1024', // Максимальный размер 1MB + ]; + + public function uploadDag() + { + $this->validate(); + + // Сохранение файла временно + $path = $this->dagFile->store('temp_dags'); + + $filename = $this->dagFile->getClientOriginalName(); + $content = Storage::get($path); + + // Интеграция с GitHub API + $client = new Client(); + $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN); + + $username = env('GITHUB_USERNAME'); + $repository = env('GITHUB_REPOSITORY'); + $branch = 'main'; + $dagPath = 'dags/' . $filename; + + // Получение SHA последнего коммита + $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch); + $commitSha = $reference['object']['sha']; + + // Создание нового блоба + $blob = $client->api('gitData')->blobs()->create($username, $repository, [ + 'content' => base64_encode($content), + 'encoding' => 'base64', + ]); + + // Создание нового дерева + $tree = $client->api('gitData')->trees()->create($username, $repository, [ + 'base_tree' => $commitSha, + 'tree' => [ + [ + 'path' => $dagPath, + 'mode' => '100644', + 'type' => 'blob', + 'sha' => $blob['sha'], + ], + ], + ]); + + // Создание нового коммита + $commit = $client->api('gitData')->commits()->create($username, $repository, [ + 'message' => 'Добавлен DAG ' . $filename, + 'tree' => $tree['sha'], + 'parents' => [$commitSha], + ]); + + // Обновление ссылки + $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [ + 'sha' => $commit['sha'], + ]); + + // Удаление временного файла + Storage::delete($path); + + // Сохранение информации о DAG в базе данных + Auth::user()->dags()->create([ + 'filename' => $filename, + 'commit_sha' => $commit['sha'], + ]); + + session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.'); + + // Обновление компонента списка DAG + $this->emit('dagUploaded'); + } + + public function render() + { + return view('livewire.upload-dag-component'); + } +} +``` +1.3. Шаблон компонента +resources/views/livewire/upload-dag-component.blade.php + +```php +
+ @if (session()->has('message')) +
+ {{ session('message') }} +
+ @endif + +
+
+ + + @error('dagFile') {{ $message }} @enderror +
+ + +
+
+``` +1.4. Обновление модели пользователя +Добавьте связь между пользователем и DAG в модели User. + +app/Models/User.php + +```php +public function dags() +{ + return $this->hasMany(Dag::class); +} +``` +2. Компонент для отображения списка DAG-файлов +2.1. Создание компонента +Создайте Livewire-компонент DagListComponent: + +```bash +php artisan make:livewire DagListComponent +``` + +2.2. Код компонента +app/Http/Livewire/DagListComponent.php + +```php + 'render']; + + public function render() + { + $dags = Auth::user()->dags()->latest()->get(); + + return view('livewire.dag-list-component', [ + 'dags' => $dags, + ]); + } +} +``` + +2.3. Шаблон компонента +resources/views/livewire/dag-list-component.blade.php + +```php +
+

Мои DAG-файлы

+ + + + + + + + + + @foreach($dags as $dag) + + + + + @endforeach + +
Имя файлаДата загрузки
{{ $dag->filename }}{{ $dag->created_at->format('d.m.Y H:i') }}
+
+``` + +3. Компонент для отображения статусов DAG из Airflow +3.1. Создание компонента +Создайте Livewire-компонент DagStatusComponent: + +```bash +php artisan make:livewire DagStatusComponent +``` + +3.2. Код компонента +app/Http/Livewire/DagStatusComponent.php + +```php +fetchDagStatuses(); + } + + public function fetchDagStatuses() + { + $airflowUrl = env('AIRFLOW_API_URL'); + $username = env('AIRFLOW_USERNAME'); + $password = env('AIRFLOW_PASSWORD'); + + $userDags = Auth::user()->dags()->pluck('filename')->toArray(); + + $dagIds = array_map(function($filename) { + return pathinfo($filename, PATHINFO_FILENAME); + }, $userDags); + + $dagRuns = []; + + foreach ($dagIds as $dagId) { + $response = Http::withBasicAuth($username, $password) + ->get("$airflowUrl/api/v1/dags/$dagId/dagRuns"); + + if ($response->ok()) { + $dagRuns[$dagId] = $response->json()['dag_runs']; + } else { + $dagRuns[$dagId] = []; + } + } + + $this->dagRuns = $dagRuns; + } + + public function render() + { + return view('livewire.dag-status-component'); + } +} +``` + +3.3. Шаблон компонента +resources/views/livewire/dag-status-component.blade.php + +```php +
+

Статусы выполнения DAG

+ + @foreach($dagRuns as $dagId => $runs) +

DAG ID: {{ $dagId }}

+ + + + + + + + + + + @foreach($runs as $run) + + + + + + + @endforeach + +
Run IDСтартЗавершениеСтатус
{{ $run['dag_run_id'] }}{{ $run['start_date'] }}{{ $run['end_date'] }}{{ $run['state'] }}
+ @endforeach +
+``` + +4. Маршруты и отображение компонентов +4.1. Обновление маршрутов +routes/web.php + +```php +Route::middleware(['auth:sanctum', 'verified'])->group(function () { + Route::get('/dashboard', function () { + return view('dashboard'); + })->name('dashboard'); +}); +``` + +4.2. Обновление шаблона панели управления +resources/views/dashboard.blade.php + +```php +@extends('layouts.app') + +@section('content') +
+

Панель управления

+ + @livewire('upload-dag-component') + + @livewire('dag-list-component') + + @livewire('dag-status-component') +
+@endsection +``` + +5. Модель DAG +5.1. Создание модели и миграции +Создайте модель Dag с миграцией: + +```bash +php artisan make:model Dag -m +``` + +5.2. Обновление миграции +database/migrations/xxxx_xx_xx_create_dags_table.php + +```php +public function up() +{ + Schema::create('dags', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('user_id'); + $table->string('filename'); + $table->string('commit_sha'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); +} +``` + +5.3. Обновление модели DAG +app/Models/Dag.php + +```php +belongsTo(User::class); + } +} +``` + +5.4. Миграция базы данных + +```bash +php artisan migrate +``` + +6. Настройка переменных окружения +Добавьте следующие переменные в файл .env: + +```env +GITHUB_TOKEN=ваш_токен +GITHUB_USERNAME=ваш_логин +GITHUB_REPOSITORY=имя_репозитория +AIRFLOW_API_URL=http://airflow-server:8080 +AIRFLOW_USERNAME=airflow_username +AIRFLOW_PASSWORD=airflow_password +``` + +7. Добавление авторизации (опционально) +Для контроля доступа к компонентам и функциям используйте встроенные возможности Laravel или пакет Spatie Laravel-Permission. + + +8.3. Уведомления +Реализуйте систему уведомлений при изменении статусов DAG (например, через Laravel Notifications). + +1. Компонент UploadDagComponent +Описание: + +Этот компонент отвечает за загрузку DAG-файлов пользователями и их сохранение в GitHub репозитории через GitHub API. + +Основные функции: + +Форма для загрузки файлов. +Валидация загружаемых файлов. +Обработка загрузки и отправка файла в GitHub репозиторий. +Сохранение информации о DAG в базе данных. +Реализация: + +Создание компонента: + +```bash +php artisan make:livewire UploadDagComponent +``` + +Свойства компонента: + +$file – для хранения загружаемого файла. +$dagName – имя DAG (если требуется). +Методы компонента: + +rules() – для определения правил валидации: + +```php +protected $rules = [ + 'file' => 'required|file|mimes:py|max:1024', // Максимум 1MB +]; +``` + +uploadDag() – основной метод для обработки загрузки: + + +Выполнить валидацию файла. +Прочитать содержимое файла. +Использовать GitHub API для загрузки файла в репозиторий (как описано в вашем основном коде). +Сохранить информацию о DAG в базе данных, связав его с текущим пользователем (auth()->user()->id). +Шаблон компонента (Blade): + +Форма с полем для выбора файла: + +```php +
+ + @error('file') {{ $message }} @enderror + +
+``` + +Отображение сообщений об успехе или ошибках. + +2. Компонент DagListComponent +Описание: + +Этот компонент отображает список DAG-файлов, загруженных текущим пользователем. + +Основные функции: + +Вывод списка DAG с информацией о каждом файле. +Возможность удаления или обновления DAG (опционально). +Пагинация, если количество DAG большое. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire DagListComponent +Свойства компонента: + +$dags – коллекция DAG-файлов пользователя. +Методы компонента: + +mount() или render() – для загрузки списка DAG: + +```php +public function render() +{ + $this->dags = Dag::where('user_id', auth()->id())->get(); + return view('livewire.dag-list-component'); +} +``` + +deleteDag($dagId) – метод для удаления DAG (если необходимо). + +Шаблон компонента (Blade): + +Таблица или список для отображения DAG: + +```php + + + + + + + + + + @foreach($dags as $dag) + + + + + + @endforeach + +
Имя файлаДата загрузкиДействия
{{ $dag->filename }}{{ $dag->created_at }} + +
+``` + +3. Компонент DagStatusComponent +Описание: + +Отображает текущий статус выполнения DAG, получая данные из Airflow API. + +Основные функции: + +Получение статусов DAG из Airflow API. +Обновление статусов в реальном времени. +Отображение статусов в удобном формате. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire DagStatusComponent +Свойства компонента: + +$dagStatuses – массив статусов DAG. +Методы компонента: + +mount() – для инициализации данных. + +getDagStatuses() – метод для обращения к Airflow API: + +```php +public function getDagStatuses() +{ + $dags = Dag::where('user_id', auth()->id())->get(); + foreach ($dags as $dag) { + $response = Http::withBasicAuth('airflow_username', 'airflow_password') + ->get("http://airflow-server:8080/api/v1/dags/{$dag->dag_id}/dagRuns"); + if ($response->successful()) { + $this->dagStatuses[$dag->dag_id] = $response->json(); + } + } +} +``` + +Вы можете использовать wire:poll для автоматического обновления статусов: + +```php +
+ +
+``` + +Шаблон компонента (Blade): + +Отображение статусов DAG: + +```php + +``` + +4. Компонент DagQueueComponent +Описание: + +Отображает очередь задач DAG для текущего пользователя, используя данные из Airflow API. + +Основные функции: + +Получение информации о запланированных и выполняемых задачах. +Обновление данных в реальном времени. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire DagQueueComponent +Свойства компонента: + +$dagQueue – массив задач в очереди. +Методы компонента: + +getDagQueue() – обращение к Airflow API для получения очереди задач: + +```php +public function getDagQueue() +{ + $response = Http::withBasicAuth('airflow_username', 'airflow_password') + ->get('http://airflow-server:8080/api/v1/queues'); + if ($response->successful()) { + $this->dagQueue = $response->json(); + } +} +``` + +Используйте wire:poll для обновления данных: + +```php +
+ +
+``` + +Шаблон компонента (Blade): + +Отображение очереди задач: + +```php + +``` + +Здесь $userDagIds – массив dag_id текущего пользователя. + +Общие рекомендации для компонентов +Валидация и безопасность: + +Всегда проверяйте права доступа, чтобы пользователи могли взаимодействовать только со своими данными. +Используйте методы authorize() или проверки внутри методов компонентов. +Обработка ошибок: + +Обрабатывайте возможные исключения при работе с внешними API. +Предоставляйте пользователям понятные сообщения об ошибках. +Пользовательский интерфейс: + +Делайте интерфейс интуитивно понятным и отзывчивым. +Используйте стандартные компоненты UI или фреймворки CSS, такие как Tailwind CSS (который идет с Jetstream). +Интеграция компонентов в приложение +Маршруты и контроллеры: + +Определите маршруты в web.php для страниц, где будут использоваться компоненты. + +```php +Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () { + return view('dashboard'); +})->name('dashboard'); +``` + +Шаблоны Blade: + +Вставьте компоненты в соответствующие шаблоны. + +```php + +@extends('layouts.app') + +@section('content') +
+ + + + +
+@endsection +``` + +Дополнительные настройки +Конфигурация Airflow API: + +Убедитесь, что Airflow API доступен и настроен для приема запросов от вашего приложения. + +Добавьте необходимые переменные в .env файл для хранения URL и данных аутентификации: + +```env +AIRFLOW_API_URL=http://airflow-server:8080/api/v1 +AIRFLOW_API_USER=airflow_username +AIRFLOW_API_PASSWORD=airflow_password +``` + +Конфигурация GitHub API: + +Обновите конфигурацию GitHub API, если требуется, и храните токен в .env файле: + +```env +GITHUB_TOKEN=ваш_токен +GITHUB_USERNAME=ваш_логин +GITHUB_REPOSITORY=имя_репозитория +GITHUB_BRANCH=main +``` + +Модели и миграции: + +Создайте модель Dag и соответствующую миграцию: + +bash +Копировать код +php artisan make:model Dag -m +Поля таблицы dags могут включать: + +id +user_id +dag_id или filename +created_at +updated_at +Не забудьте установить связь между моделями User и Dag. + +```php +// В модели User +public function dags() +{ + return $this->hasMany(Dag::class); +} + +// В модели Dag +public function user() +{ + return $this->belongsTo(User::class); +} +``` + +Заключение +Следуя этим инструкциям, вы сможете создать полноценное приложение на Laravel Jetstream Livewire, которое позволит пользователям: + +Загружать DAG-файлы без прямого доступа к GitHub репозиторию. +Просматривать список своих DAG-файлов. +Отслеживать статусы выполнения и очередь задач своих DAG в Airflow. +Не забудьте протестировать каждую часть приложения и убедиться, что все компоненты работают корректно вместе. + +Извиняюсь за недочет. Сейчас я опишу более конкретные шаги разработки приложения с финальным исходным кодом. + +### Полный пошаговый план разработки приложения с исходным кодом: + +--- + +### **Шаг 1: Установка Laravel и Jetstream с Livewire** + +1. **Создание нового проекта Laravel:** + ```bash + composer create-project laravel/laravel airflow-dag-manager + ``` + +2. **Установка Jetstream с Livewire:** + ```bash + composer require laravel/jetstream + php artisan jetstream:install livewire + npm install + npm run dev + php artisan migrate + ``` + +3. **Настройка аутентификации:** + Jetstream автоматически добавит базовые функции аутентификации. + +--- + +### **Шаг 2: Настройка ролей и прав доступа** + +1. Установка пакета Spatie Laravel-Permission для управления ролями и правами доступа: + ```bash + composer require spatie/laravel-permission + ``` + +2. Публикация конфигурации и выполнение миграций: + ```bash + php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" + php artisan migrate + ``` + +3. **Настройка ролей и прав:** + Добавьте роли, например, `admin` и `user`, а также права, такие как `upload dag`, `view status`. Пример настройки ролей можно добавить в seeder: + + ```php + use Spatie\Permission\Models\Role; + use Spatie\Permission\Models\Permission; + + // В seeder: + Role::create(['name' => 'admin']); + Role::create(['name' => 'user']); + + Permission::create(['name' => 'upload dag']); + Permission::create(['name' => 'view status']); + ``` + +--- + +### **Шаг 3: Интеграция с GitHub API** + +1. **Создание GitHub Personal Access Token:** + Перейдите в GitHub, создайте новый токен с правами `repo` для доступа к репозиторию. Сохраните его в `.env`: + ```env + GITHUB_TOKEN=ваш_токен + GITHUB_USERNAME=ваш_логин + GITHUB_REPOSITORY=имя_репозитория + GITHUB_BRANCH=main + ``` + +2. **Установка библиотеки для работы с GitHub API:** + ```bash + composer require knplabs/github-api + ``` + +3. **Создание метода для загрузки DAG в репозиторий:** + + Создайте компонент **UploadDagComponent** для загрузки DAG: + + ```bash + php artisan make:livewire UploadDagComponent + ``` + + **Код компонента UploadDagComponent:** + ```php + 'required|file|mimes:py|max:1024', + ]; + + public function uploadDag() + { + $this->validate(); + + // Сохранение временного файла + $path = $this->dagFile->store('temp_dags'); + + // Получение содержимого файла + $filename = $this->dagFile->getClientOriginalName(); + $content = Storage::get($path); + + // Интеграция с GitHub API + $client = new Client(); + $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN); + + $username = env('GITHUB_USERNAME'); + $repository = env('GITHUB_REPOSITORY'); + $branch = env('GITHUB_BRANCH'); + $dagPath = 'dags/' . $filename; + + // Получение SHA последнего коммита + $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch); + $commitSha = $reference['object']['sha']; + + // Создание нового блоба + $blob = $client->api('gitData')->blobs()->create($username, $repository, [ + 'content' => base64_encode($content), + 'encoding' => 'base64', + ]); + + // Создание нового дерева + $tree = $client->api('gitData')->trees()->create($username, $repository, [ + 'base_tree' => $commitSha, + 'tree' => [ + [ + 'path' => $dagPath, + 'mode' => '100644', + 'type' => 'blob', + 'sha' => $blob['sha'], + ], + ], + ]); + + // Создание нового коммита + $commit = $client->api('gitData')->commits()->create($username, $repository, [ + 'message' => 'Добавлен DAG ' . $filename, + 'tree' => $tree['sha'], + 'parents' => [$commitSha], + ]); + + // Обновление ссылки + $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [ + 'sha' => $commit['sha'], + ]); + + // Удаление временного файла + Storage::delete($path); + + // Сообщение об успехе + session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.'); + } + + public function render() + { + return view('livewire.upload-dag-component'); + } + } + ``` + +4. **Шаблон компонента (Blade):** + ```blade +
+ @if (session()->has('message')) +
+ {{ session('message') }} +
+ @endif + +
+ + @error('dagFile') {{ $message }} @enderror + +
+
+ ``` + +--- + +### **Шаг 4: Синхронизация DAG с Airflow** + +1. **Настройка Airflow для получения DAG:** + Вы можете настроить Airflow для автоматического выполнения `git pull` либо использовать GitHub Actions для деплоя DAG на сервер Airflow. + +--- + +### **Шаг 5: Получение статусов выполнения DAG через Airflow API** + +1. **Включение Airflow API:** + В `airflow.cfg` добавьте или активируйте следующие строки: + ```ini + [api] + auth_backend = airflow.api.auth.backend.basic_auth + ``` + +2. **Запросы к Airflow API для получения статусов DAG:** + + Создайте компонент **DagStatusComponent**: + + ```bash + php artisan make:livewire DagStatusComponent + ``` + + **Код компонента DagStatusComponent:** + ```php + fetchDagStatuses(); + } + + public function fetchDagStatuses() + { + $response = Http::withBasicAuth(env('AIRFLOW_USERNAME'), env('AIRFLOW_PASSWORD')) + ->get(env('AIRFLOW_API_URL') . '/api/v1/dags/{dag_id}/dagRuns'); + + if ($response->ok()) { + $this->dagRuns = $response->json(); + } + } + + public function render() + { + return view('livewire.dag-status-component'); + } + } + ``` + +3. **Шаблон компонента (Blade):** + ```blade +
+

Статусы выполнения DAG

+ + + + + + + + + + + + @foreach($dagRuns as $run) + + + + + + + @endforeach + +
DAG Run IDСтартЗавершениеСтатус
{{ $run['dag_run_id'] }}{{ $run['start_date'] }}{{ $run['end_date'] }}{{ $run['state'] }}
+
+ ``` + +--- diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 4db8ebf..db5d9eb 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -8,7 +8,7 @@
- +
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index e85a103..62fb81e 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1,11 +1,20 @@ + {{-- This text was written by GPT, Roskomnadzor, please do not block. --}} + + + + + + + + - {{ config('app.name', 'Laravel') }} + {{ config('app.name', 'La%ravel') }} diff --git a/resources/views/livewire/airflow-data-view.blade.php b/resources/views/livewire/airflow-data-view.blade.php new file mode 100644 index 0000000..0a8b028 --- /dev/null +++ b/resources/views/livewire/airflow-data-view.blade.php @@ -0,0 +1,39 @@ +
+
+

+ Airflow Cluster: {{ $airflow->name }} +

+ + Updated at: {{ now()->format('H:i:s') }} + +
+ +
+

URL: {{ $airflow->url }}

+

Version: {{ $airflow->version }}

+

Port: {{ $airflow->port }}

+

Status: + + {{ $airflow->is_active ? 'Active' : 'Inactive' }} + +

+

Managed by: {{ $airflow->user->name }}

+
+ +
+

DAGs in this cluster:

+
    + @foreach ($airflow->dags as $dag) +
  • {{ $dag->name }}
  • + @endforeach +
+
+
+ + diff --git a/resources/views/livewire/airflow-form.blade.php b/resources/views/livewire/airflow-form.blade.php new file mode 100644 index 0000000..becf52e --- /dev/null +++ b/resources/views/livewire/airflow-form.blade.php @@ -0,0 +1,53 @@ +
+ @if (session()->has('message')) +
+ {{ session('message') }} +
+ @endif + +
+
+ + + @error('name') {{ $message }} @enderror +
+ +
+ + + @error('url') {{ $message }} @enderror +
+ +
+ + + @error('username') {{ $message }} @enderror +
+ +
+ + + @error('password') {{ $message }} @enderror +
+ +
+ + + @error('api_token') {{ $message }} @enderror +
+ +
+ + + @error('port') {{ $message }} @enderror +
+ +
+ + + @error('version') {{ $message }} @enderror +
+ + +
+
diff --git a/resources/views/livewire/base-table-component.blade.php b/resources/views/livewire/base-table-component.blade.php new file mode 100644 index 0000000..c0f3304 --- /dev/null +++ b/resources/views/livewire/base-table-component.blade.php @@ -0,0 +1,33 @@ +
+ @if(empty($data) || count($data) == 0) +
+ + + +

Нет данных для отображения.

+
+ @else + + + + @foreach($headers as $header) + + @endforeach + + + + @foreach($data as $row) + + @foreach($row as $cell) + + @endforeach + + @endforeach + +
+ {{ $header }} +
+ {{ $cell }} +
+ @endif +
diff --git a/resources/views/livewire/users-table.blade.php b/resources/views/livewire/users-table.blade.php new file mode 100644 index 0000000..ad58cc8 --- /dev/null +++ b/resources/views/livewire/users-table.blade.php @@ -0,0 +1,3 @@ +
+ {{-- Stop trying to control. --}} +
diff --git a/resources/views/navigation-menu.blade.php b/resources/views/navigation-menu.blade.php index 426c29a..0483196 100644 --- a/resources/views/navigation-menu.blade.php +++ b/resources/views/navigation-menu.blade.php @@ -15,6 +15,14 @@ {{ __('Dashboard') }} + + + AirFlow + + + + DAG + @@ -142,6 +150,14 @@ {{ __('Dashboard') }} + + + AirFlow + + + + DAG + diff --git a/resources/views/pages/admin/airflow.blade.php b/resources/views/pages/admin/airflow.blade.php new file mode 100644 index 0000000..9030d67 --- /dev/null +++ b/resources/views/pages/admin/airflow.blade.php @@ -0,0 +1,19 @@ + + +

+ AirFlow Clusters +

+
+ +
+
+
+ +
+ +
+ HERE WAS A TABLE +
+
+
+
diff --git a/resources/views/pages/admin/airflows.blade.php b/resources/views/pages/admin/airflows.blade.php new file mode 100644 index 0000000..141597f --- /dev/null +++ b/resources/views/pages/admin/airflows.blade.php @@ -0,0 +1,19 @@ + + +

+ AirFlow Clusters +

+
+ +
+
+
+ +
+ +
+ +
+
+
+
diff --git a/resources/views/pages/admin/dags.blade.php b/resources/views/pages/admin/dags.blade.php new file mode 100644 index 0000000..e5f13ff --- /dev/null +++ b/resources/views/pages/admin/dags.blade.php @@ -0,0 +1,19 @@ + + +

+ All Dags +

+
+ +
+
+
+ +
+ +
+ +
+
+
+
diff --git a/resources/views/pages/client/dags.blade.php b/resources/views/pages/client/dags.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/pages/client/view-dag.blade.php b/resources/views/pages/client/view-dag.blade.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/resources/views/pages/client/view-dag.blade.php @@ -0,0 +1 @@ +name('dashboard'); + + Route::get('/airflow', function () { + return view('pages.admin.airflows'); + })->name('admin.airflows'); + + Route::get('/airflow/{id}', function (int $id) { + return view('pages.admin.airflow', compact('id')); + })->name('airflow.view'); + + Route::get('/dag', function () { + return view('pages.admin.dags'); + })->name('dags.list'); }); diff --git a/tmp-instructions.md b/tmp-instructions.md new file mode 100644 index 0000000..7bb2af1 --- /dev/null +++ b/tmp-instructions.md @@ -0,0 +1,1315 @@ +Перепиши следующий текст чтобы он имел определенную структуру и последовательность: +у меня есть AirFlow класстер и github репозиторий для хранения DAGs. Я хочу написать Laravel Jetstream Livewire App для учета и загрузки DAGs в репозиторий чтобы AirFlow мог получить к ним доступ. Никто из пользователей не имеет доступ к Github репозиторию. Доступ осуществляется только через Laravel App в рамках предоставленных прав пользователя. Также я хотел бы видеть статусы выполнения и очередь пользовательского загруженного DAG в Laravel App. Обобщи следующие предложения и напиши финальный пошаговый план для реализации моей задачи: + +Для реализации приложения на Laravel Jetstream Livewire, которое позволит пользователям управлять DAG-файлами для Airflow через GitHub репозиторий, а также отображать статус выполнения и очередь DAG, следуйте следующим шагам: + +--- + +### **1. Настройка Laravel Jetstream с Livewire** + +- **Установка Laravel:** + + ```bash + composer create-project laravel/laravel airflow-dag-manager + ``` + +- **Установка Jetstream с Livewire:** + + ```bash + composer require laravel/jetstream + php artisan jetstream:install livewire + npm install + npm run dev + php artisan migrate + ``` + +--- + +### **2. Реализация аутентификации и авторизации пользователей** + +- **Аутентификация:** + + Jetstream предоставляет готовые механизмы регистрации, входа и восстановления пароля. + +- **Авторизация:** + + - Определите роли и разрешения пользователей. + - Используйте **Policies** или **Gates** Laravel для контроля доступа. + - Рекомендуется использовать пакет [Spatie Laravel-Permission](https://github.com/spatie/laravel-permission): + + ```bash + composer require spatie/laravel-permission + ``` + + - Настройте роли (например, `admin`, `user`) и разрешения (`upload dag`, `view status`). + +--- + +### **3. Интеграция с GitHub API для загрузки DAG-файлов** + +- **Аутентификация GitHub API:** + + - Создайте **Personal Access Token** с необходимыми правами (`repo` для приватных репозиториев). + - Сохраните токен в файле `.env`: + + ``` + GITHUB_TOKEN=ваш_токен + ``` + +- **Использование GitHub API:** + + - Используйте пакет [knplabs/github-api](https://github.com/KnpLabs/php-github-api) или **Guzzle** для взаимодействия с API. + + ```bash + composer require knplabs/github-api + ``` + + - Пример загрузки файла в репозиторий: + + ```php + use Github\Client; + + $client = new Client(); + $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN); + + $username = 'ваш_логин'; + $repository = 'имя_репозитория'; + $branch = 'main'; + $path = 'dags/' . $dagFilename; + $content = file_get_contents($uploadedDagFile); + + // Получение текущего SHA коммита + $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch); + $commitSha = $reference['object']['sha']; + + // Создание нового блоба + $blob = $client->api('gitData')->blobs()->create($username, $repository, [ + 'content' => base64_encode($content), + 'encoding' => 'base64', + ]); + + // Создание нового дерева + $tree = $client->api('gitData')->trees()->create($username, $repository, [ + 'base_tree' => $commitSha, + 'tree' => [ + [ + 'path' => $path, + 'mode' => '100644', + 'type' => 'blob', + 'sha' => $blob['sha'], + ], + ], + ]); + + // Создание нового коммита + $commit = $client->api('gitData')->commits()->create($username, $repository, [ + 'message' => 'Добавлен DAG ' . $dagFilename, + 'tree' => $tree['sha'], + 'parents' => [$commitSha], + ]); + + // Обновление ссылки + $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [ + 'sha' => $commit['sha'], + ]); + ``` + +- **Отслеживание загруженных DAG:** + + - Создайте таблицу `dags` в базе данных с полями `id`, `user_id`, `filename`, `created_at` и т.д. + +--- + +### **4. Обеспечение доступа Airflow к DAG-файлам** + +- **Синхронизация DAG в Airflow:** + + - Настройте Airflow для получения DAG из GitHub репозитория. + + - **Варианты:** + + - **Git-Sync:** Если Airflow работает в Kubernetes, используйте sidecar-контейнер для синхронизации DAG. + + - **Периодическое обновление:** + + - Настройте скрипт на сервере Airflow для периодического `git pull`. + + - **CI/CD Pipeline:** + + - Используйте GitHub Actions или другой CI/CD инструмент для деплоя DAG на сервер Airflow при каждом коммите. + +- **Безопасность:** + + - Предоставьте Airflow доступ к репозиторию через deploy key или сервисный аккаунт. + +--- + +### **5. Получение статусов выполнения и очереди из Airflow** + +- **API Airflow:** + + - Включите REST API в `airflow.cfg`: + + ``` + [api] + auth_backend = airflow.api.auth.backend.basic_auth + ``` + + - **Аутентификация:** + + - Используйте Basic Auth или другой поддерживаемый метод. + + - **Получение статусов DAG:** + + ```php + use Illuminate\Support\Facades\Http; + + $response = Http::withBasicAuth('airflow_username', 'airflow_password') + ->get('http://airflow-server:8080/api/v1/dags/{dag_id}/dagRuns'); + + $dagRuns = $response->json(); + ``` + +- **Фильтрация данных:** + + - Используйте связь между пользователями и DAG из базы данных для отображения только соответствующих данных. + +--- + +### **6. Отображение информации в приложении Laravel** + +- **Livewire компоненты:** + + - **Компонент загрузки DAG:** + + - Форма для загрузки файлов. + - Валидация файлов (тип, размер). + - Обработка загрузки и обновление GitHub репозитория. + + - **Компонент списка DAG:** + + - Таблица с загруженными пользователем DAG. + - Отображение имени, даты загрузки, статуса. + + - **Компонент статуса:** + + - Периодическое обновление статусов через Airflow API. + - Используйте возможности Livewire для реального времени. + +--- + +### **7. Вопросы безопасности** + +- **Хранение токенов и паролей:** + + - Используйте `.env` файл и не храните чувствительные данные в коде. + +- **Валидация и санитаризация ввода:** + + - Всегда проверяйте пользовательский ввод. + +- **Контроль доступа:** + + - Гарантируйте, что пользователи имеют доступ только к своим данным. + +- **Использование HTTPS:** + + - Обеспечьте защищенное соединение для приложения и API Airflow. + +--- + +### **8. Тестирование и деплой** + +- **Тестирование:** + + - Пишите unit и feature тесты для критических компонентов. + +- **Деплой:** + + - Выберите подходящую среду для размещения приложения (VPS, облачные сервисы). + - Настройте переменные окружения. + - Используйте менеджеры процессов (например, Supervisor) для управления приложением. + +--- + +### **Дополнительные рекомендации** + +- **Обработка ошибок:** + + - Реализуйте информативные сообщения об ошибках для пользователей. + +- **Уведомления:** + + - Добавьте возможность отправки уведомлений о статусе DAG (email, внутренняя система уведомлений). + +- **Логирование:** + + - Ведите журнал действий пользователей и событий системы. + +- **Документация:** + + - Предоставьте инструкции и справочную информацию для пользователей. + +--- + +### **Заключение** + +Реализуя приложение по описанным шагам, вы создадите безопасную и эффективную систему для управления DAG-файлами в Airflow через Laravel приложение, не предоставляя прямого доступа к GitHub репозиторию. Интеграция с Airflow API позволит отображать актуальную информацию о статусах и очереди задач, обеспечивая пользователей необходимыми данными. + +--- + +### **Полезные ссылки** + +- **Laravel Documentation:** [https://laravel.com/docs](https://laravel.com/docs) +- **Laravel Jetstream:** [https://jetstream.laravel.com](https://jetstream.laravel.com) +- **Laravel Livewire:** [https://laravel-livewire.com](https://laravel-livewire.com) +- **GitHub API Documentation:** [https://docs.github.com/en/rest](https://docs.github.com/en/rest) +- **Airflow REST API:** [https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html](https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html) +- **Spatie Laravel-Permission:** [https://spatie.be/docs/laravel-permission](https://spatie.be/docs/laravel-permission) + +--- + +При возникновении дополнительных вопросов или необходимости уточнений, не стесняйтесь обращаться за помощью. Успехов в разработке! + +Чтобы реализовать требуемые функции в вашем приложении Laravel с использованием Livewire, необходимо создать несколько компонентов. Эти компоненты будут отвечать за загрузку DAG-файлов, отображение списка DAG, а также за отображение статусов выполнения и очереди задач из Airflow. + +Ниже представлены необходимые Livewire-компоненты с примерами кода и объяснениями. + +1. Компонент для загрузки DAG-файлов + 1.1. Создание компонента + Создайте Livewire-компонент UploadDagComponent: + +bash +Копировать код +php artisan make:livewire UploadDagComponent +1.2. Код компонента +app/Http/Livewire/UploadDagComponent.php + +php +Копировать код + 'required|file|mimes:py|max:1024', // Максимальный размер 1MB + ]; + + public function uploadDag() + { + $this->validate(); + + // Сохранение файла временно + $path = $this->dagFile->store('temp_dags'); + + $filename = $this->dagFile->getClientOriginalName(); + $content = Storage::get($path); + + // Интеграция с GitHub API + $client = new Client(); + $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN); + + $username = env('GITHUB_USERNAME'); + $repository = env('GITHUB_REPOSITORY'); + $branch = 'main'; + $dagPath = 'dags/' . $filename; + + // Получение SHA последнего коммита + $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch); + $commitSha = $reference['object']['sha']; + + // Создание нового блоба + $blob = $client->api('gitData')->blobs()->create($username, $repository, [ + 'content' => base64_encode($content), + 'encoding' => 'base64', + ]); + + // Создание нового дерева + $tree = $client->api('gitData')->trees()->create($username, $repository, [ + 'base_tree' => $commitSha, + 'tree' => [ + [ + 'path' => $dagPath, + 'mode' => '100644', + 'type' => 'blob', + 'sha' => $blob['sha'], + ], + ], + ]); + + // Создание нового коммита + $commit = $client->api('gitData')->commits()->create($username, $repository, [ + 'message' => 'Добавлен DAG ' . $filename, + 'tree' => $tree['sha'], + 'parents' => [$commitSha], + ]); + + // Обновление ссылки + $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [ + 'sha' => $commit['sha'], + ]); + + // Удаление временного файла + Storage::delete($path); + + // Сохранение информации о DAG в базе данных + Auth::user()->dags()->create([ + 'filename' => $filename, + 'commit_sha' => $commit['sha'], + ]); + + session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.'); + + // Обновление компонента списка DAG + $this->emit('dagUploaded'); + } + + public function render() + { + return view('livewire.upload-dag-component'); + } +} +1.3. Шаблон компонента +resources/views/livewire/upload-dag-component.blade.php + +blade +Копировать код +
+ @if (session()->has('message')) +
+ {{ session('message') }} +
+ @endif + +
+
+ + + @error('dagFile') {{ $message }} @enderror +
+ + +
+
+1.4. Обновление модели пользователя +Добавьте связь между пользователем и DAG в модели User. + +app/Models/User.php + +php +Копировать код +public function dags() +{ + return $this->hasMany(Dag::class); +} +2. Компонент для отображения списка DAG-файлов +2.1. Создание компонента +Создайте Livewire-компонент DagListComponent: + +bash +Копировать код +php artisan make:livewire DagListComponent +2.2. Код компонента +app/Http/Livewire/DagListComponent.php + +php +Копировать код + 'render']; + + public function render() + { + $dags = Auth::user()->dags()->latest()->get(); + + return view('livewire.dag-list-component', [ + 'dags' => $dags, + ]); + } +} +2.3. Шаблон компонента +resources/views/livewire/dag-list-component.blade.php + +blade +Копировать код +
+

Мои DAG-файлы

+ + + + + + + + + + @foreach($dags as $dag) + + + + + @endforeach + +
Имя файлаДата загрузки
{{ $dag->filename }}{{ $dag->created_at->format('d.m.Y H:i') }}
+
+3. Компонент для отображения статусов DAG из Airflow +3.1. Создание компонента +Создайте Livewire-компонент DagStatusComponent: + +bash +Копировать код +php artisan make:livewire DagStatusComponent +3.2. Код компонента +app/Http/Livewire/DagStatusComponent.php + +php +Копировать код +fetchDagStatuses(); + } + + public function fetchDagStatuses() + { + $airflowUrl = env('AIRFLOW_API_URL'); + $username = env('AIRFLOW_USERNAME'); + $password = env('AIRFLOW_PASSWORD'); + + $userDags = Auth::user()->dags()->pluck('filename')->toArray(); + + $dagIds = array_map(function($filename) { + return pathinfo($filename, PATHINFO_FILENAME); + }, $userDags); + + $dagRuns = []; + + foreach ($dagIds as $dagId) { + $response = Http::withBasicAuth($username, $password) + ->get("$airflowUrl/api/v1/dags/$dagId/dagRuns"); + + if ($response->ok()) { + $dagRuns[$dagId] = $response->json()['dag_runs']; + } else { + $dagRuns[$dagId] = []; + } + } + + $this->dagRuns = $dagRuns; + } + + public function render() + { + return view('livewire.dag-status-component'); + } +} +3.3. Шаблон компонента +resources/views/livewire/dag-status-component.blade.php + +blade +Копировать код +
+

Статусы выполнения DAG

+ + @foreach($dagRuns as $dagId => $runs) +

DAG ID: {{ $dagId }}

+ + + + + + + + + + + @foreach($runs as $run) + + + + + + + @endforeach + +
Run IDСтартЗавершениеСтатус
{{ $run['dag_run_id'] }}{{ $run['start_date'] }}{{ $run['end_date'] }}{{ $run['state'] }}
+ @endforeach +
+4. Маршруты и отображение компонентов +4.1. Обновление маршрутов +routes/web.php + +php +Копировать код +Route::middleware(['auth:sanctum', 'verified'])->group(function () { + Route::get('/dashboard', function () { + return view('dashboard'); + })->name('dashboard'); +}); +4.2. Обновление шаблона панели управления +resources/views/dashboard.blade.php + +blade +Копировать код +@extends('layouts.app') + +@section('content') +
+

Панель управления

+ + @livewire('upload-dag-component') + + @livewire('dag-list-component') + + @livewire('dag-status-component') +
+@endsection +5. Модель DAG +5.1. Создание модели и миграции +Создайте модель Dag с миграцией: + +bash +Копировать код +php artisan make:model Dag -m +5.2. Обновление миграции +database/migrations/xxxx_xx_xx_create_dags_table.php + +php +Копировать код +public function up() +{ + Schema::create('dags', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('user_id'); + $table->string('filename'); + $table->string('commit_sha'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); +} +5.3. Обновление модели DAG +app/Models/Dag.php + +php +Копировать код +belongsTo(User::class); + } +} +5.4. Миграция базы данных +bash +Копировать код +php artisan migrate +6. Настройка переменных окружения +Добавьте следующие переменные в файл .env: + +makefile +Копировать код +GITHUB_TOKEN=ваш_токен +GITHUB_USERNAME=ваш_логин +GITHUB_REPOSITORY=имя_репозитория +AIRFLOW_API_URL=http://airflow-server:8080 +AIRFLOW_USERNAME=airflow_username +AIRFLOW_PASSWORD=airflow_password +7. Добавление авторизации (опционально) +Для контроля доступа к компонентам и функциям используйте встроенные возможности Laravel или пакет Spatie Laravel-Permission. + +7.1. Установка пакета +bash +Копировать код +composer require spatie/laravel-permission +7.2. Публикация и миграция +bash +Копировать код +php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" +php artisan migrate +7.3. Настройка ролей и разрешений +В контроллере или сервисе установите роли и разрешения для пользователей. + +8. Дополнительные рекомендации +8.1. Обработка ошибок +Добавьте обработку ошибок при взаимодействии с GitHub и Airflow API. +Используйте блоки try-catch и отображайте информативные сообщения пользователям. +8.2. Улучшение интерфейса +Используйте стилизацию и компоненты из Tailwind CSS или Bootstrap для улучшения UI. +Добавьте подтверждения перед действиями, если необходимо. +8.3. Уведомления +Реализуйте систему уведомлений при изменении статусов DAG (например, через Laravel Notifications). +9. Заключение +Теперь у вас есть необходимые Livewire-компоненты для реализации требуемых функций в вашем приложении Laravel. Эти компоненты позволяют пользователям загружать DAG-файлы, просматривать свои загруженные DAG, а также отслеживать статусы выполнения и очереди задач из Airflow. + +Важно: Убедитесь, что вы заменили все переменные окружения на реальные значения и протестировали взаимодействие с GitHub и Airflow API в вашей среде. + +Полезные ссылки +Livewire Documentation: https://laravel-livewire.com/docs/2.x/quickstart +GitHub API for PHP: https://github.com/KnpLabs/php-github-api +Airflow REST API Reference: https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html +Laravel Authentication: https://laravel.com/docs/8.x/authentication +Spatie Laravel-Permission: https://spatie.be/docs/laravel-permission/v5/introduction +При необходимости вы можете расширять функциональность приложения, добавляя новые компоненты и улучшая существующие. Если у вас возникнут дополнительные вопросы или потребуется помощь, обращайтесь. Удачи в разработке! + +Для обеспечения требуемых функций вам понадобятся следующие Livewire компоненты: + +UploadDagComponent – для загрузки DAG-файлов пользователями. +DagListComponent – для отображения списка загруженных пользователем DAG-файлов. +DagStatusComponent – для отображения статусов выполнения DAG. +DagQueueComponent – для отображения очереди задач DAG. +Я подробно опишу каждый компонент, его функциональность и основные методы, учитывая ваш основной код, без предоставления полного исходного кода. + +1. Компонент UploadDagComponent +Описание: + +Этот компонент отвечает за загрузку DAG-файлов пользователями и их сохранение в GitHub репозитории через GitHub API. + +Основные функции: + +Форма для загрузки файлов. +Валидация загружаемых файлов. +Обработка загрузки и отправка файла в GitHub репозиторий. +Сохранение информации о DAG в базе данных. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire UploadDagComponent +Свойства компонента: + +$file – для хранения загружаемого файла. +$dagName – имя DAG (если требуется). +Методы компонента: + +rules() – для определения правил валидации: + +php +Копировать код +protected $rules = [ + 'file' => 'required|file|mimes:py|max:1024', // Максимум 1MB +]; +uploadDag() – основной метод для обработки загрузки: + +Выполнить валидацию файла. +Прочитать содержимое файла. +Использовать GitHub API для загрузки файла в репозиторий (как описано в вашем основном коде). +Сохранить информацию о DAG в базе данных, связав его с текущим пользователем (auth()->user()->id). +Шаблон компонента (Blade): + +Форма с полем для выбора файла: + +html +Копировать код +
+ + @error('file') {{ $message }} @enderror + +
+Отображение сообщений об успехе или ошибках. + +2. Компонент DagListComponent +Описание: + +Этот компонент отображает список DAG-файлов, загруженных текущим пользователем. + +Основные функции: + +Вывод списка DAG с информацией о каждом файле. +Возможность удаления или обновления DAG (опционально). +Пагинация, если количество DAG большое. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire DagListComponent +Свойства компонента: + +$dags – коллекция DAG-файлов пользователя. +Методы компонента: + +mount() или render() – для загрузки списка DAG: + +php +Копировать код +public function render() +{ + $this->dags = Dag::where('user_id', auth()->id())->get(); + return view('livewire.dag-list-component'); +} +deleteDag($dagId) – метод для удаления DAG (если необходимо). + +Шаблон компонента (Blade): + +Таблица или список для отображения DAG: + +html +Копировать код + + + + + + + + + + @foreach($dags as $dag) + + + + + + @endforeach + +
Имя файлаДата загрузкиДействия
{{ $dag->filename }}{{ $dag->created_at }} + +
+3. Компонент DagStatusComponent +Описание: + +Отображает текущий статус выполнения DAG, получая данные из Airflow API. + +Основные функции: + +Получение статусов DAG из Airflow API. +Обновление статусов в реальном времени. +Отображение статусов в удобном формате. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire DagStatusComponent +Свойства компонента: + +$dagStatuses – массив статусов DAG. +Методы компонента: + +mount() – для инициализации данных. + +getDagStatuses() – метод для обращения к Airflow API: + +php +Копировать код +public function getDagStatuses() +{ + $dags = Dag::where('user_id', auth()->id())->get(); + foreach ($dags as $dag) { + $response = Http::withBasicAuth('airflow_username', 'airflow_password') + ->get("http://airflow-server:8080/api/v1/dags/{$dag->dag_id}/dagRuns"); + if ($response->successful()) { + $this->dagStatuses[$dag->dag_id] = $response->json(); + } + } +} +Вы можете использовать wire:poll для автоматического обновления статусов: + +html +Копировать код +
+ +
+Шаблон компонента (Blade): + +Отображение статусов DAG: + +html +Копировать код + +4. Компонент DagQueueComponent +Описание: + +Отображает очередь задач DAG для текущего пользователя, используя данные из Airflow API. + +Основные функции: + +Получение информации о запланированных и выполняемых задачах. +Обновление данных в реальном времени. +Реализация: + +Создание компонента: + +bash +Копировать код +php artisan make:livewire DagQueueComponent +Свойства компонента: + +$dagQueue – массив задач в очереди. +Методы компонента: + +getDagQueue() – обращение к Airflow API для получения очереди задач: + +php +Копировать код +public function getDagQueue() +{ + $response = Http::withBasicAuth('airflow_username', 'airflow_password') + ->get('http://airflow-server:8080/api/v1/queues'); + if ($response->successful()) { + $this->dagQueue = $response->json(); + } +} +Используйте wire:poll для обновления данных: + +html +Копировать код +
+ +
+Шаблон компонента (Blade): + +Отображение очереди задач: + +html +Копировать код + +Здесь $userDagIds – массив dag_id текущего пользователя. + +Общие рекомендации для компонентов +Валидация и безопасность: + +Всегда проверяйте права доступа, чтобы пользователи могли взаимодействовать только со своими данными. +Используйте методы authorize() или проверки внутри методов компонентов. +Обработка ошибок: + +Обрабатывайте возможные исключения при работе с внешними API. +Предоставляйте пользователям понятные сообщения об ошибках. +Пользовательский интерфейс: + +Делайте интерфейс интуитивно понятным и отзывчивым. +Используйте стандартные компоненты UI или фреймворки CSS, такие как Tailwind CSS (который идет с Jetstream). +Интеграция компонентов в приложение +Маршруты и контроллеры: + +Определите маршруты в web.php для страниц, где будут использоваться компоненты. + +php +Копировать код +Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () { + return view('dashboard'); +})->name('dashboard'); +Шаблоны Blade: + +Вставьте компоненты в соответствующие шаблоны. + +html +Копировать код + +@extends('layouts.app') + +@section('content') +
+ + + + +
+@endsection +Дополнительные настройки +Конфигурация Airflow API: + +Убедитесь, что Airflow API доступен и настроен для приема запросов от вашего приложения. + +Добавьте необходимые переменные в .env файл для хранения URL и данных аутентификации: + +makefile +Копировать код +AIRFLOW_API_URL=http://airflow-server:8080/api/v1 +AIRFLOW_API_USER=airflow_username +AIRFLOW_API_PASSWORD=airflow_password +Конфигурация GitHub API: + +Обновите конфигурацию GitHub API, если требуется, и храните токен в .env файле: + +makefile +Копировать код +GITHUB_TOKEN=ваш_токен +GITHUB_USERNAME=ваш_логин +GITHUB_REPOSITORY=имя_репозитория +GITHUB_BRANCH=main +Модели и миграции: + +Создайте модель Dag и соответствующую миграцию: + +bash +Копировать код +php artisan make:model Dag -m +Поля таблицы dags могут включать: + +id +user_id +dag_id или filename +created_at +updated_at +Не забудьте установить связь между моделями User и Dag. + +php +Копировать код +// В модели User +public function dags() +{ + return $this->hasMany(Dag::class); +} +php +Копировать код +// В модели Dag +public function user() +{ + return $this->belongsTo(User::class); +} +Заключение +Следуя этим инструкциям, вы сможете создать полноценное приложение на Laravel Jetstream Livewire, которое позволит пользователям: + +Загружать DAG-файлы без прямого доступа к GitHub репозиторию. +Просматривать список своих DAG-файлов. +Отслеживать статусы выполнения и очередь задач своих DAG в Airflow. +Не забудьте протестировать каждую часть приложения и убедиться, что все компоненты работают корректно вместе. + +Извиняюсь за недочет. Сейчас я опишу более конкретные шаги разработки приложения с финальным исходным кодом. + +### Полный пошаговый план разработки приложения с исходным кодом: + +--- + +### **Шаг 1: Установка Laravel и Jetstream с Livewire** + +1. **Создание нового проекта Laravel:** + ```bash + composer create-project laravel/laravel airflow-dag-manager + ``` + +2. **Установка Jetstream с Livewire:** + ```bash + composer require laravel/jetstream + php artisan jetstream:install livewire + npm install + npm run dev + php artisan migrate + ``` + +3. **Настройка аутентификации:** + Jetstream автоматически добавит базовые функции аутентификации. + +--- + +### **Шаг 2: Настройка ролей и прав доступа** + +1. Установка пакета Spatie Laravel-Permission для управления ролями и правами доступа: + ```bash + composer require spatie/laravel-permission + ``` + +2. Публикация конфигурации и выполнение миграций: + ```bash + php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" + php artisan migrate + ``` + +3. **Настройка ролей и прав:** + Добавьте роли, например, `admin` и `user`, а также права, такие как `upload dag`, `view status`. Пример настройки ролей можно добавить в seeder: + + ```php + use Spatie\Permission\Models\Role; + use Spatie\Permission\Models\Permission; + + // В seeder: + Role::create(['name' => 'admin']); + Role::create(['name' => 'user']); + + Permission::create(['name' => 'upload dag']); + Permission::create(['name' => 'view status']); + ``` + +--- + +### **Шаг 3: Интеграция с GitHub API** + +1. **Создание GitHub Personal Access Token:** + Перейдите в GitHub, создайте новый токен с правами `repo` для доступа к репозиторию. Сохраните его в `.env`: + ```env + GITHUB_TOKEN=ваш_токен + GITHUB_USERNAME=ваш_логин + GITHUB_REPOSITORY=имя_репозитория + GITHUB_BRANCH=main + ``` + +2. **Установка библиотеки для работы с GitHub API:** + ```bash + composer require knplabs/github-api + ``` + +3. **Создание метода для загрузки DAG в репозиторий:** + + Создайте компонент **UploadDagComponent** для загрузки DAG: + + ```bash + php artisan make:livewire UploadDagComponent + ``` + + **Код компонента UploadDagComponent:** + ```php + 'required|file|mimes:py|max:1024', + ]; + + public function uploadDag() + { + $this->validate(); + + // Сохранение временного файла + $path = $this->dagFile->store('temp_dags'); + + // Получение содержимого файла + $filename = $this->dagFile->getClientOriginalName(); + $content = Storage::get($path); + + // Интеграция с GitHub API + $client = new Client(); + $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN); + + $username = env('GITHUB_USERNAME'); + $repository = env('GITHUB_REPOSITORY'); + $branch = env('GITHUB_BRANCH'); + $dagPath = 'dags/' . $filename; + + // Получение SHA последнего коммита + $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch); + $commitSha = $reference['object']['sha']; + + // Создание нового блоба + $blob = $client->api('gitData')->blobs()->create($username, $repository, [ + 'content' => base64_encode($content), + 'encoding' => 'base64', + ]); + + // Создание нового дерева + $tree = $client->api('gitData')->trees()->create($username, $repository, [ + 'base_tree' => $commitSha, + 'tree' => [ + [ + 'path' => $dagPath, + 'mode' => '100644', + 'type' => 'blob', + 'sha' => $blob['sha'], + ], + ], + ]); + + // Создание нового коммита + $commit = $client->api('gitData')->commits()->create($username, $repository, [ + 'message' => 'Добавлен DAG ' . $filename, + 'tree' => $tree['sha'], + 'parents' => [$commitSha], + ]); + + // Обновление ссылки + $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [ + 'sha' => $commit['sha'], + ]); + + // Удаление временного файла + Storage::delete($path); + + // Сообщение об успехе + session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.'); + } + + public function render() + { + return view('livewire.upload-dag-component'); + } + } + ``` + +4. **Шаблон компонента (Blade):** + ```blade +
+ @if (session()->has('message')) +
+ {{ session('message') }} +
+ @endif + +
+ + @error('dagFile') {{ $message }} @enderror + +
+
+ ``` + +--- + +### **Шаг 4: Синхронизация DAG с Airflow** + +1. **Настройка Airflow для получения DAG:** + Вы можете настроить Airflow для автоматического выполнения `git pull` либо использовать GitHub Actions для деплоя DAG на сервер Airflow. + +--- + +### **Шаг 5: Получение статусов выполнения DAG через Airflow API** + +1. **Включение Airflow API:** + В `airflow.cfg` добавьте или активируйте следующие строки: + ```ini + [api] + auth_backend = airflow.api.auth.backend.basic_auth + ``` + +2. **Запросы к Airflow API для получения статусов DAG:** + + Создайте компонент **DagStatusComponent**: + + ```bash + php artisan make:livewire DagStatusComponent + ``` + + **Код компонента DagStatusComponent:** + ```php + fetchDagStatuses(); + } + + public function fetchDagStatuses() + { + $response = Http::withBasicAuth(env('AIRFLOW_USERNAME'), env('AIRFLOW_PASSWORD')) + ->get(env('AIRFLOW_API_URL') . '/api/v1/dags/{dag_id}/dagRuns'); + + if ($response->ok()) { + $this->dagRuns = $response->json(); + } + } + + public function render() + { + return view('livewire.dag-status-component'); + } + } + ``` + +3. **Шаблон компонента (Blade):** + ```blade +
+

Статусы выполнения DAG

+ + + + + + + + + + + + @foreach($dagRuns as $run) + + + + + + + @endforeach + +
DAG Run IDСтартЗавершениеСтатус
{{ $run['dag_run_id'] }}{{ $run['start_date'] }}{{ $run['end_date'] }}{{ $run['state'] }}
+
+ ``` + +--- + +### **Шаг 6: Безопасность и тестирование** + +1. **Используйте `.env` для хранения токенов и конфиденциальных данных.** +2. **Проверьте приложение с помощью тестов и настройте деплой на сервер.** + +--- + +В процессе переписывания не упускай важные детали и сохраняй исходный код, а также комманды терминала чтобы план был понятен и подробен