Informacje wstępne o projekcie s3store
Opis projektu s3store obejmuje 2 wpisy na tym blogu:
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-s3store-oparty-na-minio-s3store-manager/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-s3store-oparty-na-minio-s3store-gui/
Opis rozwiązania MinIO
MinIO to lekka i „budżetowa” obiektowa pamięć masowa, w 100% zgodna z API Amazon S3 (Simple Storage Service). Najlepiej sprawdza się do trzymania danych nieustrukturyzowanych, czyli np. zdjęć, filmów, logów, plików eksportów czy różnych „artefaktów” z systemów.
Jest uniwersalne, bo możesz je wykorzystać m.in. do:
- archiwizacji danych,
- składowania i analizy dużych zbiorów danych,
- backupów oraz odtwarzania awaryjnego (DR).
MinIO pozwala też łatwo zbudować klaster z wielu mniejszych instancji storage’u (tzw. podejście „microstorage” – dużo małych elementów zamiast jednego dużego). Dzięki temu rozwiązanie jest proste do skalowania (dokładasz kolejne nody) i potrafi zapewnić wysoką dostępność, bo awaria pojedynczej instancji nie musi zatrzymać całej usługi.
S3 (i MinIO jako „S3-compatible”) nie jest filesystemem, tylko API do obiektów:
- Bucket = logiczny „kontener”
- Object = plik + metadane, identyfikowany przez klucz (np. uploads/mors.1.0.0_openshift.tar.gz)
Nie ma „montowania S3 jako PVC” w naturalny sposób. Więcej informacji:
Założenia projektu s3store

Projekt s3store składa się z 2 aplikacji:
1. Aplikacja s3store-manager, która obsługuje funkcjonalność
- MinIO Server (S3 API)
- MinIO Console (admin)
i zawiera elementy
- Secret z root credentials (admin) MinIO (np. MINIO_ROOT_USER, MINIO_ROOT_PASSWORD)
- Deployment
- Service port 9000 → S3 API, port 9001 → MinIO Console
- Route(y): route do Console (żeby adminowo klikać z przeglądarki). route do S3 API (jeśli chcesz upload bezpośrednio z laptopa / presigned URLs)
MinIO ma wbudowaną konsolę web i standardowo wystawia ją na osobnym porcie.
ewentualnie Job „init” do bucket/user/policy
2. Aplikacja s3store-gui, który obsługuje funkcjonalność
- PHP: upload + listing + download link
- PHP odbiera upload
- PHP robi putObject() do MinIO przez S3 API
- PHP pokazuje link/listę obiektów w bucket
Struktura aplikacji s3store-gui w projekcie s3store-prod
/repo/pakiety/s3store/1.0.0/s3store-gui -rw-r--r-- 1 bastuser bastuser 554 Feb 14 15:06 cm-s3store-gui-s3.yaml -rw-r--r-- 1 bastuser bastuser 888 Feb 14 15:06 deploy-s3store-gui.yaml -rw-r--r-- 1 bastuser bastuser 775 Feb 14 15:06 Dockerfile -rw-r--r-- 1 bastuser bastuser 11215 Feb 14 15:05 index.php -rw-r--r-- 1 bastuser bastuser 190 Feb 14 15:07 svc-s3store-gui.yaml
Przygotowanie Dockerfile na potrzeby zbudowania obrazu aplikacji
FROM registry.access.redhat.com/ubi9/php-82:latest
USER 0
RUN printf "upload_max_filesize=100M\npost_max_size=110M\nmemory_limit=256M\nmax_execution_time=300\n" > /etc/php.d/zz-upload.ini
WORKDIR /opt/app-root/src
COPY index.php /opt/app-root/src/index.php
# Composer + AWS SDK:
# 1) require bez --no-dev
# 2) install bez devów
RUN php -r "copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');" \
&& php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer \
&& rm -f /tmp/composer-setup.php \
&& composer --no-interaction --working-dir=/opt/app-root/src require aws/aws-sdk-php:^3 \
&& composer --no-interaction --working-dir=/opt/app-root/src install --no-dev --prefer-dist --no-progress --optimize-autoloader
RUN chgrp -R 0 /opt/app-root/src /etc/php.d \
&& chmod -R g=u /opt/app-root/src /etc/php.d
USER 1001
EXPOSE 8080
CMD ["/usr/libexec/s2i/run"]ConfigMap YAML z danymi dostępowymi dla aplikacji s3store-gui
apiVersion: v1 kind: ConfigMap metadata: name: s3store-gui-s3 namespace: s3store-prod data: # najlepsze dla aplikacji działającej W KLASTRZE: S3_ENDPOINT: "http://s3store-manager.s3store-prod.svc:9000" S3_BUCKET: "uploads" AWS_REGION: "homelab" AWS_ACCESS_KEY_ID: "s3store-gui-user" AWS_SECRET_ACCESS_KEY: "cGW8oKh0mleGaNKvWHecLCApaDYj+w2S" # MinIO + własny endpoint -> path-style najbezpieczniej: S3_PATH_STYLE: "true" # endpoint jest HTTP, więc verify SSL nie ma znaczenia, ale zostawiamy spójnie: S3_VERIFY_SSL: "true"
Deployment aplikacji s3store-manager w projekcie s3store-prod
apiVersion: apps/v1
kind: Deployment
metadata:
name: s3store-manager
namespace: s3store-prod
spec:
replicas: 1
selector:
matchLabels:
app: s3store-manager
template:
metadata:
labels:
app: s3store-manager
spec:
containers:
- name: minio
image: image-registry.openshift-image-registry.svc:5000/s3store-prod/s3store-manager:latest
imagePullPolicy: Always
envFrom:
- secretRef:
name: s3store-minio-root
- configMapRef:
name: s3store-minio-config
ports:
- name: s3api
containerPort: 9000
- name: console
containerPort: 9001
volumeMounts:
- name: data
mountPath: /data
readinessProbe:
httpGet:
path: /minio/health/ready
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
initialDelaySeconds: 20
periodSeconds: 20
volumes:
- name: data
persistentVolumeClaim:
claimName: s3store-manager-data
Service dla s3store-gui
apiVersion: v1
kind: Service
metadata:
name: s3store-gui
namespace: s3store-prod
spec:
selector:
app: s3store-gui
ports:
- name: http
port: 8080
targetPort: 8080Utworzenie policy dla aplikacji s3store-gui (tylko bucket uploads)
Przygotowanie aplikacji s3store-gui w projekcie s3store-prod
cd /repo/pakiety/s3store/1.0.0/s3store-gui oc project s3store-prod # ConfigMap oc apply -f cm-s3store-gui-s3.yaml # utwórz BuildConfig typu docker, build z lokalnego katalogu oc new-build --name=s3store-gui --binary --strategy=docker -n s3store-prod # wystartuj build, biorąc Dockerfile z tego katalogu oc start-build s3store-gui --from-dir=. --follow -n s3store-prod # Deploy + Service oc apply -f deploy-s3store-gui.yaml oc apply -f svc-s3store-gui.yaml # Route do aplikacji (Bootstrap UI) oc delete route s3store-gui -n s3store-prod --ignore-not-found oc create route edge s3store-gui --service=s3store-gui --port=http -n s3store-prod oc get route s3store-gui -n s3store-prod # restart aplikacji oc rollout restart deployment/s3store-gui
Kod aplikacji PHP s3store-gui


