From f4bc3566aad78c007e654d71a8c8e911238aec0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Thu, 25 Jan 2024 17:29:29 +0100 Subject: [PATCH] Working assignment script --- app/Console/Commands/SetGitlabCdVariables.php | 812 ++++++++++++++++++ app/Enums/EnvironmentScope.php | 15 + app/Lib/CdEnvVariable.php | 209 +++++ app/Providers/AppServiceProvider.php | 6 + app/Traits/Enums/BaseEnum.php | 263 ++++++ composer.json | 3 +- composer.lock | 136 +-- config/gitlab.php | 14 +- 8 files changed, 1377 insertions(+), 81 deletions(-) create mode 100644 app/Console/Commands/SetGitlabCdVariables.php create mode 100644 app/Enums/EnvironmentScope.php create mode 100644 app/Lib/CdEnvVariable.php create mode 100644 app/Traits/Enums/BaseEnum.php diff --git a/app/Console/Commands/SetGitlabCdVariables.php b/app/Console/Commands/SetGitlabCdVariables.php new file mode 100644 index 0000000..57cb0eb --- /dev/null +++ b/app/Console/Commands/SetGitlabCdVariables.php @@ -0,0 +1,812 @@ +> + * Keyed with `\App\Enums\EnvironmentScope`. + */ + private static array $defaultEnvVars = []; + + /** + * Create a new console command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + + // * Init default vars reference + if (empty(self::$defaultEnvVars)) { + self::$defaultEnvVars = [ + 'APP_DEBUG' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'APP_DEBUG', + value: 'false', + protected: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'APP_DEBUG', + value: 'false', + protected: true, + masked: false, + ))->isProjectDefault(), + ], + 'APP_ENV' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'APP_ENV', + value: 'staging', + protected: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'APP_ENV', + value: 'production', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'APP_KEY' => (new CdEnvVariable( + environment_scope: EnvironmentScope::all, + key: 'APP_KEY', + value: $this->generateRandomKey(), + protected: true, + masked: true, + ))->isProjectDefault(), + 'APP_NAME' => (new CdEnvVariable( + environment_scope: EnvironmentScope::all, + key: 'APP_NAME', + value: 'ChangeMe', + protected: true, + ))->isProjectDefault(), + 'DOMAIN' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'DOMAIN', + value: '$CI_PROJECT_NAME.re7-s-web.fr', + protected: true, + raw: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'DOMAIN', + value: 'changeme.changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'CERT_ISSUER' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'CERT_ISSUER', + value: 're7-letsencrypt-production-dns01', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'CERT_ISSUER', + value: 'letsencrypt-production-http01', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'DB_DATABASE' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'DB_DATABASE', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'DB_DATABASE', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'DB_USERNAME' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'DB_USERNAME', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'DB_USERNAME', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'DB_PASSWORD' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'DB_PASSWORD', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'DB_PASSWORD', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'MAIL_ENCRYPTION' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'MAIL_ENCRYPTION', + value: 'null', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'MAIL_ENCRYPTION', + value: 'tls', + ))->isProjectDefault(), + ], + 'MAIL_HOST' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'MAIL_HOST', + value: 'null', + protected: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'MAIL_HOST', + value: 'smtp-relay.brevo.com', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'MAIL_PORT' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'MAIL_PORT', + value: 'null', + protected: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'MAIL_PORT', + value: '587', + protected: true, + ))->isProjectDefault(), + ], + 'MAIL_USERNAME' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'MAIL_USERNAME', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'MAIL_USERNAME', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'MAIL_PASSWORD' => [ + EnvironmentScope::staging->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::staging, + key: 'MAIL_PASSWORD', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + EnvironmentScope::production->value => (new CdEnvVariable( + environment_scope: EnvironmentScope::production, + key: 'MAIL_PASSWORD', + value: 'changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + ], + 'MAIL_FROM_ADDRESS' => (new CdEnvVariable( + environment_scope: EnvironmentScope::all, + key: 'MAIL_FROM_ADDRESS', + value: 'no-reply@changeme.changeme', + protected: true, + masked: true, + ))->isProjectDefault(), + 'PVC_SIZE' => (new CdEnvVariable( + environment_scope: EnvironmentScope::all, + key: 'PVC_SIZE', + value: '1Gi', + protected: true, + )), + 'LDC_WEB_API_KEY' => (new CdEnvVariable( + environment_scope: EnvironmentScope::all, + key: 'LDC_WEB_API_KEY', + value: 'changeme', + protected: true, + masked: true, + )), + 'SENTRY_LARAVEL_DSN' => (new CdEnvVariable( + environment_scope: EnvironmentScope::all, + key: 'SENTRY_LARAVEL_DSN', + value: 'changeme', + protected: true, + masked: true, + )), + ]; + self::$defaultEnvVars = Arr::sortRecursive(self::$defaultEnvVars); + } //end if + + $this->client = app(GitLabManager::class); + } + + /** + * Execute the console command. + * + * ! This command may throw exceptions if there is a unhandled difference between actual vars and template. + * + * @return void + * @throws \RuntimeException If an error occurs while comunicating with gitlab. + * @throws \Psy\Readline\Hoa\ConsoleException If the user cancels this command. + */ + public function handle(): void + { + $this->warn( + '! This command may throw exceptions if there is a unhandled difference between actual vars and template.' + ); + $this->warn('!! Sensitive data is masked !!'); + + $projects = $this->client->projects(); + + $projectId = $this->option('projectId'); + /** @var int|null $projectId */ + $projectId = \is_numeric($projectId) ? intval($projectId) : null; + + /** @var string|null */ + $projectNameWithNamespace = null; + + if (is_null($projectId)) { + $this->getInput( + 'Quel projet veut-on paramètrer ?', + [ + 'required', + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + function ( + string $parameter, + string|null $value, + callable $fail + ) use ( + $projects, + &$projectId, + &$projectNameWithNamespace + ) { + $results = collect($projects->all(['search' => $value])); + if (!$results->count()) { + $fail('Project not found'); + return; + } + $projectNameWithNamespace = $results->count() > 1 ? $this->getChoice( + 'Veuillez choisir parmis les résultats', + $results->pluck('name_with_namespace')->all() + ) : strval(Arr::get(Arr::get($results, 0), 'name_with_namespace')); + + if (empty($projectNameWithNamespace)) { + $fail('Project not found'); + return; + } + + $this->info("Selected project {$projectNameWithNamespace}"); + + $projectId = Arr::get($results->where( + 'name_with_namespace', + $projectNameWithNamespace + )->firstOrFail(), 'id'); + } + ] + ); + } else { + $projectNameWithNamespace = Arr::get($this->client->projects()->show($projectId), 'name_with_namespace'); + + if (empty($projectNameWithNamespace)) { + throw new \RuntimeException( + "Could not get 'name_with_namespace' for project project id `{$projectId}`" + ); + } + } //end if + + /** @var int $projectId */ + + $this->info("Using project id `{$projectId}` name `{$projectNameWithNamespace}`"); + + \sleep(6); + + $actualProjectVars = Arr::sort($this->getProjectCdVars($projectId)); + + $newVars = Arr::sort($this->mergeVars(self::$defaultEnvVars, $actualProjectVars)); + + $this->showAndConfirmDiff($actualProjectVars, $newVars); + + $this->warn('The following vars will be injected and replace previous CD vars'); + $this->line($this->stringifyVars($newVars)); + + if (!$this->confirm('Save these to git lab ?')) { + throw new ConsoleException('Operation aborted'); + } + + if ( + $this->getChoice( + 'Again: Previous vars will be lost and these will be saved, are you sure ?', + ['yes I am sure', 'no cancel please'] + ) === 'no cancel please' + ) { + throw new ConsoleException('Operation aborted'); + } + + // * Remove existing vars before inserting all + $this->removeExistingProjectCdVars($projectId); + $this->saveProjectCdVarsToGitlab($projectId, $newVars); + + $this->info('Done !!!'); + $this->info('Please check on ' . $this->getProjetCdVarsUrl($projectId)); + } + + // * PRIVATE GITLAB + + /** + * Save compiled vars on Gitlab project + * + * @param integer $projectId + * @param array> $newVars + * @return void + */ + private function saveProjectCdVarsToGitlab(int $projectId, array $newVars): void + { + try { + $updateProjectVar = function (int $projectId, CdEnvVariable $var) { + $this->client->projects()->addVariable( + $projectId, + $var->getFormatedGitlabKey(), + $var->value, + $var->protected, + $var->environment_scope->value, + [ + 'masked' => $var->masked, + 'raw' => $var->raw + ] + ); + }; + foreach ($newVars as $varOrVarList) { + if ($varOrVarList instanceof CdEnvVariable) { + $updateProjectVar($projectId, $varOrVarList); + continue; + } + foreach ($varOrVarList as $var) { + $updateProjectVar($projectId, $var); + } + } + } catch (\Exception $e) { + $this->warn( + 'Sorry an error just happened, please check actual env vars to prevent project crash ok k8s' + ); + dump($e); + exit; + } //end try + } + + /** + * Remove all existing project CD vars + * + * @param integer $projectId + * @return void + */ + private function removeExistingProjectCdVars(int $projectId): void + { + collect($this->queryProjectCdVars($projectId))->each(function (array $gitlabVar) use ($projectId) { + try { + $this->client->projects()->removeVariable( + $projectId, + Arr::get($gitlabVar, 'key'), + [ + 'filter' => [ + 'environment_scope' => Arr::get($gitlabVar, 'environment_scope') + ] + ] + ); + } catch (\Exception $e) { + $this->warn( + 'Sorry an error just happened, please check actual env vars to prevent project crash ok k8s' + ); + dump($e); + exit; + } //end try + }); + } + + /** + * Get Project vars + * + * @param integer $projectId The project id for which we want cd variables. + * @return array> + */ + private function getProjectCdVars(int $projectId): array + { + return Arr::sortRecursive(collect($this->queryProjectCdVars($projectId)) + ->map(function (array $envVar) { + $envVar = collect($envVar); + $env_scope = EnvironmentScope::make($envVar->get('environment_scope', '')); + if (!$env_scope) { + throw new \Error('Unhandled scope \'' . $envVar->get('environment_scope', '') . '\''); + } + $envVar->put('environment_scope', $env_scope); + return $envVar->all(); + })->filter(fn (array $envVar) => Arr::get($envVar, 'variable_type') === 'env_var') + ->map(fn (array $envVar) => CdEnvVariable::make($envVar)) + ->groupBy(fn (CdEnvVariable $envVar) => $envVar->key, true) + ->map(function (Collection $envVars) { + if ($envVars->count() === 1) { + return $envVars->first(); + } + return $envVars->values() + ->keyBy(fn (CdEnvVariable $envVar) => $envVar->environment_scope->value) + ->all(); + })->all()); + } + + /** + * Query Gitlab project vars + * + * @param integer $projectId + * @return array + */ + private function queryProjectCdVars(int $projectId): array + { + // ! per_page max is 100, if this limit is reached need to implement pagination scrapping. + return $this->client->projects()->variables($projectId, ['per_page' => 100]); + } + + /** + * Get project CD vars url + * + * @param integer $projectId + * @return string + */ + private function getProjetCdVarsUrl(int $projectId): string + { + return Str::of(Arr::get($this->client->projects()->show($projectId), 'web_url')) + ->finish('/')->append('-/settings/ci_cd'); + } + + // * PRIVATE MISC + + /** + * Show diff and request user consent + * + * @param array $actualProjectVars + * @param array $newVars + * @return void + * @throws \Psy\Readline\Hoa\ConsoleException If user wants to abort. + * @throws \RuntimeException If diff is empty. + */ + private function showAndConfirmDiff(array $actualProjectVars, array $newVars): void + { + // * Calc diff + $builder = new UnifiedDiffOutputBuilder( + // * custom header + "--- Original\n+++ New\n", + // * do not add line numbers to the diff + false + ); + $differ = new Differ($builder); + $diff = Str::of($differ->diff( + $this->stringifyVars(...$actualProjectVars), + $this->stringifyVars(...$newVars), + ))->explode("\n"); + + $diff->each(function (string $line) { + if (Str::of($line)->startsWith('-')) { + $this->error($line); + return; + } + if (Str::of($line)->startsWith('+')) { + $this->info($line); + return; + } + $this->line($line); + }); + + if ($diff->count() <= 3) { + throw new \RuntimeException( + 'There is no diff between skeletton Cd vars and actual vars, no need to update, Operation aborted' + ); + } + + if (!$this->confirm('The following diff will be set, continue ?')) { + throw new ConsoleException('Operation aborted'); + } + } + + /** + * Stringify a var list + * + * @param \App\Lib\CdEnvVariable|array ...$vars + * @return string + */ + private function stringifyVars(CdEnvVariable|array ...$vars): string + { + $out = Str::of(''); + foreach ($vars as $var) { + if (is_array($var)) { + $out = $out->append($this->stringifyVars(...$var)); + continue; + } + $out = $out->append($var)->newLine(); + } + return $out; + } + + /** + * Join variables + * + * @param array> ...$cdVars + * @return array> + */ + private function mergeVars(array ...$cdVars): array + { + /** @var array>|null */ + $firstArr = Arr::pull($cdVars, 0); + if (!$firstArr) { + return []; + } + collect($cdVars)->each(function (array $cdVarArray) use (&$firstArr) { + /** @var array>|null $cdVarArray */ + foreach ($cdVarArray as $key => $cdVarList) { + /** @var \App\Lib\CdEnvVariable|array|null The default config for actual key */ + $firstArrCdVarList = Arr::get($firstArr, $key); + + // ? A default config exists for this key ? + if (!$firstArrCdVarList) { + // * Assign and continue + Arr::set($firstArr, $key, $cdVarList); + continue; + } + // * Need to merge props form here. + // * This should indicate that environment_scope is all + if ($firstArrCdVarList instanceof \App\Lib\CdEnvVariable) { + $this->assertEnvironmentScopeIs(EnvironmentScope::all, ...Arr::wrap($cdVarList)); + + // ? Are we going to set all scopes var to an existing array of var with different scopes ? + if (is_array($cdVarList)) { + if ($firstArrCdVarList->environment_scope->equals(EnvironmentScope::all)) { + Arr::set($firstArr, $key, $this->askWhichShouldBeKeptOnMergeScopes( + $cdVarList, + $firstArrCdVarList + )); + continue; + } + Arr::set($firstArr, $key, $cdVarList); + continue; + } + // * Replace firArrVar. + Arr::set($firstArr, $key, $cdVarList); + continue; + } //end if + + if ($cdVarList instanceof \App\Lib\CdEnvVariable) { + if ($cdVarList->environment_scope->equals(EnvironmentScope::all)) { + Arr::set($firstArr, $key, $this->askWhichShouldBeKeptOnMergeScopes( + $firstArrCdVarList, + $cdVarList + )); + continue; + } + Arr::set($firstArr, $key, $cdVarList); + continue; + } + + // * This should indicate that there is no environment_scope in array. + $newCdVarList = $firstArrCdVarList; + $this->assertEnvironmentScopeIsNot(EnvironmentScope::all, ...$cdVarList); + foreach ($cdVarList as $environment_scope_value => $cdVar) { + // ! The can merge all scope with other scopes + $newCdVarList[$environment_scope_value] = $cdVar; + } + Arr::set($firstArr, $key, $newCdVarList); + } //end foreach + }); + return $firstArr; + } + + /** + * + * @param array $firstArrCdVarList + * @param \App\Lib\CdEnvVariable $varWithAllScope + * @return array|CdEnvVariable + */ + private function askWhichShouldBeKeptOnMergeScopes( + array $firstArrCdVarList, + CdEnvVariable $varWithAllScope + ): array|CdEnvVariable { + $this->info(' --- VAR ARRAY --->'); + collect($firstArrCdVarList)->each(fn (CdEnvVariable $var) => $this->info($var)); + $this->info(' --- VAR `All` scope --->'); + $this->info($varWithAllScope); + + $message = + 'How do you want to proceed in order to merge `All` scope var into a multiple scopes var array ?'; + $replaceMessage = 'Replace the var array with the `All` scope var'; + $keepMessage = 'Replace the `All` scope var with var array'; + switch ($this->getChoice($message, [$replaceMessage, $keepMessage], $replaceMessage)) { + case $replaceMessage: + return $varWithAllScope; + case $keepMessage: + default: + return $firstArrCdVarList; + } + } + + /** + * Asset that a variable `environment_scope` has a certain value. + * + * @param \App\Enums\EnvironmentScope $environment_scope + * @param \App\Lib\CdEnvVariable ...$var + * @return void + * @throws \RuntimeException If the variable `environment_scope` is different that expected. + */ + private function assertEnvironmentScopeIs( + EnvironmentScope $environment_scope, + CdEnvVariable ...$var + ): void { + foreach ($var as $cdVar) { + if (!$cdVar->environment_scope->equals($environment_scope)) { + $scopeLabel = $environment_scope->label(); + $this->error(' --- CONTEXT --->'); + collect($var)->each(fn (CdEnvVariable $var) => $this->info($var)); + $scopeLabel = $environment_scope->label(); + throw new \RuntimeException( + "environment_scope should have been `{$scopeLabel}` into var {$cdVar}" + ); + } + } + } + + /** + * Asset that a variable `environment_scope` dow not have a certain value. + * + * @param \App\Enums\EnvironmentScope $environment_scope + * @param \App\Lib\CdEnvVariable ...$var + * @return void + * @throws \RuntimeException If the variable `environment_scope` is different that expected. + */ + private function assertEnvironmentScopeIsNot( + EnvironmentScope $environment_scope, + CdEnvVariable ...$var + ): void { + foreach ($var as $cdVar) { + if ($cdVar->environment_scope->equals($environment_scope)) { + $scopeLabel = $environment_scope->label(); + throw new \RuntimeException( + "environment_scope shouldn't be `{$scopeLabel}` on var {$cdVar}" + ); + } + } + } + + /** + * Request user to make a choice + * + * @param string $askMessage + * @param array $choices + * @param string|null $default + * @return string + */ + private function getChoice( + string $askMessage, + array $choices, + string $default = null + ): string { + return $this->choice($askMessage, $choices, $default); + } + + /** + * Get user input + * + * @param string $askMessage Ask something to the user. + * @param string|array $validate As you would pass to laravel validator. + * @param string $errorMessage Message to show in case of wrong input. + * @return string + */ + private function getInput( + string $askMessage, + string|array $validate = 'min:3|max:255', + string $errorMessage = 'Invalid value try again' + ): string { + $value = null; + while (is_null($value)) { + try { + /** @var string|null $tmp */ + $tmp = $this->ask($askMessage); + $validator = Validator::make(['input' => $tmp], [ + 'input' => \is_string($validate) ? "required|{$validate}" : + \array_merge(['required'], $validate) + ]); + $validator->validate(); + /** @var string $value */ + $value = $tmp; + } catch (ValidationException $e) { + $this->error(sprintf( + "{$errorMessage} %s", + \implode(',', collect($e->errors())->flatten()->all()) + )); + continue; + } + }; //end while + return $value; + } + + /** + * Generate a random key for the application. + * + * ! Copy from \Illuminate\Foundation\Console\KeyGenerateCommand + * + * @return string + */ + protected function generateRandomKey() + { + return 'base64:' . base64_encode( + Encrypter::generateKey(config('app.cipher')) + ); + } +} diff --git a/app/Enums/EnvironmentScope.php b/app/Enums/EnvironmentScope.php new file mode 100644 index 0000000..2f21eee --- /dev/null +++ b/app/Enums/EnvironmentScope.php @@ -0,0 +1,15 @@ + + */ + private static array $readableProps = [ + 'environment_scope', 'key', 'value', 'protected', + 'masked', 'raw', 'isDefault', + ]; + + /** + * CdEnvVariable + * + * @param \App\Enums\EnvironmentScope $environment_scope The scope on which the variable applies. + * @param string $key The variable name. + * @param string $value The variable value. + * @param boolean $protected Export variable to pipelines running on + * protected branches and tags only. + * @param boolean $masked Variable will be masked in job logs. + * Requires values to meet regular expression requirements. + * @param boolean $raw Char '$' will be treated as the start of a reference + * to another variable. + * @param string $variable_type Should always be 'env_var' for now. + * @return void + */ + public function __construct( + protected EnvironmentScope $environment_scope, + protected string $key, + protected string $value, + protected bool $protected = false, + protected bool $masked = false, + protected bool $raw = false, + protected string $variable_type = 'env_var', + ) { + } + + /** + * Make a CdEnvVariable + * + * @param array $params + * @return self + */ + public static function make( + array $params + ): self { + self::removeVarNamePrefixes($params); + return new self(...$params); + } + + /** + * Property accessor + * + * @param string $name + * @return boolean|string|\App\Enums\EnvironmentScope + * @throws \Error If the property is not accessible. + */ + public function __get(string $name): bool|string|EnvironmentScope + { + if (\in_array($name, self::$readableProps)) { + return $this->{$name}; + } + throw new \Error('No property ' . __CLASS__ . "::{$name}"); + } + + /** + * Return is default. + * + * @return self + */ + public function isProjectDefault(): self + { + $this->isDefault = true; + return $this; + } + + /** + * Normalize var name + * + * @return string + */ + public function getFormatedGitlabKey(): string + { + // * Format with wanted prefixes + if ($this->isDefault) { + return sprintf( + '%s_%s_%s', + self::$k8sPrefix, + self::$prefix, + Str::upper($this->key) + ); + } + return sprintf( + '%s_%s', + self::$k8sPrefix, + str::upper($this->key) + ); + } + + // * PRIVATE + /** + * Remove var name prefixes + * + * @param array $params + * @return void + */ + private static function removeVarNamePrefixes(array &$params): void + { + // * Force remove k8sPrefix + if (Str::startsWith(Arr::get($params, 'key'), self::$k8sPrefix)) { + Arr::set($params, 'key', Str::replaceStart(self::$k8sPrefix . '_', '', Arr::get($params, 'key'))); + } + // * Force remove prefix + if (Str::startsWith(Arr::get($params, 'key'), self::$prefix)) { + Arr::set($params, 'key', Str::replaceStart(self::$prefix . '_', '', Arr::get($params, 'key'))); + } + } + + /** + * String representation of the env variable + * + * @return string + */ + public function __toString(): string + { + $rowLength = 40; + $titlePad = 20; + $printStr = Str::of('')->newLine(); + + + // * TOP ROW + $printStr = $printStr->append(\str_pad(" VAR `{$this->key}` ", $rowLength, '_', \STR_PAD_BOTH))->newLine(); + + $printStr = $printStr->append(\str_pad('value', $titlePad, pad_type: \STR_PAD_LEFT)) + ->append(' => '); + $value = $this->masked ? + Str::mask($this->value, '*', 0, intval(ceil(strlen($this->value) * 0.65))) : $this->value; + $printStr = $printStr->append("`{$value}`")->newLine(); + + $printStr = $printStr->append(\str_pad('environment_scope', $titlePad, pad_type: \STR_PAD_LEFT)) + ->append(' => ')->append('`') + ->append($this->environment_scope->label()) + ->append('`')->newLine(); + + if ($this->protected or $this->masked or $this->raw) { + $printStr = $printStr->append(\str_pad('options', $titlePad, pad_type: \STR_PAD_LEFT)) + ->append(' => ') + ->append(collect([ + $this->protected ? 'protected' : null, + $this->masked ? 'masked' : null, + $this->raw ? 'raw' : null, + ])->filter()->implode(','))->newLine(); + } + $printStr = $printStr->append(\str_pad('variable_type', $titlePad, pad_type: \STR_PAD_LEFT)) + ->append(' => ') + ->append("`{$this->variable_type}`"); + + // * BOTTOM ROW + $printStr = $printStr->newLine() + ->append(\str_pad('', $rowLength, '_', \STR_PAD_LEFT)) + ->newLine(); + + return $printStr->toString(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2389797..7c2246a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use GrahamCampbell\GitLab\Facades\GitLab; use GrahamCampbell\GitLab\GitLabManager; use Illuminate\Support\ServiceProvider; @@ -23,5 +24,10 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + app()->bind(GitLabManager::class, function () { + /** @var \Gitlab\Client */ + $client = GitLab::connection(GitLab::getDefaultConnection()); + return $client; + }); } } diff --git a/app/Traits/Enums/BaseEnum.php b/app/Traits/Enums/BaseEnum.php new file mode 100644 index 0000000..d6016cb --- /dev/null +++ b/app/Traits/Enums/BaseEnum.php @@ -0,0 +1,263 @@ +name]; + } + + /** + * Get enum label. + * + * @return string + */ + public function label(): string + { + return self::toLabels()[$this->name]; + } + + /** + * Get enum name. + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + // * STATIC FETCH METHODS + + /** + * Get enum labels. + * + * @return array + */ + public static function toLabels(): array + { + if (empty(self::getCache('labels'))) { + $labels = self::getLabelsConstant(); + self::setCache('labels', \collect(self::cases()) + ->mapWithKeys(function ($enum) use ($labels) { + if (!\is_null($labels) and !isset($labels[$enum->name])) { + throw new \Error('Missing definition label ' . self::class . " for {$enum->name}"); + } + return [$enum->name => \trans( + !\is_null($labels) ? $labels[$enum->name] : Str::ucfirst($enum->name) + )]; + })->all()); + } + return self::getCache('labels'); + } + + /** + * Get enum values. + * + * @return array + */ + public static function toValues(): array + { + if (empty(self::getCache('values'))) { + self::setCache('values', \collect(self::cases()) + ->mapWithKeys(fn ($enum) => [$enum->name => $enum->value])->all()); + } + return self::getCache('values'); + } + + /** + * Get enums. + * + * @return array + */ + public static function all(): array + { + return self::cases(); + } + + /** + * Get enums. + * + * @return array + */ + public static function toArray(): array + { + if (empty(self::getCache('enums'))) { + $definitions = self::getCustomDefinitions(); + self::setCache('enums', \collect(self::cases()) + ->mapWithKeys(function ($enum) use ($definitions) { + $baseArray = [ + 'name' => $enum->name, + 'label' => $enum->label(), + 'value' => $enum->value, + ]; + foreach ($definitions as $index => $elements) { + $index = Str::lower(Str::singular($index)); + if (!isset($elements[$enum->name])) { + throw new \Error("Missing definition {$index} " . self::class . " for {$enum->name}"); + } + $baseArray[$index] = $elements[$enum->name]; + } + return [$enum->name => (object)$baseArray]; + })->all()); + } + return self::getCache('enums'); + } + + /** + * Fetch enum using value. + * + * @param self|integer|string $value + * @param string $field + * @return self|null + */ + public static function make(self|int|string $value, string $field = 'value'): self|null + { + if ($field === "label") { + $enumLabels = array_change_key_case(array_flip(self::toLabels()), CASE_LOWER); + $value = (isset($enumLabels[strtolower($value)])) ? $enumLabels[strtolower($value)] : false; + $field = 'name'; + } + return !\is_object($value) ? + \collect(self::cases())->keyBy($field)->get($value) : $value; + } + + /** + * Test if 2 enum are equals. + * + * @param object ...$others + * @return boolean + * @throws \Error If provided element are not enums that extends BaseEnum. + */ + public function equals(object ...$others): bool + { + foreach ($others as $other) { + if (!\in_array(BaseEnum::class, \class_uses_recursive($other))) { + throw new \Error(__METHOD__ . + ' requires $enum to use ' . BaseEnum::class); + } + if ( + get_class($this) === get_class($other) + && $this->value === $other->value + ) { + return true; + } + } + return false; + } + + /** + * Serialize to json. + * + * @return mixed + */ + public function jsonSerialize(): mixed + { + return $this->value; + } + + + // * CACHE + + /** + * Get cache array. + * + * @param string $index + * @return array + */ + private static function getCache(string $index): array + { + self::initCache(); + if (!isset($GLOBALS[self::class]['props'][$index])) { + $GLOBALS[self::class]['props'][$index] = []; + } + return $GLOBALS[self::class]['props'][$index]; + } + + /** + * Set cache array. + * + * @param string $index + * @param array $items + * @return void + */ + private static function setCache(string $index, array $items): void + { + self::initCache(); + if (!isset($GLOBALS[self::class]['props'][$index])) { + $GLOBALS[self::class]['props'][$index] = []; + } + $GLOBALS[self::class]['props'][$index] = $items; + } + + /** + * Init cache if needed. + * + * @return void + */ + private static function initCache(): void + { + if (!isset($GLOBALS[self::class])) { + $GLOBALS[self::class] = [ + 'props' => [ + 'enums' => [], + 'values' => [], + 'labels' => [] + ], + 'customProps' => null + ]; + } + } + + // * PRIVATE + + /** + * Fetch the LABELS constant if exists. + * + * @return array|null + */ + private static function getLabelsConstant(): array|null + { + $class_name = self::class; + try { + $constant_reflex = new \ReflectionClassConstant($class_name, 'LABELS'); + return $constant_reflex->getValue(); + } catch (\ReflectionException $e) { + return null; + } + } + + /** + * Get custom constants. + * + * @return array + */ + private static function getCustomDefinitions(): array + { + // ? Has custom props in cache ? + if (!isset($GLOBALS[self::class]) or \is_null($GLOBALS[self::class]['customProps'])) { + $names = \collect(self::cases())->keyBy('name'); + + $GLOBALS[self::class]['customProps'] = \collect((new \ReflectionClass(self::class))->getConstants()) + ->filter(function (mixed $constant, string $key) use ($names) { + if ($key === 'LABELS' or !\is_array($constant)) { + return false; + } + return \collect($constant)->keys() + ->filter(fn (string $name) => $names->has($name))->count(); + })->all(); + } + return $GLOBALS[self::class]['customProps']; + } +} diff --git a/composer.json b/composer.json index 740edbe..fd75033 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.10", "laravel/sanctum": "^3.3", - "laravel/tinker": "^2.8" + "laravel/tinker": "^2.8", + "sebastian/diff": "^5.1" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index dee33cd..52d938d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8cfd935253b074b4600945f52beceb9a", + "content-hash": "14267b564f41df466ad8f3fffd3b577b", "packages": [ { "name": "brick/math", @@ -4101,6 +4101,73 @@ ], "time": "2023-11-08T05:53:05+00:00" }, + { + "name": "sebastian/diff", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T10:55:06+00:00" + }, { "name": "symfony/cache", "version": "v6.4.2", @@ -8707,73 +8774,6 @@ ], "time": "2023-12-21T08:37:17+00:00" }, - { - "name": "sebastian/diff", - "version": "5.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-22T10:55:06+00:00" - }, { "name": "sebastian/environment", "version": "6.0.1", diff --git a/config/gitlab.php b/config/gitlab.php index d44f670..59b00a6 100644 --- a/config/gitlab.php +++ b/config/gitlab.php @@ -41,15 +41,10 @@ return [ 'connections' => [ 'main' => [ - 'token' => 'your-token', + 'token' => env('GITLAB_ADMIN_TOKEN'), + 'url' => env('GITLAB_URL'), 'method' => 'token' ], - - 'alternative' => [ - 'token' => 'your-token', - 'method' => 'oauth' - ], - ], /* @@ -70,11 +65,6 @@ return [ 'connector' => null ], - 'bar' => [ - 'driver' => 'illuminate', - 'connector' => 'redis' - ], - ], ];