Files
invyone/backend-spring/src/main/java/com/erp/provisioning/SchemaCopier.java
T
gbpark 8be7e16e56
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m28s
서브도메인설정
2026-04-24 04:56:40 +09:00

133 lines
5.3 KiB
Java

package com.erp.provisioning;
import com.erp.tenant.TenantDbSettings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
/**
* pg_dump → psql 파이프로 스키마 복제.
*
* ★ 보안 강화 (2026-04-24 Codex 리뷰 반영):
* - 이전 버전: "sh -c 'pg_dump ... | psql ...'" 로 쉘 문자열 구성 → command injection 표면.
* - 현재: ProcessBuilder 에 각 인자를 배열로 전달 + JVM 내부에서 stdout→stdin 파이프.
* - DB 이름은 ^[a-z][a-z0-9_]{{2,40}}$ 화이트리스트 재검증.
* - PGPASSWORD 는 여전히 env 로 전달. (.pgpass 전환은 별도 작업)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SchemaCopier {
private static final Pattern SAFE_DB_NAME = Pattern.compile("^[a-z][a-z0-9_]{2,40}$");
private static final int BUFFER_SIZE = 64 * 1024;
private static final long TIMEOUT_MILLIS = 5 * 60 * 1000; // 5분
private final TenantDbSettings settings;
public void copySchema(String srcDb, String dstDb) throws IOException, InterruptedException {
requireSafeDbName(srcDb);
requireSafeDbName(dstDb);
ProcessBuilder dumpPb = new ProcessBuilder(
"pg_dump",
"-h", settings.host(),
"-p", String.valueOf(settings.port()),
"-U", settings.username(),
"-s", // schema only
"--no-owner",
"--no-privileges",
srcDb
);
dumpPb.environment().put("PGPASSWORD", settings.password());
dumpPb.redirectErrorStream(false);
ProcessBuilder restorePb = new ProcessBuilder(
"psql",
"-h", settings.host(),
"-p", String.valueOf(settings.port()),
"-U", settings.username(),
"-d", dstDb,
"-v", "ON_ERROR_STOP=1"
);
restorePb.environment().put("PGPASSWORD", settings.password());
restorePb.redirectErrorStream(false);
Process dump = dumpPb.start();
Process restore = restorePb.start();
// dump stdout -> restore stdin
Thread pipe = new Thread(() -> {
try (InputStream in = dump.getInputStream();
OutputStream out = restore.getOutputStream()) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
} catch (IOException e) {
log.warn("[Provisioning] pipe error (dump→psql): {}", e.getMessage());
}
}, "schema-copy-pipe");
pipe.setDaemon(true);
// stderr 캡처 (비동기)
StringBuilder dumpErr = new StringBuilder();
StringBuilder restoreErr = new StringBuilder();
Thread dumpErrT = asyncCollect(dump.getErrorStream(), dumpErr, "pg_dump-stderr");
Thread restoreErrT = asyncCollect(restore.getErrorStream(), restoreErr, "psql-stderr");
pipe.start();
dumpErrT.start();
restoreErrT.start();
long deadline = System.currentTimeMillis() + TIMEOUT_MILLIS;
boolean dumpDone = dump.waitFor(TIMEOUT_MILLIS, java.util.concurrent.TimeUnit.MILLISECONDS);
if (!dumpDone) { dump.destroyForcibly(); restore.destroyForcibly(); throw new IOException("pg_dump timed out"); }
long remaining = Math.max(1, deadline - System.currentTimeMillis());
boolean restoreDone = restore.waitFor(remaining, java.util.concurrent.TimeUnit.MILLISECONDS);
if (!restoreDone) { restore.destroyForcibly(); throw new IOException("psql timed out"); }
pipe.join(5_000);
dumpErrT.join(5_000);
restoreErrT.join(5_000);
int dumpExit = dump.exitValue();
int restoreExit = restore.exitValue();
if (dumpExit != 0 || restoreExit != 0) {
String dumpLog = dumpErr.length() > 2000 ? dumpErr.substring(0, 2000) + "" : dumpErr.toString();
String restoreLog = restoreErr.length() > 2000 ? restoreErr.substring(0, 2000) + "" : restoreErr.toString();
throw new IOException(String.format(
"schema copy failed (dumpExit=%d, restoreExit=%d)\n--pg_dump stderr--\n%s\n--psql stderr--\n%s",
dumpExit, restoreExit, dumpLog, restoreLog));
}
log.info("[Provisioning] COPY SCHEMA: {} -> {}", srcDb, dstDb);
}
private static void requireSafeDbName(String name) {
if (name == null || !SAFE_DB_NAME.matcher(name).matches()) {
throw new IllegalArgumentException("unsafe db name: " + name);
}
}
private static Thread asyncCollect(InputStream in, StringBuilder sink, String threadName) {
Thread t = new Thread(() -> {
try (InputStream is = in) {
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) {
sink.append(new String(buf, 0, n, StandardCharsets.UTF_8));
if (sink.length() > 10_000) break; // runaway 방어
}
} catch (IOException ignored) { }
}, threadName);
t.setDaemon(true);
return t;
}
}