Skip to content
This repository was archived by the owner on Oct 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ AI_URL=https://api.openai.com
AI_CHAT_COMPLETIONS_ENDPOINT=/v1/chat/completions
AI_EMBEDDING_ENDPOINT=/v1/embeddings
AI_REMOTE_EMBEDDING=true

S3SERVER_STORAGE_PATH=state/
30 changes: 30 additions & 0 deletions app/Exceptions/KopsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace App\Exceptions;

use Exception;
use Throwable;

/**
* Class KopsException.
*
* This class is the exception for kops.
*
* @author Marcel Menk <marcel.menk@ipvx.io>
*/
class KopsException extends Exception
{
/**
* Construct the exception.
*
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($message, $code, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
3 changes: 2 additions & 1 deletion app/Helpers/API/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Helpers\API;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\MessageBag;
use Throwable;

class Response
Expand All @@ -19,7 +20,7 @@ class Response
*
* @return JsonResponse
*/
public static function generate(int $code, string $status, string $message, array | Throwable | null $data = null): JsonResponse
public static function generate(int $code, string $status, string $message, array | Throwable | MessageBag | null $data = null): JsonResponse
{
$response = [
'status' => $status,
Expand Down
3 changes: 2 additions & 1 deletion app/Helpers/Git/GitRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ protected function runCommand($command)
}
}

$result = Process::path($this->repoPath)
$result = Process::timeout(config('process.timeout'))
->path($this->repoPath)
->env($env)
->run($command);

Expand Down
275 changes: 275 additions & 0 deletions app/Helpers/Kops/KopsDeployment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<?php

declare(strict_types=1);

namespace App\Helpers\Kops;

use App\Exceptions\KopsException;
use App\Helpers\Kubernetes\YamlFormatter;
use App\Models\Kubernetes\Clusters\Cluster;
use App\Models\Kubernetes\Clusters\ClusterData;
use App\Models\Kubernetes\Clusters\ClusterSecretData;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use LaravelS3Server\Models\S3AccessCredential;
use Symfony\Component\Yaml\Yaml;

/**
* Class KopsDeployment.
*
* This class is the helper for the Kops deployment.
*
* @author Marcel Menk <marcel.menk@ipvx.io>
*/
class KopsDeployment
{
/**
* Generate a deployment.
*
* @param Cluster $cluster
* @param array $data
* @param array $secretData
* @param bool $replaceExisting
*/
public static function generate(Cluster $cluster, array $data = [], array $secretData = [], bool $replaceExisting = false): string
{
if (Storage::disk('local')->exists($cluster->kopsPath) && !$replaceExisting) {
throw new KopsException('Forbidden', 403);
}

if ($replaceExisting) {
Storage::disk('local')->deleteDirectory($cluster->kopsPath);

$s3Credentials = S3AccessCredential::where('access_key_id', $cluster->id)->first();
} else {
$s3Credentials = S3AccessCredential::create([
'access_key_id' => $cluster->id,
'secret_access_key' => Str::random(32),
'description' => 'Kops state store for cluster ' . $cluster->id,
'bucket' => $cluster->id,
]);
}

$clusterDirectoryStatus = Storage::disk('local')->makeDirectory($cluster->kopsPath);

if (!$clusterDirectoryStatus) {
throw new KopsException('Server Error', 500);
}

$files = collect();

$cluster->template->fullTree->each(function ($item) use ($cluster, $data, $secretData, &$files) {
if ($item->type === 'file') {
$files->push(self::createFile($item, $cluster, $data, $secretData));
} elseif ($item->type === 'folder') {
$files->concat(self::createFolder($item, $cluster, $data, $secretData));
}
});

$files->filter()->sortBy('object.sort')->each(function ($file) use ($cluster, $replaceExisting, $s3Credentials) {
if ($replaceExisting) {
$cmd = ['kops', 'replace', '-f', $cluster->kopsPath . $file->object->path, '--force'];
} else {
$cmd = ['kops', 'create', '-f', $cluster->kopsPath . $file->object->path];
}

$result = Process::timeout(config('process.timeout'))
->env([
'S3_ENDPOINT' => config('app.url') . '/s3',
'S3_FORCE_PATH_STYLE' => 'true',
'KOPS_STATE_STORE' => 's3://' . $cluster->id,
...($s3Credentials ? [
'AWS_ACCESS_KEY_ID' => $s3Credentials->access_key_id,
'AWS_SECRET_ACCESS_KEY' => $s3Credentials->secret_access_key,
] : []),
])
->run($cmd);

if (!$result->successful()) {
throw new KopsException('Failed to create cluster', 500);
}
});

return self::kubeconfig($cluster);
}

/**
* Delete a deployment.
*
* @param Cluster $cluster
*/
public static function delete(Cluster $cluster)
{
if (!Storage::disk('local')->exists($cluster->kopsPath)) {
throw new KopsException('Not Found', 404);
}

$s3Credentials = S3AccessCredential::where('access_key_id', $cluster->id)->first();
$files = collect();

$cluster->template->fullTree->each(function ($item) use ($cluster, &$files) {
if ($item->type === 'file') {
$files->push(
self::createFile(
$item,
$cluster,
$cluster->clusterData->mapWithKeys(function (ClusterData $data) {
return [$data->key => $data->value];
})->toArray(),
$cluster->clusterSecretData->mapWithKeys(function (ClusterSecretData $data) {
return [$data->key => $data->value];
})->toArray()
)
);
} elseif ($item->type === 'folder') {
$files->concat(
self::createFolder(
$item,
$cluster,
$cluster->clusterData->mapWithKeys(function (ClusterData $data) {
return [$data->key => $data->value];
})->toArray(),
$cluster->clusterSecretData->mapWithKeys(function (ClusterSecretData $data) {
return [$data->key => $data->value];
})->toArray()
)
);
}
});

$files->filter()->sortBy('object.sort')->each(function ($file) use ($cluster, $s3Credentials) {
$cmd = ['kops', 'delete', '-f', $cluster->kopsPath . $file->object->path, '--yes'];
$result = Process::timeout(config('process.timeout'))
->env([
'S3_ENDPOINT' => config('app.url') . '/s3',
'S3_FORCE_PATH_STYLE' => 'true',
'KOPS_STATE_STORE' => 's3://' . $cluster->id,
...($s3Credentials ? [
'AWS_ACCESS_KEY_ID' => $s3Credentials->access_key_id,
'AWS_SECRET_ACCESS_KEY' => $s3Credentials->secret_access_key,
] : []),
])
->run($cmd);

if (!$result->successful()) {
throw new KopsException('Failed to delete cluster', 500);
}
});

if (!Storage::disk('local')->deleteDirectory($cluster->kopsPath)) {
throw new KopsException('Server Error', 500);
}
}

/**
* Generate a kubeconfig.
*
* @param Cluster $cluster
*
* @return string
*/
public static function kubeconfig(Cluster $cluster): string
{
$s3Credentials = S3AccessCredential::where('access_key_id', $cluster->id)->first();

$path = 'kubeconfig/' . $cluster->id . '.yaml';
$fullPath = Storage::disk('local')->path($path);

$cmd = ['kops', 'export', 'kubeconfig', '--all', '--admin', '--path', $fullPath];

$result = Process::timeout(config('process.timeout'))
->env([
'S3_ENDPOINT' => config('app.url') . '/s3',
'S3_FORCE_PATH_STYLE' => 'true',
'KOPS_STATE_STORE' => 's3://' . $cluster->id,
...($s3Credentials ? [
'AWS_ACCESS_KEY_ID' => $s3Credentials->access_key_id,
'AWS_SECRET_ACCESS_KEY' => $s3Credentials->secret_access_key,
] : []),
])
->run($cmd);

if (!$result->successful()) {
throw new KopsException('Failed to generate kubeconfig', 500);
}

$kubeconfig = Storage::disk('local')->get($path);
Storage::disk('local')->delete($path);

return $kubeconfig;
}

/**
* Create a folder.
*
* @param object $item
* @param Cluster $cluster
* @param array $data
* @param array $secretData
*
* @return Collection<object|null>
*/
private static function createFolder(object $item, Cluster $cluster, array $data = [], array $secretData = []): Collection
{
$files = collect();
$path = $cluster->kopsPath . $item->object->path;

Storage::disk('local')->makeDirectory($path);

$item->children?->each(function ($child) use ($cluster, $data, $secretData, &$files) {
if ($child->type === 'file') {
$files->push(self::createFile($child, $cluster, $data, $secretData));
} elseif ($child->type === 'folder') {
$files->concat(self::createFolder($child, $cluster, $data, $secretData));
}
});

return $files;
}

/**
* Create a file.
*
* @param object $item
* @param Cluster $cluster
* @param array $data
* @param array $secretData
*
* @return object|null
*/
private static function createFile(object $item, Cluster $cluster, array $data = [], array $secretData = [])
{
$path = $cluster->kopsPath . $item->object->path;

try {
$templateContent = Yaml::parse(
Blade::render(
str_replace("\t", ' ', $item->object->content),
[
'data' => $data,
'secret' => $secretData,
],
false
)
);

if (!$templateContent) {
Storage::disk('local')->delete($path);

return;
}

Storage::disk('local')->put($path, YamlFormatter::format(Yaml::dump($templateContent, 10, 2)));

return $item;
} catch (Exception $e) {
throw new KopsException('Server Error', 500);
}

return null;
}
}
4 changes: 2 additions & 2 deletions app/Helpers/Kubernetes/HelmManifests.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ protected static function pullOciChart(): string
$ociRef = rtrim(self::$repoUrl, '/') . '/' . self::$chartName;

$cmd = ['helm', 'pull', $ociRef, '--untar', '--destination', $tmpDir];
$result = Process::run($cmd);
$result = Process::timeout(config('process.timeout'))->run($cmd);

if (!$result->successful()) {
throw new HelmException('Failed to download chart');
Expand All @@ -187,7 +187,7 @@ protected static function pullOciChartFromFullRef(string $ociRef): string
}

$cmd = ['helm', 'pull', $ociRef, '--untar', '--destination', $tmpDir];
$result = Process::run($cmd);
$result = Process::timeout(config('process.timeout'))->run($cmd);

if (!$result->successful()) {
throw new HelmException('Failed to download chart');
Expand Down
Loading