fix ui and docker compose script

This commit is contained in:
Kohaku-Blueleaf
2025-10-11 00:14:23 +08:00
parent 25341dfe2f
commit 7173f1c598
7 changed files with 225 additions and 28 deletions

View File

@@ -51,6 +51,11 @@ python scripts/generate_docker_compose.py --config kohakuhub.conf
- Auto-generated admin secret token
- Option to use custom secrets
5. **Network:**
- External Docker bridge network support
- Allows cross-compose communication with external PostgreSQL/S3 services
- Automatically added to hub-api and lakefs when using external services
**Example: Interactive Mode**
```
@@ -197,6 +202,10 @@ secret_key = minioadmin
[security]
session_secret = your-secret-here
admin_secret = your-admin-secret
[network]
# Optional: for cross-compose communication
external_network = shared-network
```
For external PostgreSQL or S3:
@@ -215,8 +224,40 @@ endpoint = https://your-s3-endpoint.com
access_key = your-access-key
secret_key = your-secret-key
region = us-east-1
[network]
# Required if external services are in different Docker Compose
external_network = shared-network
```
**Using External Docker Network:**
If PostgreSQL or S3 are in separate Docker Compose setups, you need a shared network:
```bash
# Create the shared network first
docker network create shared-network
# Your PostgreSQL docker-compose.yml
services:
postgres:
# ... your config
networks:
- shared-network
networks:
shared-network:
external: true
# Generate KohakuHub with external network
python scripts/generate_docker_compose.py --config kohakuhub.conf
```
The generator will automatically:
- Add the external network to `hub-api` and `lakefs` services
- Configure them to use both `default` (hub-net) and the external network
- Allow container name resolution across compose files
**Important Notes:**
- Shell scripts automatically use LF line endings (configured in `.gitattributes`)
- Database initialization runs automatically on LakeFS startup

View File

@@ -146,7 +146,9 @@ def generate_lakefs_service(config: dict) -> str:
force_path_style = "true"
else:
s3_endpoint = config["s3_endpoint"]
force_path_style = "true" if "minio" in s3_endpoint.lower() else "false"
# Use path-style for all non-AWS endpoints (MinIO, CloudFlare R2, custom S3)
# Only AWS S3 (*.amazonaws.com) should use virtual-hosted style
force_path_style = "false" if "amazonaws.com" in s3_endpoint.lower() else "true"
# Add entrypoint and volumes for database initialization
entrypoint_config = ""
@@ -160,6 +162,16 @@ def generate_lakefs_service(config: dict) -> str:
- ./scripts/lakefs-entrypoint.sh:/scripts/lakefs-entrypoint.sh:ro
- ./scripts/init-databases.sh:/scripts/init-databases.sh:ro"""
# Add external network if needed (for external postgres or s3)
lakefs_networks_str = ""
if config.get("external_network") and (
not config["postgres_builtin"] or not config["s3_builtin"]
):
lakefs_networks_str = f""" networks:
- default
- {config['external_network']}
"""
return f""" lakefs:
image: treeverse/lakefs:latest
container_name: lakefs
@@ -180,7 +192,7 @@ def generate_lakefs_service(config: dict) -> str:
user: "${{UID}}:${{GID}}"
{depends_on_str} volumes:
{volumes_config}
"""
{lakefs_networks_str}"""
def generate_hub_api_service(config: dict) -> str:
@@ -197,6 +209,16 @@ def generate_hub_api_service(config: dict) -> str:
for dep in depends_on:
depends_on_str += f" - {dep}\n"
# Add external network if needed (for external postgres or s3)
networks_str = ""
if config.get("external_network") and (
not config["postgres_builtin"] or not config["s3_builtin"]
):
networks_str = f""" networks:
- default
- {config['external_network']}
"""
# Database configuration
if config["postgres_builtin"]:
db_url = f"postgresql://{config['postgres_user']}:{config['postgres_password']}@postgres:5432/{config['postgres_db']}"
@@ -261,7 +283,7 @@ def generate_hub_api_service(config: dict) -> str:
- KOHAKU_HUB_DEFAULT_ORG_PUBLIC_QUOTA_BYTES=100_000_000
volumes:
- ./hub-meta/hub-api:/hub-api-creds
"""
{networks_str}"""
def generate_hub_ui_service() -> str:
@@ -299,8 +321,16 @@ def generate_docker_compose(config: dict) -> str:
content = "# docker-compose.yml\n# Generated by KohakuHub docker-compose generator\n\nservices:\n"
content += "\n".join(services)
# Network configuration
content += "\nnetworks:\n default:\n name: hub-net\n"
# Add external network if specified
if config.get("external_network"):
content += f""" {config['external_network']}:
external: true
"""
return content
@@ -368,6 +398,13 @@ def load_config_file(config_path: Path) -> dict:
config["session_secret"] = generate_secret()
config["admin_secret"] = generate_secret()
# Network section
if parser.has_section("network"):
net = parser["network"]
config["external_network"] = net.get("external_network", fallback="")
else:
config["external_network"] = ""
return config
@@ -418,6 +455,12 @@ secret_key = minioadmin
# Session and admin secrets (auto-generated if not specified)
# session_secret = your-session-secret-here
# admin_secret = your-admin-secret-here
[network]
# External bridge network (optional)
# Use this if PostgreSQL or S3 are in different Docker Compose setups
# Create the network first: docker network create shared-network
# external_network = shared-network
"""
output_path.write_text(template, encoding="utf-8")
@@ -577,6 +620,26 @@ def interactive_config() -> dict:
# LakeFS encryption key
config["lakefs_encrypt_key"] = generate_secret()
# Network configuration
print()
print("--- Network Configuration ---")
use_external_network = False
if not config["postgres_builtin"] or not config["s3_builtin"]:
use_external_network = ask_yes_no(
"Use external Docker network for cross-compose communication?",
default=False,
)
if use_external_network:
config["external_network"] = ask_string(
"External network name", default="shared-network"
)
print()
print(f"Note: Make sure the network exists:")
print(f" docker network create {config['external_network']}")
else:
config["external_network"] = ""
return config
@@ -622,21 +685,34 @@ def generate_and_write_files(config: dict):
print(f"S3 Storage: {'Built-in MinIO' if config['s3_builtin'] else 'Custom S3'}")
if not config["s3_builtin"]:
print(f" Endpoint: {config['s3_endpoint']}")
if config.get("external_network"):
print(f"External Network: {config['external_network']}")
print(f"Session Secret: {config['session_secret'][:20]}...")
print(f"Admin Secret: {config['admin_secret'][:20]}...")
print("-" * 60)
print()
print("Next steps:")
print("1. Review the generated docker-compose.yml")
print("2. Build frontend: npm run build --prefix ./src/kohaku-hub-ui")
print("3. Start services: docker-compose up -d")
step_num = 1
if config.get("external_network"):
print(f"{step_num}. Create external network if not exists:")
print(f" docker network create {config['external_network']}")
step_num += 1
print()
print(f"{step_num}. Review the generated docker-compose.yml")
step_num += 1
print(f"{step_num}. Build frontend: npm run build --prefix ./src/kohaku-hub-ui")
step_num += 1
print(f"{step_num}. Start services: docker-compose up -d")
print()
if config["lakefs_use_postgres"]:
print(" Note: Databases will be created automatically on first startup:")
print(f" - {config['postgres_db']} (hub-api)")
print(f" - {config['lakefs_db']} (LakeFS)")
print()
print("4. Access at: http://localhost:28080")
step_num += 1
print(f"{step_num}. Access at: http://localhost:28080")
print()

View File

@@ -39,6 +39,7 @@
<script setup>
import hljs from "highlight.js/lib/core";
import { copyToClipboard } from "@/utils/clipboard";
import { ElMessage } from "element-plus";
import { ref, computed, watch, onMounted } from "vue";
@@ -143,9 +144,13 @@ function escapeHtml(unsafe) {
.replace(/'/g, "&#039;");
}
function copyCode() {
navigator.clipboard.writeText(props.code);
ElMessage.success("Code copied to clipboard");
async function copyCode() {
const success = await copyToClipboard(props.code);
if (success) {
ElMessage.success("Code copied to clipboard");
} else {
ElMessage.error("Failed to copy");
}
}
// Watch for code or language changes

View File

@@ -601,6 +601,7 @@ huggingface-cli download {{ repoInfo?.id }}</pre
<script setup>
import { repoAPI } from "@/utils/api";
import { useAuthStore } from "@/stores/auth";
import { copyToClipboard } from "@/utils/clipboard";
import MarkdownViewer from "@/components/common/MarkdownViewer.vue";
import { ElMessage } from "element-plus";
import dayjs from "dayjs";
@@ -932,20 +933,32 @@ async function createReadme() {
}
}
function copyCloneUrl() {
navigator.clipboard.writeText(cloneUrl.value);
ElMessage.success("Clone URL copied to clipboard");
async function copyCloneUrl() {
const success = await copyToClipboard(cloneUrl.value);
if (success) {
ElMessage.success("Clone URL copied to clipboard");
} else {
ElMessage.error("Failed to copy");
}
}
function copyGitCloneUrl() {
navigator.clipboard.writeText(gitCloneUrl.value);
ElMessage.success("Git clone URL copied to clipboard");
async function copyGitCloneUrl() {
const success = await copyToClipboard(gitCloneUrl.value);
if (success) {
ElMessage.success("Git clone URL copied to clipboard");
} else {
ElMessage.error("Failed to copy");
}
}
function copyRepoId() {
async function copyRepoId() {
const repoId = `${props.namespace}/${props.name}`;
navigator.clipboard.writeText(repoId);
ElMessage.success("Repository ID copied to clipboard");
const success = await copyToClipboard(repoId);
if (success) {
ElMessage.success("Repository ID copied to clipboard");
} else {
ElMessage.error("Failed to copy");
}
}
// Watchers

View File

@@ -243,6 +243,7 @@
<script setup>
import MarkdownViewer from "@/components/common/MarkdownViewer.vue";
import CodeViewer from "@/components/common/CodeViewer.vue";
import { copyToClipboard } from "@/utils/clipboard";
import { ElMessage } from "element-plus";
import { useAuthStore } from "@/stores/auth";
@@ -515,15 +516,23 @@ function downloadFile() {
window.open(fileUrl.value, "_blank");
}
function copyFileUrl() {
async function copyFileUrl() {
const fullUrl = window.location.origin + fileUrl.value;
navigator.clipboard.writeText(fullUrl);
ElMessage.success("File URL copied to clipboard");
const success = await copyToClipboard(fullUrl);
if (success) {
ElMessage.success("File URL copied to clipboard");
} else {
ElMessage.error("Failed to copy");
}
}
function copyContent() {
navigator.clipboard.writeText(fileContent.value);
ElMessage.success("Content copied to clipboard");
async function copyContent() {
const success = await copyToClipboard(fileContent.value);
if (success) {
ElMessage.success("Content copied to clipboard");
} else {
ElMessage.error("Failed to copy");
}
}
function navigateToFolder(folderPath) {

View File

@@ -141,6 +141,7 @@
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
import { authAPI, settingsAPI } from "@/utils/api";
import { copyToClipboard } from "@/utils/clipboard";
import { ElMessage, ElMessageBox } from "element-plus";
import dayjs from "dayjs";
@@ -237,9 +238,13 @@ async function handleRevokeToken(id) {
}
}
function copyToken() {
navigator.clipboard.writeText(newToken.value);
ElMessage.success("Token copied to clipboard");
async function copyToken() {
const success = await copyToClipboard(newToken.value);
if (success) {
ElMessage.success("Token copied to clipboard");
} else {
ElMessage.error("Failed to copy token");
}
}
onMounted(() => {

View File

@@ -0,0 +1,48 @@
/**
* Copy text to clipboard with fallback for non-secure contexts
* @param {string} text - Text to copy
* @returns {Promise<boolean>} - True if successful
*/
export async function copyToClipboard(text) {
// Try modern Clipboard API first (requires HTTPS or localhost)
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn(
"Clipboard API failed, falling back to textarea method:",
err,
);
}
}
// Fallback for non-secure contexts (HTTP)
return copyToClipboardFallback(text);
}
/**
* Fallback method using textarea (works in non-secure contexts)
* @param {string} text - Text to copy
* @returns {boolean} - True if successful
*/
function copyToClipboardFallback(text) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "-9999px";
document.body.appendChild(textarea);
try {
textarea.select();
textarea.setSelectionRange(0, text.length);
const successful = document.execCommand("copy");
document.body.removeChild(textarea);
return successful;
} catch (err) {
console.error("Fallback copy failed:", err);
document.body.removeChild(textarea);
return false;
}
}