Working assignment script

This commit is contained in:
Jérémy Munsch 2024-01-25 17:29:29 +01:00
parent a44f49b91d
commit f4bc3566aa
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
8 changed files with 1377 additions and 81 deletions

View file

@ -0,0 +1,812 @@
<?php
namespace App\Console\Commands;
use App\Enums\EnvironmentScope;
use App\Lib\CdEnvVariable;
use GrahamCampbell\GitLab\GitLabManager;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Encryption\Encrypter;
use Illuminate\Validation\ValidationException;
use Psy\Readline\Hoa\ConsoleException;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
class SetGitlabCdVariables extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:gitlab:set_variables {--P|projectId=}';
/**
* The console command description.
*
* @var string
*/
protected $description = "Set default variables for an Uwil Project";
/**
* The gitlab client
*
* @var \Gitlab\Client
*/
private \Gitlab\Client $client;
/**
* The vars that should be present by default
*
* * These are type env_var.
*
* @var array<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>>
* 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<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>> $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<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>>
*/
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<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>> ...$cdVars
* @return array<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>>
*/
private function mergeVars(array ...$cdVars): array
{
/** @var array<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>>|null */
$firstArr = Arr::pull($cdVars, 0);
if (!$firstArr) {
return [];
}
collect($cdVars)->each(function (array $cdVarArray) use (&$firstArr) {
/** @var array<string,\App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>>|null $cdVarArray */
foreach ($cdVarArray as $key => $cdVarList) {
/** @var \App\Lib\CdEnvVariable|array<string,\App\Lib\CdEnvVariable>|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<string,\App\Lib\CdEnvVariable> $firstArrCdVarList
* @param \App\Lib\CdEnvVariable $varWithAllScope
* @return array<string,\App\Lib\CdEnvVariable>|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<string> $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'))
);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
enum EnvironmentScope: string
{
use BaseEnum;
case all = '*';
case staging = 'staging';
case production = 'production';
case reviewAny = 'review/*';
}

209
app/Lib/CdEnvVariable.php Normal file
View file

@ -0,0 +1,209 @@
<?php
namespace App\Lib;
use App\Enums\EnvironmentScope;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* @property-read \App\Enums\EnvironmentScope $environment_scope The scope on which the variable applies.
* @property-read string $key The variable name.
* @property-read string $value The variable value.
* @property-read boolean $protected Export variable to pipelines running on
* protected branches and tags only.
* @property-read boolean $masked Variable will be masked in job logs.
* Requires values to meet regular expression
* requirements.
* @property-read boolean $raw Char '$' will be treated as the start of a reference
* to another variable.
* @property-read boolean $isDefault Variable is needed for all projects.
* @package App\Lib
*/
class CdEnvVariable implements \Stringable
{
/**
* The k8s prefix
*
* @var string
*/
private static string $k8sPrefix = 'APP';
/**
* The prefix to use for defaults en vars
*
* @var string
*/
private static string $prefix = 'LRVL';
/**
* The variable is default
*
* @var boolean
*/
private bool $isDefault = false;
/**
* Properties that can be accessed from the object.
*
* @var array<string>
*/
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<string|boolean|\App\Enums\EnvironmentScope> $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<string|boolean|\App\Enums\EnvironmentScope> $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();
}
}

View file

@ -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;
});
}
}

View file

@ -0,0 +1,263 @@
<?php
namespace App\Traits\Enums;
use Illuminate\Support\Str;
trait BaseEnum
{
// * GENERIC METHODS
/**
* Get enum value.
*
* @return string|integer
*/
public function value(): string|int
{
return self::toValues()[$this->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<string>
*/
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<integer|string>
*/
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'];
}
}

View file

@ -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",

136
composer.lock generated
View file

@ -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",

View file

@ -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'
],
],
];