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
'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();
}
?>
s3store-gui
s3store-gui
Bucket: = htmlspecialchars($bucket) ?>
Endpoint: = htmlspecialchars(envv('S3_ENDPOINT')) ?>
">= htmlspecialchars($msg) ?>
Nie udało się pobrać listy obiektów: = htmlspecialchars($listError) ?>
Upload pliku (.tar.gz, max 100MB)
Pliki w bucketcie = htmlspecialchars($bucket) ?>
nazwa
wersja
warstwa
archiwum
autor
środowisko
Brak plików.
= htmlspecialchars($r['nazwa']) ?>
= htmlspecialchars($r['wersja']) ?>
= htmlspecialchars($r['warstwa']) ?>
= htmlspecialchars($r['archiwum']) ?>
= htmlspecialchars($r['autor']) ?>
= htmlspecialchars($r['srodowisko']) ?>

