Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 130
CommandController
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
1260
0.00% covered (danger)
0.00%
0 / 130
 index
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 store
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 show
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 update
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 destroy
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 validateData
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 stopCommands
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 35
 getArtisanCommands
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 17
 execCommand
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 12
 getSignatures
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 15
 getSignature
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getMonitoringHistory
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 23
 serviceScaleUp
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
1<?php
2
3namespace Qmp\Laravel\ApiGateway\Controllers\Monitoring;
4
5use Illuminate\Database\Eloquent\Collection as EloquentCollection;
6use Illuminate\Http\Request;
7use Illuminate\Http\JsonResponse;
8use Illuminate\Http\Response;
9use Illuminate\Support\Carbon;
10use Illuminate\Support\Facades\Artisan;
11use Illuminate\Support\Collection;
12use Illuminate\Support\Facades\Cache;
13use Illuminate\Support\Facades\DB;
14use Illuminate\Support\Facades\Log;
15use Illuminate\Support\Str;
16use Illuminate\Validation\Rule;
17use Qmp\Laravel\ApiGateway\Controllers\AbstractApiController;
18use Qmp\Laravel\CommandsLaravel\Models\CommandMonitoring;
19use Qmp\Laravel\CommandsLaravel\Models\Cron;
20use Qmp\Laravel\MicroService\Client\Client;
21use Qmp\Laravel\MicroService\Client\Tools\Request as ClientRequest;
22use ReflectionException;
23
24class CommandController extends AbstractApiController
25{
26    const CACHE_KEY_SIGNATURES = 'artisan-commands-signatures';
27    const SUBDAYS_MONITORING = 1;
28
29    /**
30     * @return EloquentCollection|Cron[]
31     */
32    public function index(): EloquentCollection
33    {
34        return Cron::all();
35    }
36
37    /**
38     * Store a newly created resource in storage.
39     *
40     * @param Request $request
41     * @return JsonResponse
42     */
43    public function store(Request $request): JsonResponse
44    {
45
46        $validatedData = array_merge($this->validateData($request), ['prod' => env('PROD')]);
47        Cron::create($validatedData);
48
49        return response()->json(['status' => 'ok']);
50    }
51
52    /**
53     * Display the specified resource.
54     *
55     * @param Cron
56     * @return JsonResponse
57     */
58    public function show(Cron $cron): JsonResponse
59    {
60        return response()->json([
61            'cron' => $cron->toArray()
62        ]);
63    }
64
65    /**
66     * Update the specified resource in storage.
67     *
68     * @param Request $request
69     * @param $id
70     * @return JsonResponse
71     */
72    public function update(Request $request, $id): JsonResponse
73    {
74
75        $validatedData = array_merge($this->validateData($request, $id), ['prod' => env('PROD')]);
76
77        $model = Cron::findOrFail($id);
78        $result = $model->update($validatedData);
79
80        return response()->json(['status' => $result ? 'ok' : 'ko']);
81
82    }
83
84    /**
85     * Remove the specified resource from storage.
86     *
87     * @param $id
88     * @return JsonResponse
89     */
90    public function destroy(Cron $cron): JsonResponse
91    {
92        $result = $cron->delete();
93
94        return response()->json(['status' => $result ? 'ok' : 'ko']);
95    }
96
97    /**
98     * @param Request $request
99     * @param null $id
100     * @return array
101     */
102    protected function validateData(Request $request, $id = null)
103    {
104        $rules = [
105            'name' => 'required|string',
106            'service' => 'required|string',
107            'type' => 'required|string|in:cron,queue,other',
108            'pattern' => 'nullable|string'
109        ];
110
111        if ($id != null) {
112            $rules['id'] = [
113                'required',
114                Rule::in([$id])
115            ];
116        }
117
118        $data = $request->validate($rules);
119        if (empty($data['pattern'])) {
120            $data['pattern'] = '* * * * *';
121        }
122
123        return $data;
124    }
125
126    /**
127     * @param Request $request
128     * @return JsonResponse
129     */
130    public function stopCommands(string $action, Request $request): jsonResponse
131    {
132        $request->merge(['action' => $request->route('action')]);
133
134        $request->validate([
135            'command_ids' => 'required|array',
136            'action' => 'required|in:stop,run,pause,play'
137        ]);
138
139        switch ($action) {
140            case 'stop' :
141                DB::table('crons')->whereIn('id', $request->command_ids)->update(['active' => 0]);
142                break;
143
144            case 'run' :
145                $commands = Cron::whereIn('id', $request->command_ids)->get();
146                $services = $commands->filter(function ($command) {
147                    $command->active = 1;
148                    $command->save();
149
150                    return $command->type == 'queue' && $command->pause == 0;
151
152                })->map(function ($command) {
153                    return [$command->service => 1];
154                })->collapse()->toArray();
155
156                if (count($services) > 0) {
157                    $this->serviceScaleUp($services);
158                }
159                break;
160
161            case 'pause' :
162                DB::table('crons')->whereIn('id', $request->command_ids)->update(['pause' => 1]);
163                break;
164
165            case 'play' :
166                $commands = Cron::whereIn('id', $request->command_ids)->get();
167                $services = $commands->filter(function ($command) {
168                    $command->pause = 0;
169                    $command->save();
170
171                    return $command->type == 'queue' && $command->active = 1;
172
173                })->map(function ($command) {
174                    return [$command->service => 1];
175                })->collapse()->toArray();
176
177                if (count($services) > 0) {
178                    $this->serviceScaleUp($services);
179                }
180                break;
181        }
182
183        return response()->json(['status' => 'ok']);
184
185    }
186
187    /**
188     * Get artisan commands with signature
189     *
190     * @return Collection
191     */
192    public function getArtisanCommands(): Collection
193    {
194        return Cron::all()->map(function ($command) {
195            $signature = $this->getSignature($command->name);
196            $history = $this->getMonitoringHistory($command);
197
198            return [
199                'id' => $command->id,
200                'name' => $command->name,
201                'signature' => $signature,
202                'service' => $command->service,
203                'type' => $command->type,
204                'pattern' => $command->pattern,
205                'state' => $command->active,
206                'pause' => $command->pause,
207                'last_run' => $history->last_run,
208                'is_ok' => $history->is_ok,
209                'is_running' => $command->running,
210                'warnings' => $history->warnings,
211                'errors' => $history->errors
212            ];
213        });
214    }
215
216    /**
217     * @param Request $request
218     * @return JsonResponse
219     */
220    public function execCommand(Request $request): JsonResponse
221    {
222        $request->validate([
223            'id' => 'required',
224            'command' => 'required|string',
225            'service' => 'required|string'
226        ]);
227
228        try {
229
230            $clientRequest = ClientRequest::createObject('service_docker_scale', "services-exec", [
231                'body' => [
232                    'command' => 'php artisan ' . $request->command,
233                    'service' => $request->service
234                ]
235            ]);
236
237            $response = Client::systemSend('post', $clientRequest);
238
239            if (!Str::startsWith($response->code, 2)) {
240                throw new \Exception('Response code service_docker_scale : ' . $response->code);
241            }
242
243            return response()->json(['output' => $response->content['output']]);
244
245        } catch (\Exception $e) {
246            Log::debug('Unable to exec command : ' . var_export(['message' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile()], true));
247            return response()->json(['status' => 'ko', 'message' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
248        }
249    }
250
251    /**
252     * @return \Illuminate\Support\Collection
253     */
254    protected function getSignatures(): Collection
255    {
256
257        if (Cache::has(self::CACHE_KEY_SIGNATURES)) {
258            return Cache::get(self::CACHE_KEY_SIGNATURES);
259        }
260
261        $commands = collect(Artisan::all())->map(function ($class, $key) {
262
263            $reflection = new \ReflectionClass($class);
264
265            try {
266                $reflectionProperty = $reflection->getProperty('signature');
267                $reflectionProperty->setAccessible(true);
268
269                if ($reflectionProperty->getValue($class)) {
270                    return preg_replace('!\s+!', ' ', $reflectionProperty->getValue($class));
271                }
272
273                return $class->getName();
274
275            } catch (ReflectionException $e) {
276                //Log::debug("Artisan Command $key : {$e->getMessage()}");
277            }
278
279        })->reject(function ($signature) {
280            return is_null($signature);
281        })->sort();
282
283        Cache::put(self::CACHE_KEY_SIGNATURES, $commands, now()->addDay(1));
284
285        return $commands;
286    }
287
288    /**
289     * @param string $command
290     * @return string|null
291     */
292    protected function getSignature(string $command): ?string
293    {
294        return $this->getSignatures()->get($command);
295    }
296
297    /**
298     * @param $command
299     * @return \stdClass
300     */
301    protected function getMonitoringHistory($command): \stdClass
302    {
303        $is_ok = false;
304        $errors = 0;
305        $warnings = 0;
306
307        $monitoring = CommandMonitoring::where('command', $command->name)
308            ->where('created_at', '>=', Carbon::now()->subDays(self::SUBDAYS_MONITORING))
309            ->orderBy('created_at', 'desc')
310            ->get();
311
312        $monitoring->each(function ($log) use (&$errors, &$warnings) {
313            if (!is_null($log->error)) {
314                $errors++;
315            }
316
317            if (!is_null($log->warning)) {
318                $warnings++;
319            }
320        });
321
322        $lastTwo = $monitoring->take(2);
323
324        $lastTwo->each(function ($log) use (&$is_ok) {
325            if (!$log->running) {
326                $is_ok = $log->pass;
327            }
328        });
329
330        $last = $lastTwo->first();
331
332        return (object)[
333            'last_run' => $last ? $last->created_at->toDateTimeString() : null,
334            'is_ok' => (int)$is_ok,
335            'errors' => $errors,
336            'warnings' => $warnings
337        ];
338    }
339
340    protected function serviceScaleUp(array $services)
341    {
342        $clientRequest = ClientRequest::createObject('service_docker_scale', "services-scale", [
343            'body' => [
344                'services' => $services
345            ]
346        ]);
347
348        $response = Client::systemSend('post', $clientRequest);
349
350        if (!Str::startsWith($response->code, 2)) {
351            throw new \Exception('unable to scale service ');
352        }
353    }
354}