# po zmianach w kodzie aplikacji oc start-build s3store-gui --from-dir=. --follow oc rollout restart deploy/s3store-gui -n s3store-prod oc rollout status deploy/s3store-gui -n s3store-prod
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Aws\S3\S3Client;
function envv(string $k, ?string $default=null): string {
$v = getenv($k);
return ($v === false || $v === '') ? ($default ?? '') : $v;
}
function s3client(): S3Client {
$endpoint = envv('S3_ENDPOINT');
$region = envv('AWS_REGION', 'homelab');
$verifySsl = strtolower(envv('S3_VERIFY_SSL', 'true')) === 'true';
$pathStyle = strtolower(envv('S3_PATH_STYLE', 'true')) === 'true';
$opts = [
'version' => 'latest',
'region' => $region,
'endpoint' => $endpoint,
'credentials' => [
'key' => envv('AWS_ACCESS_KEY_ID'),
'secret' => envv('AWS_SECRET_ACCESS_KEY'),
],
'use_path_style_endpoint' => $pathStyle,
'http' => [
'verify' => $verifySsl,
'connect_timeout' => 5,
'timeout' => 300,
],
];
return new S3Client($opts);
}
function humanBytes(int $bytes): string {
$units = ['B','KB','MB','GB','TB'];
$i = 0;
$v = (float)$bytes;
while ($v >= 1024 && $i < count($units)-1) { $v /= 1024; $i++; }
return sprintf(($i === 0 ? '%.0f %s' : '%.2f %s'), $v, $units[$i]);
}
function parseArchiveName(string $filename): array {
// Oczekiwany format: s3storemanager_1.0.0_coo_openshift.tar.gz
$re = '/^([a-zA-Z0-9-]+)_([0-9]+\.[0-9]+\.[0-9]+)_([a-zA-Z0-9-]+)_([a-zA-Z0-9-]+)\.tar\.gz$/';
if (!preg_match($re, $filename, $m)) {
return [
'ok' => false,
'nazwa' => '',
'wersja' => '',
'warstwa' => '',
'token' => '',
];
}
return [
'ok' => true,
'nazwa' => $m[1],
'wersja' => $m[2],
'token' => $m[3], // np. "coo" (nie pokazujemy w tabeli, ale możesz tagować jeśli chcesz)
'warstwa' => $m[4], // np. "openshift"
];
}
function sanitizeTagValue(string $v): string {
$v = trim($v);
// tag values w S3 nie lubią śmieci; upraszczamy:
$v = preg_replace('/[^\pL\pN \(\)\-_.]/u', '_', $v) ?? $v;
// ogranicz długość (konserwatywnie)
return mb_substr($v, 0, 200);
}
$bucket = envv('S3_BUCKET', 'uploads');
$s3 = s3client();
$authors = [
'Jan Kowalski (Developer)',
'Adam Nowak (Administrator)',
];
$envs = ['PROD', 'DEV'];
$msg = null;
$msgType = 'info';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$author = $_POST['author'] ?? '';
$envSel = $_POST['environment'] ?? '';
if (!in_array($author, $authors, true)) {
$msg = 'Nieprawidłowa wartość pola Autor.';
$msgType = 'danger';
} elseif (!in_array($envSel, $envs, true)) {
$msg = 'Nieprawidłowa wartość pola Środowisko.';
$msgType = 'danger';
} elseif (!isset($_FILES['file'])) {
$msg = 'Brak pliku w żądaniu.';
$msgType = 'danger';
} elseif ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$msg = 'Błąd uploadu do PHP (kod: ' . (int)$_FILES['file']['error'] . ').';
$msgType = 'danger';
} else {
$maxBytes = 100 * 1024 * 1024; // 100MB
$size = (int)$_FILES['file']['size'];
$origName = (string)$_FILES['file']['name'];
$filename = basename($origName);
if ($size <= 0 || $size > $maxBytes) {
$msg = 'Plik musi mieć max 100MB.';
$msgType = 'danger';
} elseif (!str_ends_with($filename, '.tar.gz')) {
$msg = 'Dozwolone są tylko pliki .tar.gz';
$msgType = 'danger';
} else {
$parsed = parseArchiveName($filename);
if (!$parsed['ok']) {
$msg = 'Nazwa pliku musi mieć format: nazwa_1.0.0_coo_openshift.tar.gz (np. s3storemanager_1.0.0_coo_openshift.tar.gz)';
$msgType = 'danger';
} else {
// dodatkowa walidacja MIME (miękka)
$tmpPath = (string)$_FILES['file']['tmp_name'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpPath) ?: 'application/octet-stream';
$allowedMimes = ['application/gzip', 'application/x-gzip', 'application/octet-stream'];
if (!in_array($mime, $allowedMimes, true)) {
$msg = 'Wygląda na to, że to nie jest gzip (MIME: ' . $mime . ').';
$msgType = 'danger';
} else {
// TAGI wymagane przez Ciebie:
$tags = [
'nazwa' => sanitizeTagValue($parsed['nazwa']),
'wersja' => sanitizeTagValue($parsed['wersja']),
'warstwa' => sanitizeTagValue($parsed['warstwa']),
'archiwum' => sanitizeTagValue($filename),
'autor' => sanitizeTagValue($author),
'srodowisko' => sanitizeTagValue($envSel),
];
// AWS SDK przyjmuje Tagging jako querystring "k=v&k2=v2"
$tagging = http_build_query($tags, '', '&', PHP_QUERY_RFC3986);
try {
$s3->putObject([
'Bucket' => $bucket,
'Key' => $filename, // obiekt w bucketcie uploads
'Body' => fopen($tmpPath, 'rb'),
'ContentType' => 'application/gzip',
'Tagging' => $tagging,
]);
$msg = 'Wgrano: ' . $filename . ' (' . humanBytes($size) . ') + zapisano tagi.';
$msgType = 'success';
} catch (Throwable $e) {
$msg = 'Błąd S3 putObject(): ' . $e->getMessage();
$msgType = 'danger';
}
}
}
}
}
}
// LISTA OBIEKTÓW
$rows = [];
$listError = null;
try {
$res = $s3->listObjectsV2([
'Bucket' => $bucket,
'MaxKeys' => 200,
]);
$objects = $res['Contents'] ?? [];
foreach ($objects as $o) {
$key = (string)$o['Key'];
// interesują nas tylko .tar.gz (dla porządku)
if (!str_ends_with($key, '.tar.gz')) {
continue;
}
$parsed = parseArchiveName($key);
// pobierz tagi (autor/środowisko i reszta)
$tagsMap = [];
try {
$tres = $s3->getObjectTagging([
'Bucket' => $bucket,
'Key' => $key,
]);
foreach (($tres['TagSet'] ?? []) as $t) {
$tagsMap[(string)$t['Key']] = (string)$t['Value'];
}
} catch (Throwable $e) {
// jeśli brak uprawnień do tagów, tabela nadal pokaże dane z nazwy pliku
$tagsMap = [];
}
$rows[] = [
'nazwa' => $tagsMap['nazwa'] ?? ($parsed['ok'] ? $parsed['nazwa'] : ''),
'wersja' => $tagsMap['wersja'] ?? ($parsed['ok'] ? $parsed['wersja'] : ''),
'warstwa' => $tagsMap['warstwa'] ?? ($parsed['ok'] ? $parsed['warstwa'] : ''),
'archiwum' => $tagsMap['archiwum'] ?? $key,
'autor' => $tagsMap['autor'] ?? '',
'srodowisko' => $tagsMap['srodowisko'] ?? '',
];
}
} catch (Throwable $e) {
$listError = $e->getMessage();
}
?>
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<title>s3store-gui</title>
</head>
<body class="container py-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h1 class="h3 m-0">s3store-gui</h1>
<div class="text-muted small">
Bucket: <code><?= htmlspecialchars($bucket) ?></code>
Endpoint: <code><?= htmlspecialchars(envv('S3_ENDPOINT')) ?></code>
</div>
</div>
<?php if ($msg): ?>
<div class="alert alert-<?= htmlspecialchars($msgType) ?>"><?= htmlspecialchars($msg) ?></div>
<?php endif; ?>
<?php if ($listError): ?>
<div class="alert alert-warning">
Nie udało się pobrać listy obiektów: <?= htmlspecialchars($listError) ?>
</div>
<?php endif; ?>
<div class="card mb-4">
<div class="card-body">
<h2 class="h5">Upload pliku (.tar.gz, max 100MB)</h2>
<form method="post" enctype="multipart/form-data" class="row g-3">
<div class="col-12">
<label class="form-label">Plik</label>
<input
class="form-control"
type="file"
name="file"
accept=".tar.gz,application/gzip,application/x-gzip"
required
>
<div class="form-text">
Przykład: <code>s3storemanager_1.0.0_coo_openshift.tar.gz</code>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Autor</label>
<select class="form-select" name="author" required>
<option value="" selected disabled>Wybierz…</option>
<?php foreach ($authors as $a): ?>
<option value="<?= htmlspecialchars($a) ?>"><?= htmlspecialchars($a) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Środowisko</label>
<select class="form-select" name="environment" required>
<option value="" selected disabled>Wybierz…</option>
<?php foreach ($envs as $e): ?>
<option value="<?= htmlspecialchars($e) ?>"><?= htmlspecialchars($e) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<button class="btn btn-primary">Wyślij do S3 (MinIO)</button>
<div class="form-text">
Serwer sprawdza rozszerzenie <code>.tar.gz</code> oraz rozmiar ≤ 100MB i dopisuje tagi: nazwa, wersja, warstwa, archiwum, autor, srodowisko.
</div>
</div>
</form>
</div>
</div>
<h2 class="h5 mb-2">Pliki w bucketcie <code><?= htmlspecialchars($bucket) ?></code></h2>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>nazwa</th>
<th>wersja</th>
<th>warstwa</th>
<th>archiwum</th>
<th>autor</th>
<th>środowisko</th>
</tr>
</thead>
<tbody>
<?php if (!$rows): ?>
<tr><td colspan="6" class="text-muted">Brak plików.</td></tr>
<?php else: ?>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= htmlspecialchars($r['nazwa']) ?></td>
<td><?= htmlspecialchars($r['wersja']) ?></td>
<td><?= htmlspecialchars($r['warstwa']) ?></td>
<td><code><?= htmlspecialchars($r['archiwum']) ?></code></td>
<td><?= htmlspecialchars($r['autor']) ?></td>
<td><?= htmlspecialchars($r['srodowisko']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>

