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.
1043 lines
41 KiB
1043 lines
41 KiB
|
4 weeks ago
|
<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>
|