You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
autoflow-web-console/src/components/atoms/organisms/AutoScriptDialog.vue

1043 lines
41 KiB

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { useAutoflowStore } from "@/stores/autoflowStore";
import { storeToRefs } from "pinia";
const props = withDefaults(
defineProps<{ currentRefId?: number | null }>(),
{ currentRefId: null }
);
const emit = defineEmits<{
(e: "close-modal"): void;
(e: "generated"): void;
}>();
const { projectId } = storeToRefs(useAutoflowStore());
/** 자주 쓰는 Base Image 목록 (선택 또는 직접 입력) */
const BASE_IMAGE_PRESETS = [
"python:3.10",
"python:3.11",
"python:3.12",
"python:3.10-slim",
"python:3.11-slim",
"pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime",
"pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime",
"tensorflow/tensorflow:2.14.0",
"tensorflow/tensorflow:2.15.0",
"nvidia/cuda:12.1.0-runtime-ubuntu22.04",
"nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04",
];
const BASE_IMAGE_CUSTOM_VALUE = "__custom__";
/** Base Image: 목록 선택 값 또는 '__custom__'(직접 입력) */
const selectedBaseImage = ref("python:3.10");
function syncSelectedBaseImageFromForm() {
const v = (form.value.base_image || "").trim();
selectedBaseImage.value = BASE_IMAGE_PRESETS.includes(v) ? v : BASE_IMAGE_CUSTOM_VALUE;
}
function onBaseImageSelect(v: string) {
if (v && v !== BASE_IMAGE_CUSTOM_VALUE) {
form.value.base_image = v;
}
}
// 학습용 입력 폼 (테스트용 샘플값)
const form = ref({
base_image: "python:3.10",
env: [
{ name: "WORKSPACE", value: "/workspace" },
{ name: "LOG_LEVEL", value: "info" },
] as { name: string; value: string }[],
scriptFile: null as File[] | null,
parameters: [
{ key: "model_name", value: "sample_model" },
{ key: "data_path", value: "/data" },
] as { key: string; value: string }[],
// 스크립트 다운로드 방식: minio = MinIO 직접 접근, backend = Backend URL(curl)
downloadMethod: "minio" as "minio" | "backend",
// MinIO 직접 접근 시 Pod에서 접속할 주소 (비우면 업로드 응답의 endpoint 사용, 보통 localhost라 Pod에서 안 됨)
minioEndpointPod: "http://minio-service.kubeflow.svc.cluster.local:9000",
// MinIO 직접 접근 시 인증: "secret" = K8s Secret, "direct" = Access Key 직접 입력
minioAuthMethod: "direct" as "secret" | "direct",
minioSecretName: "mlpipeline-minio-artifact",
// 직접 입력 시 (비우면 credentials env 미포함)
minioAccessKey: "minioadmin",
minioSecretKey: "minioadmin",
// Backend URL (downloadMethod가 backend일 때만 사용)
backendUrl: "http://autoflow-server-mgmt.default.svc.cluster.local:8080",
// MLflow 사용 (체크 시 생성 YAML에 MLFLOW_TRACKING_URI 등 env 추가) — 로컬테스트 스크립트(kfp_mlflow_pipeline_v10_gpu_auth_local 등, WSL 환경)와 동일 기본값
useMlflow: false,
mlflowTrackingUri: "http://mlflow-server.kubeflow.svc.cluster.local:5000",
mlflowExperimentName: "Default",
mlflowUser: "user",
mlflowPassword: "WjWjIi13KEkO",
/** MinIO 인증: direct=직접 입력(minimal_gpu 등 다른 스크립트와 동일), secret=K8s 시크릿 이름 */
mlflowMinioAuthMethod: "direct",
/** K8s 시크릿 이름 (mlflowMinioAuthMethod가 secret일 때 사용) */
mlflowMinioSecretName: "minio-mlflow-secret",
/** 아티팩트 저장소(S3/MinIO). 직접 입력일 때 사용. minimal_gpu와 동일하게 minio-mlflow/minio-mlflow-12345. 업로드 직전 래퍼에서 AWS_*를 MLFLOW_*로 맞춤. */
mlflowS3EndpointUrl: "http://minio-mlflow.kubeflow.svc.cluster.local:9000",
mlflowAwsAccessKeyId: "minio-mlflow",
mlflowAwsSecretAccessKey: "minio-mlflow-12345",
// 자주 쓰는 파라미터 고정 필드
epochs: "10",
batch_size: "32",
learning_rate: "0.001",
});
// 환경 변수 추가/삭제
function addEnv() {
form.value.env.push({ name: "", value: "" });
}
function removeEnv(i: number) {
form.value.env.splice(i, 1);
}
// 파라미터 추가/삭제
function addParameter() {
form.value.parameters.push({ key: "", value: "" });
}
function removeParameter(i: number) {
form.value.parameters.splice(i, 1);
}
// KFP v2 IR 파이프라인 YAML — 스크립트 없을 때 (이미지 내 경로 사용, 파일 없으면 실패)
const YAML_TEMPLATE_NO_SCRIPT = `# PIPELINE DEFINITION
# Name: auto-script-training
# Description: Auto Script generated training pipeline (KFP v2).
components:
comp-train:
executorLabel: exec-train
inputDefinitions:
parameters: {}
outputDefinitions: {}
deploymentSpec:
executors:
exec-train:
container:
image: "{{ base_image }}"
{{ no_script_command_block }}
env:
{{ env_yaml }}
pipelineInfo:
name: auto-script-training
description: Auto Script generated training pipeline (KFP v2).
root:
dag:
tasks:
train:
componentRef:
name: comp-train
taskInfo:
name: train
inputDefinitions:
parameters: {}
schemaVersion: 2.1.0
sdkVersion: kfp-2.14.6
`;
// 스크립트 업로드 시: Pod에서 백엔드 URL로 curl 다운로드 후 실행
const YAML_TEMPLATE_WITH_SCRIPT = `# PIPELINE DEFINITION
# Name: auto-script-training
# Description: Auto Script generated training pipeline (KFP v2). Script downloaded at runtime.
components:
comp-train:
executorLabel: exec-train
inputDefinitions:
parameters: {}
outputDefinitions: {}
deploymentSpec:
executors:
exec-train:
container:
image: "{{ base_image }}"
command:
- sh
- -c
- |
{{ script_command_body }}
args:
{{ parameters_args }}
env:
{{ script_env }}
{{ env_yaml }}
pipelineInfo:
name: auto-script-training
description: Auto Script generated training pipeline (KFP v2).
root:
dag:
tasks:
train:
componentRef:
name: comp-train
taskInfo:
name: train
inputDefinitions:
parameters: {}
schemaVersion: 2.1.0
sdkVersion: kfp-2.14.6
`;
// 스크립트 업로드 시: Pod에서 MinIO에 직접 접근해 다운로드 후 실행 (curl/Backend 불필요)
// credentials는 command 내 export로 설정 (KFP가 container env를 적용하지 않는 경우 대비)
const YAML_TEMPLATE_MINIO_DIRECT = `# PIPELINE DEFINITION
# Name: auto-script-training
# Description: Auto Script (KFP v2). Script from MinIO direct.
components:
comp-train:
executorLabel: exec-train
inputDefinitions:
parameters: {}
outputDefinitions: {}
deploymentSpec:
executors:
exec-train:
container:
image: "{{ base_image }}"
command:
- sh
- -c
- |
{{ minio_command_body }}
args:
{{ parameters_args }}
env:
{{ minio_script_env }}
{{ env_yaml }}
pipelineInfo:
name: auto-script-training
description: Auto Script (KFP v2). Script from MinIO direct.
root:
dag:
tasks:
train:
componentRef:
name: comp-train
taskInfo:
name: train
inputDefinitions:
parameters: {}
schemaVersion: 2.1.0
sdkVersion: kfp-2.14.6
`;
const generatedYaml = ref("");
const errorMsg = ref("");
const saving = ref(false);
const scriptObjectName = ref(""); // 업로드 후 MinIO objectName (storagePath)
const scriptEntrypoint = ref("train.py"); // 실행할 파일명 (originalName)
// 폼 → KFP executor env 형식 (들여쓰기 8칸)
function envToYaml(): string {
const list = form.value.env.filter((e) => (e.name || "").trim() !== "");
if (list.length === 0) return " []";
return list
.map(
(e) =>
` - name: ${e.name}\n value: "${String(e.value).replace(/"/g, '\\"')}"`
)
.join("\n");
}
/** MLflow 사용 시 스크립트에 MLflow 코드가 없어도 run/params/metric/artifact 자동 로깅. key=value 인자를 --key value 로 바꿔 argparse 호환. (single-quote 안에 넣을 코드라 작은따옴표 사용 금지) */
function getMlflowWrapperCode(): string {
return [
"import os, sys, json, runpy",
"import mlflow",
"uri = os.environ.get(\"MLFLOW_TRACKING_URI\")",
"if uri: mlflow.set_tracking_uri(uri)",
"exp = os.environ.get(\"MLFLOW_EXPERIMENT_NAME\")",
"if exp: mlflow.set_experiment(exp)",
"with mlflow.start_run():",
" kfp_run_id = os.environ.get(\"KFP_RUN_ID\", \"\").strip()",
" if kfp_run_id: mlflow.set_tag(\"kubeflow_run_id\", kfp_run_id)",
" print(\"[MLflow] KFP_RUN_ID=\", repr(kfp_run_id), \"run_id=\", mlflow.active_run().info.run_id if mlflow.active_run() else None, flush=True)",
" raw = sys.argv[2:]",
" for arg in raw:",
" if \"=\" in arg:",
" k, v = arg.split(\"=\", 1)",
" mlflow.log_param(k, v)",
" argv = [sys.argv[1]]",
" for arg in raw:",
" if \"=\" in arg:",
" k, v = arg.split(\"=\", 1)",
" argv.extend([\"--\" + k, v])",
" sys.argv = argv",
" runpy.run_path(sys.argv[0], run_name=\"__main__\")",
" for p in [\"/workspace/outputs/metrics.json\", \"/workspace/metrics.json\"]:",
" if os.path.exists(p):",
" try:",
" with open(p) as f: d = json.load(f)",
" for k, v in d.items():",
" if isinstance(v, (int, float)): mlflow.log_metric(k, v)",
" except Exception: pass",
" break",
" try:",
" if os.path.isdir(\"/workspace/outputs\"):",
" ak = os.environ.get(\"MLFLOW_AWS_ACCESS_KEY_ID\") or os.environ.get(\"AWS_ACCESS_KEY_ID\", \"\")",
" sk = os.environ.get(\"MLFLOW_AWS_SECRET_ACCESS_KEY\") or os.environ.get(\"AWS_SECRET_ACCESS_KEY\", \"\")",
" if ak: os.environ[\"AWS_ACCESS_KEY_ID\"] = ak",
" if sk: os.environ[\"AWS_SECRET_ACCESS_KEY\"] = sk",
" mlflow.log_artifacts(\"/workspace/outputs\", artifact_path=\"outputs\")",
" print(\"[MLflow] artifacts uploaded from /workspace/outputs\", flush=True)",
" except Exception as e:",
" print(\"[MLflow] artifact upload failed:\", e, flush=True)",
].join("\n");
}
/** run 명령: useWrapper면 MLflow 래퍼로 실행(파라미터/아티팩트 자동 로깅), 아니면 exec python만 */
function getMlflowRunCommand(scriptPath: string, useWrapper: boolean): string {
if (!useWrapper) return "exec python " + scriptPath + ' "$@"';
const code = getMlflowWrapperCode();
const indented = code.split("\n").map((l) => " " + l).join("\n");
return "python -c '\n" + indented + "\n ' " + scriptPath + ' "$@"';
}
/** MLflow 사용 시 env에 추가할 YAML 블록 (들여쓰기 8칸) */
function mlflowEnvToYaml(): string {
if (!form.value.useMlflow) return "";
const uri = (form.value.mlflowTrackingUri || "").trim();
if (!uri) return "";
const lines: string[] = [];
lines.push(` - name: MLFLOW_TRACKING_URI\n value: "${String(uri).replace(/"/g, '\\"')}"`);
const exp = (form.value.mlflowExperimentName || "").trim();
if (exp) lines.push(` - name: MLFLOW_EXPERIMENT_NAME\n value: "${String(exp).replace(/"/g, '\\"')}"`);
const user = (form.value.mlflowUser || "").trim();
if (user) lines.push(` - name: MLFLOW_TRACKING_USERNAME\n value: "${String(user).replace(/"/g, '\\"')}"`);
const pass = (form.value.mlflowPassword || "").trim();
if (pass) lines.push(` - name: MLFLOW_TRACKING_PASSWORD\n value: "${String(pass).replace(/"/g, '\\"')}"`);
lines.push(" - name: KFP_RUN_ID\n value: \"{{workflow.uid}}\"");
// 아티팩트: S3 엔드포인트는 공통, 자격증명은 직접 입력 또는 K8s 시크릿 참조
const s3Endpoint = (form.value.mlflowS3EndpointUrl || "").trim() || "http://minio-mlflow.kubeflow.svc.cluster.local:9000";
lines.push(` - name: MLFLOW_S3_ENDPOINT_URL\n value: "${String(s3Endpoint).replace(/"/g, '\\"')}"`);
if (form.value.mlflowMinioAuthMethod === "secret") {
const secretName = (form.value.mlflowMinioSecretName || "").trim() || "minio-mlflow-secret";
lines.push(` - name: MLFLOW_AWS_ACCESS_KEY_ID\n valueFrom:\n secretKeyRef:\n name: ${secretName}\n key: accesskey`);
lines.push(` - name: MLFLOW_AWS_SECRET_ACCESS_KEY\n valueFrom:\n secretKeyRef:\n name: ${secretName}\n key: secretkey`);
} else {
const s3Key = (form.value.mlflowAwsAccessKeyId || "").trim() || "minio-mlflow";
const s3Secret = (form.value.mlflowAwsSecretAccessKey || "").trim() || "minio-mlflow-12345";
lines.push(` - name: MLFLOW_AWS_ACCESS_KEY_ID\n value: "${String(s3Key).replace(/"/g, '\\"')}"`);
lines.push(` - name: MLFLOW_AWS_SECRET_ACCESS_KEY\n value: "${String(s3Secret).replace(/"/g, '\\"')}"`);
}
return lines.length > 0 ? lines.join("\n") : "";
}
// 폼 → executor args (key=value 리스트, 들여쓰기 8칸)
function parametersToArgs(): string {
const fixed: Record<string, string> = {
epochs: form.value.epochs,
batch_size: form.value.batch_size,
learning_rate: form.value.learning_rate,
};
const extra = form.value.parameters.filter((p) => (p.key || "").trim() !== "");
const merged = { ...fixed };
extra.forEach((p) => (merged[p.key] = p.value));
const entries = Object.entries(merged).filter(([, v]) => v !== undefined && v !== "");
if (entries.length === 0) return " []";
return entries.map(([k, v]) => ` - "${k}=${v}"`).join("\n");
}
async function generateYaml() {
errorMsg.value = "";
try {
const base_image = (form.value.base_image || "python:3.10").trim();
const env_yaml = envToYaml();
const parameters_args = parametersToArgs();
const fileObj = Array.isArray(form.value.scriptFile)
? form.value.scriptFile[0]
: form.value.scriptFile;
const backendUrl = (form.value.backendUrl || "").trim();
const downloadMethod = form.value.downloadMethod || "minio";
if (fileObj && projectId.value) {
const regUserId = getRegUserId();
const originalName = fileObj.name || "train.py";
// 파이썬 파일은 원본 이름 그대로 저장 → entrypoint가 train.py 등으로 맞아서 파이프라인에서 로드 가능
const fd = new FormData();
fd.append("refId", String(props.currentRefId ?? 0));
fd.append("refType", "TRAINING_SCRIPT");
fd.append("title", "Script - " + originalName);
fd.append("description", "Auto Script 학습용 스크립트");
fd.append("version", "1");
fd.append("regUserId", regUserId);
fd.append("projectId", String(projectId.value));
fd.append("file", fileObj, originalName);
const res = await AttachmentsService.upload(fd as any);
const data = res?.data ?? res;
const attachment = data?.attachment ?? data;
const storagePath = attachment?.storagePath ?? attachment?.storage_path;
const entrypointName = attachment?.originalName ?? attachment?.original_name ?? originalName ?? "train.py";
if (!storagePath) {
errorMsg.value = "스크립트 업로드 후 저장 경로를 가져오지 못했습니다.";
return;
}
scriptObjectName.value = storagePath;
scriptEntrypoint.value = entrypointName;
const envBase = form.value.env.filter((e) => (e.name || "").trim() !== "").length > 0 ? env_yaml : "";
const mlflowBlock = mlflowEnvToYaml();
const env_yaml_extra = [envBase, mlflowBlock].filter(Boolean).join("\n") || " []";
const needMlflowInstall = !!(form.value.useMlflow && (form.value.mlflowTrackingUri || "").trim());
if (downloadMethod === "minio") {
const minioBucket = (data?.minioBucket ?? data?.minio_bucket ?? "").trim();
const endpointFromBackend = (data?.minioEndpoint ?? data?.minio_endpoint ?? "").trim();
const endpointPod = (form.value.minioEndpointPod ?? "").trim();
const minioEndpoint = endpointPod || endpointFromBackend;
if (!minioBucket) {
errorMsg.value = "업로드 응답에 MinIO bucket이 없습니다. 백엔드 설정을 확인하세요.";
return;
}
if (!minioEndpoint) {
errorMsg.value = "MinIO Endpoint(Pod용)를 입력하거나, 업로드 응답에 minioEndpoint가 있어야 합니다.";
return;
}
let minio_script_env =
` - name: S3_ENDPOINT\n value: "${String(minioEndpoint).replace(/"/g, '\\"')}"\n` +
` - name: S3_BUCKET\n value: "${String(minioBucket).replace(/"/g, '\\"')}"\n` +
` - name: SCRIPT_OBJECT_KEY\n value: "${String(storagePath).replace(/"/g, '\\"')}"\n`;
const authMethod = form.value.minioAuthMethod || "direct";
const directAccessKey = (form.value.minioAccessKey ?? "").trim();
const directSecretKey = (form.value.minioSecretKey ?? "").trim();
const secretName = (form.value.minioSecretName || "").trim();
const ak = directAccessKey || "minioadmin";
const sk = directSecretKey || "minioadmin";
if (authMethod === "secret" && secretName) {
minio_script_env +=
` - name: AWS_ACCESS_KEY_ID\n valueFrom:\n secretKeyRef:\n name: ${secretName}\n key: accesskey\n` +
` - name: AWS_SECRET_ACCESS_KEY\n valueFrom:\n secretKeyRef:\n name: ${secretName}\n key: secretkey\n`;
} else {
minio_script_env +=
` - name: AWS_ACCESS_KEY_ID\n value: "${String(ak).replace(/"/g, '\\"')}"\n` +
` - name: AWS_SECRET_ACCESS_KEY\n value: "${String(sk).replace(/"/g, '\\"')}"\n`;
}
// command 내 export용: 셸 single-quote 안에서 쓰이므로 ' -> '\''
const shellEsc = (s: string) => String(s).replace(/'/g, "'\\''");
const ep = scriptEntrypoint.value;
const scriptPathMinio = "/workspace/" + ep;
const minio_command_body =
" set -e\n" +
" export AWS_ACCESS_KEY_ID='" + shellEsc(ak) + "'\n" +
" export AWS_SECRET_ACCESS_KEY='" + shellEsc(sk) + "'\n" +
" pip install -q awscli\n" +
" aws s3 cp s3://$S3_BUCKET/$SCRIPT_OBJECT_KEY " + scriptPathMinio + " --endpoint-url $S3_ENDPOINT\n" +
(needMlflowInstall ? " echo 'numpy<2' > /tmp/constraints.txt && pip install -q -c /tmp/constraints.txt \"numpy>=1.24,<2\" \"pyarrow>=13,<15\" mlflow boto3\n " : "") +
" " + getMlflowRunCommand(scriptPathMinio, needMlflowInstall);
let yaml = YAML_TEMPLATE_MINIO_DIRECT.replace(/\{\{\s*base_image\s*\}\}/g, base_image)
.replace(/\{\{\s*env_yaml\s*\}\}/g, env_yaml_extra)
.replace(/\{\{\s*parameters_args\s*\}\}/g, parameters_args)
.replace(/\{\{\s*entrypoint\s*\}\}/g, ep)
.replace(/\{\{\s*minio_script_env\s*\}\}/g, minio_script_env)
.replace(/\{\{\s*minio_command_body\s*\}\}/g, minio_command_body);
generatedYaml.value = yaml;
} else {
if (!backendUrl) {
errorMsg.value = "Backend URL 방식을 사용하려면 'Backend URL'을 입력하세요.";
return;
}
const script_env =
` - name: SCRIPT_OBJECT_NAME\n value: "${String(storagePath).replace(/"/g, '\\"')}"\n - name: BACKEND_URL\n value: "${String(backendUrl).replace(/"/g, '\\"')}"\n`;
const epBackend = scriptEntrypoint.value;
const scriptPathBackend = "/workspace/" + epBackend;
const script_command_body =
" set -e\n" +
' curl -s -o ' + scriptPathBackend + ' "$BACKEND_URL/api/attachments/download?objectName=$SCRIPT_OBJECT_NAME"\n' +
(needMlflowInstall ? " echo 'numpy<2' > /tmp/constraints.txt && pip install -q -c /tmp/constraints.txt \"numpy>=1.24,<2\" \"pyarrow>=13,<15\" mlflow boto3\n " : "") +
" " + getMlflowRunCommand(scriptPathBackend, needMlflowInstall);
let yaml = YAML_TEMPLATE_WITH_SCRIPT.replace(/\{\{\s*base_image\s*\}\}/g, base_image)
.replace(/\{\{\s*env_yaml\s*\}\}/g, env_yaml_extra)
.replace(/\{\{\s*parameters_args\s*\}\}/g, parameters_args)
.replace(/\{\{\s*entrypoint\s*\}\}/g, epBackend)
.replace(/\{\{\s*script_env\s*\}\}/g, script_env)
.replace(/\{\{\s*script_command_body\s*\}\}/g, script_command_body);
generatedYaml.value = yaml;
}
} else {
const entrypoint = scriptEntrypoint.value || "train.py";
if (fileObj && !projectId.value) {
errorMsg.value = "스크립트를 사용하려면 프로젝트를 선택하세요.";
return;
}
const noScriptEnvBase = envToYaml();
const noScriptMlflow = mlflowEnvToYaml();
const noScriptEnv = [noScriptEnvBase, noScriptMlflow].filter(Boolean).join("\n") || " []";
const needMlflowInstallNoScript = !!(form.value.useMlflow && (form.value.mlflowTrackingUri || "").trim());
const noScriptRunCmd = getMlflowRunCommand(entrypoint, needMlflowInstallNoScript);
const no_script_command_block = needMlflowInstallNoScript
? " command:\n - sh\n - -c\n - |\n set -e\n echo 'numpy<2' > /tmp/constraints.txt && pip install -q -c /tmp/constraints.txt \"numpy>=1.24,<2\" \"pyarrow>=13,<15\" mlflow boto3\n " + noScriptRunCmd + "\n args:\n" + parameters_args
: " command:\n - python\n - \"" + entrypoint + "\"\n args:\n" + parameters_args;
let yaml = YAML_TEMPLATE_NO_SCRIPT.replace(/\{\{\s*base_image\s*\}\}/g, base_image)
.replace(/\{\{\s*env_yaml\s*\}\}/g, noScriptEnv)
.replace(/\{\{\s*parameters_args\s*\}\}/g, parameters_args)
.replace(/\{\{\s*entrypoint\s*\}\}/g, entrypoint)
.replace(/\{\{\s*no_script_command_block\s*\}\}/g, no_script_command_block);
generatedYaml.value = yaml;
}
} catch (e: any) {
console.error(e);
const err = e as { response?: { data?: { error?: string }; status?: number }; message?: string };
const backendError = err.response?.data?.error;
if (backendError) {
errorMsg.value = "스크립트 업로드 실패: " + backendError;
} else {
errorMsg.value = "YAML 생성 중 오류가 발생했습니다. " + (err?.message ?? String(e));
}
}
}
function downloadYaml() {
if (!generatedYaml.value) {
errorMsg.value = "먼저 [YAML 생성]을 실행해주세요.";
return;
}
const blob = new Blob([generatedYaml.value], { type: "text/yaml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `training-pipeline_${getTimestampForFileName()}.yaml`;
a.click();
URL.revokeObjectURL(url);
}
async function saveAsTrainingScript() {
if (!generatedYaml.value) {
errorMsg.value = "먼저 [YAML 생성]을 실행해주세요.";
return;
}
if (!projectId.value) {
errorMsg.value = "프로젝트를 선택해주세요.";
return;
}
errorMsg.value = "";
try {
saving.value = true;
const ts = getTimestampForFileName();
const yamlFileName = `training-pipeline_${ts}.yaml`;
const blob = new Blob([generatedYaml.value], { type: "text/yaml" });
const file = new File([blob], yamlFileName, { type: "text/yaml" });
const fd = new FormData();
const regUserId = getRegUserId();
fd.append("refId", String(props.currentRefId ?? 0));
fd.append("refType", "TRAINING_SCRIPT");
fd.append("title", "Auto Script - " + yamlFileName);
fd.append("description", "Auto Script로 생성된 학습 YAML");
fd.append("version", "1");
fd.append("regUserId", regUserId);
fd.append("projectId", String(projectId.value));
fd.append("file", file);
await AttachmentsService.upload(fd as any);
emit("generated");
emit("close-modal");
} catch (e) {
console.error(e);
errorMsg.value = "Training Script 저장에 실패했습니다.";
} finally {
saving.value = false;
}
}
/** 파일명에 쓸 일시 문자열 (예: 20260303_143052) */
function getTimestampForFileName(): string {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, "0");
const d = String(now.getDate()).padStart(2, "0");
const h = String(now.getHours()).padStart(2, "0");
const min = String(now.getMinutes()).padStart(2, "0");
const s = String(now.getSeconds()).padStart(2, "0");
return `${y}${m}${d}_${h}${min}${s}`;
}
function getRegUserId(): string {
try {
const raw = localStorage.getItem("autoflow-auth") || "{}";
const auth = JSON.parse(raw);
return (
auth?.userInfo?.username ??
auth?.userinfo?.username ??
auth?.username ??
""
);
} catch {
return "";
}
}
function onEsc(e: KeyboardEvent) {
if (e.key === "Escape") emit("close-modal");
}
/** 스크립트 저장 시 YAML에 반영할 MinIO 설정을 백엔드에서 불러와 폼에 채움 (직접 입력 불필요) */
async function loadMinioConfig() {
try {
const res = await AttachmentsService.getMinioConfig();
const data = (res?.data ?? res) as Record<string, string> | undefined;
if (!data) return;
if ((data.minioEndpointPod ?? "").trim()) form.value.minioEndpointPod = (data.minioEndpointPod ?? "").trim();
if ((data.minioAccessKey ?? "").trim()) form.value.minioAccessKey = (data.minioAccessKey ?? "").trim();
if ((data.minioSecretKey ?? "").trim()) form.value.minioSecretKey = (data.minioSecretKey ?? "").trim();
} catch {
// 설정 API 없거나 실패 시 폼 기본값 유지
}
}
/** MLflow 설정을 백엔드에서 불러와 폼에 채움. localhost는 Pod에서 접근 불가하므로 덮어쓰지 않음 */
async function loadMlflowConfig() {
try {
const res = await AttachmentsService.getMlflowConfig();
const data = (res?.data ?? res) as Record<string, string> | undefined;
if (!data) return;
const uri = (data.mlflowTrackingUri ?? "").trim();
if (uri && !/^https?:\/\/localhost(\b|:)/i.test(uri)) form.value.mlflowTrackingUri = uri;
if ((data.mlflowUser ?? "").trim()) form.value.mlflowUser = (data.mlflowUser ?? "").trim();
if ((data.mlflowPassword ?? "").trim()) form.value.mlflowPassword = (data.mlflowPassword ?? "").trim();
} catch {
// 설정 API 없거나 실패 시 폼 기본값 유지
}
}
onMounted(() => {
addEnv();
addParameter();
syncSelectedBaseImageFromForm();
loadMinioConfig();
loadMlflowConfig();
window.addEventListener("keydown", onEsc);
});
onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
Auto Script KFP 학습용 파이프라인 YAML 생성
</v-card-title>
<v-card-text class="pa-6" style="max-height: 70vh; overflow-y: auto">
<v-form>
<!-- Base Image: 목록 선택 또는 직접 입력 -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Base Image</label
>
<v-select
v-model="selectedBaseImage"
:items="[
...BASE_IMAGE_PRESETS.map((t) => ({ title: t, value: t })),
{ title: '직접 입력', value: BASE_IMAGE_CUSTOM_VALUE },
]"
item-title="title"
item-value="value"
variant="outlined"
density="compact"
hide-details
@update:model-value="onBaseImageSelect"
/>
<v-text-field
v-if="selectedBaseImage === BASE_IMAGE_CUSTOM_VALUE"
v-model="form.base_image"
variant="outlined"
density="compact"
hide-details
placeholder="예: python:3.10, my-registry.io/my-image:tag"
class="mt-2"
/>
</div>
<!-- 환경 변수 -->
<div class="mb-4">
<div class="d-flex align-center justify-space-between mb-2">
<label class="text-subtitle-2 font-weight-medium"
>환경 변수</label
>
<v-btn size="small" variant="tonal" @click="addEnv"
>+ 추가</v-btn
>
</div>
<div
v-for="(row, i) in form.env"
:key="i"
class="d-flex align-center gap-2 mb-2"
>
<v-text-field
v-model="row.name"
variant="outlined"
density="compact"
hide-details
placeholder="NAME"
class="flex-grow-1"
/>
<v-text-field
v-model="row.value"
variant="outlined"
density="compact"
hide-details
placeholder="value"
class="flex-grow-1"
/>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="removeEnv(i)"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
</div>
</div>
<!-- 학습 스크립트 업로드 (선택) -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>학습 스크립트 (선택) — train.py 등</label
>
<v-file-input
v-model="form.scriptFile"
label="파일 선택"
variant="outlined"
density="compact"
hide-details
accept=".py,.sh"
clearable
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
선택 시 스크립트는 MinIO에 업로드됩니다. 아래 방식 중 하나로 Pod가 스크립트를 받아옵니다.
</p>
</div>
<!-- MLflow 사용 -->
<div class="mb-4">
<v-checkbox
v-model="form.useMlflow"
label="MLflow 사용 (생성 YAML에 MLFLOW_TRACKING_URI 등 env 추가)"
density="compact"
hide-details
class="mt-0"
/>
<template v-if="form.useMlflow">
<v-text-field
v-model="form.mlflowTrackingUri"
label="MLflow Tracking URI"
variant="outlined"
density="compact"
hide-details
placeholder="http://mlflow-server.kubeflow.svc.cluster.local:5000"
class="mt-2"
/>
<v-text-field
v-model="form.mlflowExperimentName"
label="MLflow Experiment Name (선택)"
variant="outlined"
density="compact"
hide-details
placeholder="Default"
class="mt-2"
/>
<v-text-field
v-model="form.mlflowUser"
label="MLflow Username (Basic Auth 사용 시에만)"
variant="outlined"
density="compact"
hide-details
placeholder="admin"
class="mt-2"
/>
<v-text-field
v-model="form.mlflowPassword"
label="MLflow Password (Basic Auth 사용 시에만)"
type="password"
variant="outlined"
density="compact"
hide-details
placeholder="비밀번호 입력"
class="mt-2"
/>
<v-radio-group v-model="form.mlflowMinioAuthMethod" hide-details density="compact" class="mt-2">
<v-radio label="K8s 시크릿 이름 (MLflow 아티팩트용)" value="secret" />
<v-radio label="직접 입력 (기본값 사용)" value="direct" />
</v-radio-group>
<v-text-field
v-if="form.mlflowMinioAuthMethod === 'secret'"
v-model="form.mlflowMinioSecretName"
label="아티팩트 시크릿 이름"
variant="outlined"
density="compact"
hide-details
placeholder="minio-mlflow-secret"
class="mt-2"
/>
<template v-if="form.mlflowMinioAuthMethod === 'direct'">
<v-text-field
v-model="form.mlflowAwsAccessKeyId"
label="아티팩트 Access Key"
variant="outlined"
density="compact"
hide-details
class="mt-2"
/>
<v-text-field
v-model="form.mlflowAwsSecretAccessKey"
label="아티팩트 Secret Key"
type="password"
variant="outlined"
density="compact"
hide-details
class="mt-2"
/>
</template>
<v-text-field
v-model="form.mlflowS3EndpointUrl"
label="아티팩트 S3 엔드포인트"
variant="outlined"
density="compact"
hide-details
placeholder="http://minio-mlflow.kubeflow.svc.cluster.local:9000"
class="mt-2"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
다이얼로그 열 때 서버 설정에서 불러옵니다. Pod에서 접근 가능한 URI를 입력하세요.
</p>
</template>
</div>
<!-- 스크립트 다운로드 방식 -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block"
>스크립트 다운로드 방식</label
>
<v-radio-group v-model="form.downloadMethod" hide-details density="compact" class="mt-0">
<v-radio label="MinIO 직접 접근 (권장)" value="minio" />
<v-radio label="Backend URL (curl)" value="backend" />
</v-radio-group>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
MinIO 직접 접근: Pod가 저장소(MinIO)에 바로 접속해 받습니다. Backend URL 불필요.
</p>
</div>
<!-- MinIO Endpoint Pod용 (MinIO 직접 접근 시) -->
<div v-if="form.downloadMethod === 'minio'" class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>MinIO Endpoint (Pod에서 접속할 주소)</label
>
<v-text-field
v-model="form.minioEndpointPod"
variant="outlined"
density="compact"
hide-details
placeholder="http://minio-service.kubeflow.svc.cluster.local:9000"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
다이얼로그 열 때 서버 설정(minio.endpoint.pod)에서 불러옵니다. 필요 시 수정하세요.
</p>
</div>
<!-- MinIO 인증 (MinIO 직접 접근 시) -->
<div v-if="form.downloadMethod === 'minio'" class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block"
>MinIO 인증</label
>
<v-radio-group v-model="form.minioAuthMethod" hide-details density="compact" class="mt-0 mb-2">
<v-radio label="직접 입력 (Access Key / Secret Key)" value="direct" />
<v-radio label="K8s 시크릿 이름 (스크립트 다운로드용)" value="secret" />
</v-radio-group>
<template v-if="form.minioAuthMethod === 'direct'">
<v-text-field
v-model="form.minioAccessKey"
label="MinIO Access Key"
variant="outlined"
density="compact"
hide-details
class="mb-2"
placeholder="minioadmin"
/>
<v-text-field
v-model="form.minioSecretKey"
label="MinIO Secret Key"
type="password"
variant="outlined"
density="compact"
hide-details
placeholder="minioadmin"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
다이얼로그 열 때 서버 설정에서 불러와 YAML에 반영됩니다. 필요 시 수정하세요.
</p>
</template>
<template v-else>
<v-text-field
v-model="form.minioSecretName"
label="시크릿 이름 (스크립트 다운로드)"
variant="outlined"
density="compact"
hide-details
placeholder="mlpipeline-minio-artifact"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
KFP 기본: mlpipeline-minio-artifact (키: accesskey, secretkey). 실행 네임스페이스에 시크릿이 있어야 합니다.
</p>
</template>
</div>
<!-- Backend URL (Backend URL 방식일 때만) -->
<div v-if="form.downloadMethod === 'backend'" class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Backend URL (스크립트 다운로드용)</label
>
<v-text-field
v-model="form.backendUrl"
variant="outlined"
density="compact"
hide-details
placeholder="http://autoflow-server-mgmt.default.svc.cluster.local:8080"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
Pod가 이 주소로 스크립트를 받아옵니다. 클러스터 내 접근 가능한 백엔드 주소를 입력하세요.
</p>
</div>
<!-- 고정 파라미터 -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block"
>학습 파라미터</label
>
<v-row dense>
<v-col cols="4">
<v-text-field
v-model="form.epochs"
variant="outlined"
density="compact"
hide-details
label="epochs"
/>
</v-col>
<v-col cols="4">
<v-text-field
v-model="form.batch_size"
variant="outlined"
density="compact"
hide-details
label="batch_size"
/>
</v-col>
<v-col cols="4">
<v-text-field
v-model="form.learning_rate"
variant="outlined"
density="compact"
hide-details
label="learning_rate"
/>
</v-col>
</v-row>
</div>
<!-- 추가 파라미터 (키-값) -->
<div class="mb-4">
<div class="d-flex align-center justify-space-between mb-2">
<label class="text-subtitle-2 font-weight-medium"
>추가 파라미터</label
>
<v-btn size="small" variant="tonal" @click="addParameter"
>+ 추가</v-btn
>
</div>
<div
v-for="(row, i) in form.parameters"
:key="i"
class="d-flex align-center gap-2 mb-2"
>
<v-text-field
v-model="row.key"
variant="outlined"
density="compact"
hide-details
placeholder="key"
class="flex-grow-1"
/>
<v-text-field
v-model="row.value"
variant="outlined"
density="compact"
hide-details
placeholder="value"
class="flex-grow-1"
/>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="removeParameter(i)"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
</div>
</div>
<!-- YAML 생성 버튼 -->
<div class="mb-3">
<v-btn color="primary" @click="generateYaml">YAML 생성</v-btn>
</div>
<!-- 생성된 YAML 미리보기 -->
<div v-if="generatedYaml" class="mb-3">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>생성된 YAML</label
>
<v-textarea
:model-value="generatedYaml"
variant="outlined"
readonly
rows="12"
hide-details
class="mono"
/>
</div>
<div v-if="errorMsg" class="text-error mb-2">{{ errorMsg }}</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end flex-wrap" style="padding: 16px 24px">
<v-btn
v-if="generatedYaml"
color="success"
variant="tonal"
:disabled="saving"
@click="downloadYaml"
>
다운로드
</v-btn>
<v-btn
v-if="generatedYaml"
color="success"
:loading="saving"
@click="saveAsTrainingScript"
>
Training Script로 저장
</v-btn>
<v-btn variant="text" :disabled="saving" @click="emit('close-modal')">
</v-btn>
</v-card-actions>
</v-card>
</template>
<style scoped>
.mono {
font-family: ui-monospace, monospace;
font-size: 0.85em;
}
</style>