feat(desktop): add type declaration for @brand-logo alias in brand-logo.d.ts
Deployment Config CI / Deployment Config (push) Successful in 12s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Desktop Client Build / Build Desktop Client (push) Successful in 22m8s

This commit is contained in:
2026-05-31 13:38:37 +08:00
parent 10d630db63
commit bdad050a8b
15 changed files with 339 additions and 62 deletions
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -94,6 +94,9 @@ export default defineConfig({
resolve: {
alias: {
'@renderer': resolve(rootDir, 'src/renderer'),
// Single source of truth for the brand mark; lives in public/ so the
// main process and electron-builder can read the same file.
'@brand-logo': resolve(rootDir, 'public/logo.svg'),
...workspaceAliases,
},
},
+2 -1
View File
@@ -5,7 +5,7 @@
"description": "省心推客户端 — 连接和管理您的媒体账号,轻松发布和监控内容表现。",
"author": {
"name": "Liang Xu",
"email": "liangxu@qq.com",
"email": "liangxu0920@qq.com",
"url": "https://shengxintui.com"
},
"type": "module",
@@ -13,6 +13,7 @@
"scripts": {
"dev": "electron-vite dev --inspect=9229",
"build": "electron-vite build",
"gen:icons": "node scripts/generate-brand-icons.cjs",
"obfuscate": "node scripts/obfuscate.cjs",
"build:obf": "pnpm run build && pnpm run obfuscate",
"package:mac": "pnpm run build:obf && electron-builder --mac --arm64 --universal",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,210 @@
#!/usr/bin/env node
/*
* Generates every brand icon asset the desktop client needs from the single
* source of truth: public/logo.svg.
*
* Outputs:
* build/icon.icns macOS app icon (electron-builder)
* build/icon.png Linux app icon + Windows/icns fallback (1024x1024)
* build/icon.ico Windows app icon
* src/main/brand-icons.ts base64 icons consumed by the main process
* (tray + dock + window + Windows overlay)
*
* Re-run after changing public/logo.svg:
* node scripts/generate-brand-icons.cjs
*
* Requires `rsvg-convert`, `sips` and `iconutil` (preinstalled on macOS via
* librsvg + the system toolchain).
*/
const { execFileSync } = require('node:child_process')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const APP_DIR = path.resolve(__dirname, '..')
const LOGO_PATH = path.join(APP_DIR, 'public', 'logo.svg')
const BUILD_DIR = path.join(APP_DIR, 'build')
const OUT_TS = path.join(APP_DIR, 'src', 'main', 'brand-icons.ts')
// Brand palette — keep in sync with the renderer (#1677ff is Ant Design blue).
const BRAND_BLUE = '#1677ff'
const DANGER_RED = '#ff4d4f'
const LOGO_FILL = '#dfdfdf' // the fill baked into logo.svg
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brand-icons-'))
const cleanup = () => fs.rmSync(tmpDir, { recursive: true, force: true })
function rsvg(args) {
return execFileSync('rsvg-convert', args)
}
/** Extract the inner markup (the <path> elements) and viewBox from logo.svg. */
function readLogo() {
const raw = fs.readFileSync(LOGO_PATH, 'utf8')
const viewBox = (raw.match(/viewBox="([^"]+)"/) || [])[1]
const inner = (raw.match(/<svg[^>]*>([\s\S]*)<\/svg>/) || [])[1]
if (!viewBox || !inner) {
throw new Error('Could not parse public/logo.svg (missing viewBox or body)')
}
const [, , vbW, vbH] = viewBox.split(/\s+/).map(Number)
return { inner: inner.trim(), width: vbW, height: vbH }
}
const logo = readLogo()
/** A square app icon: logo centered on a rounded-rect (squircle-ish) bg. */
function squareIconSvg(bg) {
const CANVAS = 1024
// macOS icon grid: 824x824 art area inside a 1024 canvas (100px margin).
const PLATE = 824
const MARGIN = (CANVAS - PLATE) / 2
const targetW = PLATE * 0.62
const scale = targetW / logo.width
const drawnW = logo.width * scale
const drawnH = logo.height * scale
const tx = MARGIN + (PLATE - drawnW) / 2
const ty = MARGIN + (PLATE - drawnH) / 2
return `<svg xmlns="http://www.w3.org/2000/svg" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
<rect x="${MARGIN}" y="${MARGIN}" width="${PLATE}" height="${PLATE}" rx="185" ry="185" fill="${bg}"/>
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${logo.inner}</g>
</svg>`
}
/**
* Tray glyph: the logo centered (with padding) inside a 32px square canvas.
* tray.ts loads it with scaleFactor 2, so it shows at ~16pt on retina menu
* bars. Padding keeps the wide/landscape mark from touching the canvas edges.
*/
function trayLogoSvg(fill) {
const CANVAS = 32 // tray asset is 32x32 px
// Glyph height inside the 32px canvas — the single knob for tray size.
// Bump up for a larger glyph, down for a smaller one.
const targetH = 20
const scale = targetH / logo.height
const drawnW = logo.width * scale
const drawnH = logo.height * scale
const tx = (CANVAS - drawnW) / 2
const ty = (CANVAS - drawnH) / 2
const inner = fill ? logo.inner.split(LOGO_FILL).join(fill) : logo.inner
return `<svg xmlns="http://www.w3.org/2000/svg" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${inner}</g>
</svg>`
}
function svgToPng(svg, name, extraArgs = []) {
const svgPath = path.join(tmpDir, `${name}.svg`)
const pngPath = path.join(tmpDir, `${name}.png`)
fs.writeFileSync(svgPath, svg)
rsvg([...extraArgs, '-o', pngPath, svgPath])
return pngPath
}
function b64(pngPath) {
return fs.readFileSync(pngPath).toString('base64')
}
/** Wrap a PNG buffer in a minimal (PNG-payload) .ico container. */
function pngToIco(pngBuffer) {
const header = Buffer.alloc(6)
header.writeUInt16LE(0, 0) // reserved
header.writeUInt16LE(1, 2) // type: icon
header.writeUInt16LE(1, 4) // image count
const entry = Buffer.alloc(16)
entry.writeUInt8(0, 0) // width (0 => 256)
entry.writeUInt8(0, 1) // height (0 => 256)
entry.writeUInt8(0, 2) // palette
entry.writeUInt8(0, 3) // reserved
entry.writeUInt16LE(1, 4) // color planes
entry.writeUInt16LE(32, 6) // bits per pixel
entry.writeUInt32LE(pngBuffer.length, 8) // size of image data
entry.writeUInt32LE(6 + 16, 12) // offset to image data
return Buffer.concat([header, entry, pngBuffer])
}
function buildAppIcons() {
// 1024 master for the normal app icon.
const masterPng = svgToPng(squareIconSvg(BRAND_BLUE), 'app-normal', ['-w', '1024', '-h', '1024'])
fs.mkdirSync(BUILD_DIR, { recursive: true })
fs.copyFileSync(masterPng, path.join(BUILD_DIR, 'icon.png'))
// .icns via an .iconset downscaled with sips.
const iconset = path.join(tmpDir, 'icon.iconset')
fs.mkdirSync(iconset, { recursive: true })
const variants = [
[16, 'icon_16x16.png'],
[32, 'icon_16x16@2x.png'],
[32, 'icon_32x32.png'],
[64, 'icon_32x32@2x.png'],
[128, 'icon_128x128.png'],
[256, 'icon_128x128@2x.png'],
[256, 'icon_256x256.png'],
[512, 'icon_256x256@2x.png'],
[512, 'icon_512x512.png'],
[1024, 'icon_512x512@2x.png'],
]
for (const [size, file] of variants) {
const dest = path.join(iconset, file)
execFileSync('sips', ['-z', String(size), String(size), masterPng, '--out', dest], {
stdio: 'ignore',
})
}
execFileSync('iconutil', ['-c', 'icns', iconset, '-o', path.join(BUILD_DIR, 'icon.icns')])
// .ico from a 256px render.
const ico256 = svgToPng(squareIconSvg(BRAND_BLUE), 'app-ico', ['-w', '256', '-h', '256'])
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngToIco(fs.readFileSync(ico256)))
}
function buildMainProcessIcons() {
// Tray icons are 32x32 px and loaded @2x in tray.ts (=> ~16pt on retina).
// Glyph size is controlled by targetH in trayLogoSvg(), not by an rsvg -h.
const trayNormal = b64(svgToPng(trayLogoSvg(), 'tray-normal'))
const trayDanger = b64(svgToPng(trayLogoSvg(DANGER_RED), 'tray-danger'))
// Dock / window icons: logo on the brand plate (normal) and danger plate.
const appNormal = b64(svgToPng(squareIconSvg(BRAND_BLUE), 'dock-normal', ['-w', '512', '-h', '512']))
const appDanger = b64(svgToPng(squareIconSvg(DANGER_RED), 'dock-danger', ['-w', '512', '-h', '512']))
// Windows taskbar overlay: a small danger badge (status cue, not the logo).
const overlaySvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="15" fill="${DANGER_RED}"/>
<rect x="14" y="7" width="4" height="12" rx="2" fill="#ffffff"/>
<circle cx="16" cy="24" r="2.4" fill="#ffffff"/>
</svg>`
const overlay = b64(svgToPng(overlaySvg, 'win-overlay'))
const banner = `// AUTO-GENERATED from public/logo.svg by scripts/generate-brand-icons.cjs.
// Do not edit by hand — run \`node scripts/generate-brand-icons.cjs\` instead.
/* eslint-disable */
/* prettier-ignore-start */`
const body = [
banner,
'',
'// macOS menu-bar template silhouette (rendered @2x).',
`export const TRAY_ICON_BASE64 =\n '${trayNormal}'`,
'',
'// Tray icon shown when accounts have health issues (rendered @2x, not a template).',
`export const TRAY_DANGER_ICON_BASE64 =\n '${trayDanger}'`,
'',
'// Dock / window icon — logo on the brand-blue plate.',
`export const APP_ICON_NORMAL_BASE64 =\n '${appNormal}'`,
'',
'// Dock / window icon — logo on a danger plate when issues exist.',
`export const APP_ICON_DANGER_BASE64 =\n '${appDanger}'`,
'',
'// Small Windows taskbar overlay badge signalling account health issues.',
`export const WINDOWS_OVERLAY_ICON_BASE64 =\n '${overlay}'`,
'/* prettier-ignore-end */',
'',
].join('\n')
fs.writeFileSync(OUT_TS, body)
}
try {
buildAppIcons()
buildMainProcessIcons()
console.log('✓ build/icon.icns, build/icon.png, build/icon.ico')
console.log('✓ src/main/brand-icons.ts')
} finally {
cleanup()
}
@@ -2,6 +2,11 @@ import { nativeImage } from 'electron/common'
import type { BrowserWindow as ElectronBrowserWindow } from 'electron/main'
import { app } from 'electron/main'
import {
APP_ICON_DANGER_BASE64,
APP_ICON_NORMAL_BASE64,
WINDOWS_OVERLAY_ICON_BASE64,
} from './brand-icons'
import { createRuntimeSnapshot } from './runtime-snapshot'
import { updateTrayIssueIndicator } from './tray'
@@ -11,12 +16,6 @@ type AppIndicatorImage = ReturnType<typeof nativeImage.createFromBuffer>
const HEALTH_ISSUE_AUTH_STATES = new Set(['expired', 'revoked', 'challenge_required'])
const ISSUE_INDICATOR_REFRESH_MS = 15_000
const APP_ICON_NORMAL_BASE64 =
'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'
const APP_ICON_DANGER_BASE64 =
'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'
const WINDOWS_OVERLAY_ICON_BASE64 =
'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'
let refreshTimer: ReturnType<typeof setInterval> | null = null
const appIconCache = new Map<AppIconTone, AppIndicatorImage>()
File diff suppressed because one or more lines are too long
+10 -11
View File
@@ -2,28 +2,27 @@ import { nativeImage } from 'electron/common'
import type { Tray as ElectronTray } from 'electron/main'
import { Menu, Tray, app } from 'electron/main'
import { TRAY_DANGER_ICON_BASE64, TRAY_ICON_BASE64 } from './brand-icons'
type TrayIconImage = ReturnType<typeof nativeImage.createFromBuffer>
// The tray PNGs are generated at 2x (see scripts/generate-brand-icons.cjs), so
// they stay crisp on retina menu bars.
const TRAY_ICON_SCALE_FACTOR = 2
let tray: ElectronTray | null = null
let normalTrayIcon: TrayIconImage | null = null
let dangerTrayIcon: TrayIconImage | null = null
const TRAY_ICON_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAnUlEQVR4nN2UwQmAMAxF360Dee8I' +
'LuNIxT2cpIv0oJcIEqJGMSIG/qX8fn5/msDfqgcKUIEmqHLW3xHsgAmYTzAJ1+2yOURXNI/77qLo' +
'VvzQufX8EchAEmQ5s2LZjUCThwMTg8E3IymG07PSzotFqoqUHcJZ3akWSTctOYST0cT3hMOiCGte' +
'2HcjakCIHGmiltDW+eNrU7t/dNF/txaahq+VoZ+VygAAAABJRU5ErkJggg=='
const TRAY_DANGER_ICON_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAACSUlEQVQ4jd2Uz04TURTGf2dmkG5spwU3mOgKXoCFGlwM0qZpDYEFvoLEPyuN6F4TTYxLWPAKaqILxKZtmMSQ6CvAygUuhNIpLpowt3NcaMt0GkASN/qtbs6533e+e++5B/41yEnJRqmUlsPDMiLTAmMACt9Q3dBz5z6MrK8fnElYJyeHmrncI1FdAjLHcAMReZEJw1fi++ZU4abnudj2W0RunHSaGOoYs5D1/SAetJJOcZw3ZxAFmMFxXqvnOccKN7PZh8BMgriLyFNVnVXVWeAZsJvYk2/Z9oN4oHcVjVIpbYXhV8CN5T9Ztj2fqVT246RWsZiLOp33wPVYODDt9qULm5s/+hyLMTcTot9leHiuK9oqFMZbhcI4QKZS2bdV54G92H53KJUqD16Fqpc43qq7ttYECPL5lUh1K1LdCvL5ZYB0vd4Q1dU+hkhP48jx7z49qqNfuk4V7vTicLfrPLLtzwnOxUHHfxk9YRXZiSdE5ApAplrdFliJ2VrOVKvbAFanczXB6WkcOY4iP1F0sVUs5gDcWu2eJTJhiUxk6/X7AAeeN6oiiwnORq9Id3FMu23aqnPper0RZx943mjHcd4BU7Fw07Tbl7vt1vel9/P5JwLPEy72RHW1+1CW6jVVvQ2M9F0DLLm12ssBxwDqeU7gOB8Z/H2noeYaU4oPo76uEN83GLMA1P5YUqSKMbeSE26g3bK+H7jGlET1MRAk8zE0BZbcMCwnJxucMuh3p6bOD6VS5ciypiWKxn4ZlB1gI2y317sP9X/gJ6o+6LM9+rGLAAAAAElFTkSuQmCC'
function createTrayIcon(tone: 'normal' | 'danger'): TrayIconImage {
if (tone === 'danger') {
if (dangerTrayIcon) {
return dangerTrayIcon
}
const icon = nativeImage.createFromBuffer(Buffer.from(TRAY_DANGER_ICON_BASE64, 'base64'))
const icon = nativeImage.createFromBuffer(Buffer.from(TRAY_DANGER_ICON_BASE64, 'base64'), {
scaleFactor: TRAY_ICON_SCALE_FACTOR,
})
icon.setTemplateImage(false)
dangerTrayIcon = icon.isEmpty() ? createTrayIcon('normal') : icon
return dangerTrayIcon
@@ -34,7 +33,7 @@ function createTrayIcon(tone: 'normal' | 'danger'): TrayIconImage {
}
const buffer = Buffer.from(TRAY_ICON_BASE64, 'base64')
const icon = nativeImage.createFromBuffer(buffer)
const icon = nativeImage.createFromBuffer(buffer, { scaleFactor: TRAY_ICON_SCALE_FACTOR })
if (icon.isEmpty()) {
return nativeImage.createEmpty()
}
+7
View File
@@ -0,0 +1,7 @@
// Type for the `@brand-logo` alias (electron.vite.config.ts → public/logo.svg).
// Kept in its own ambient (non-module) declaration file so vue-tsc resolves the
// alias the same way Vite does at build time.
declare module '@brand-logo' {
const src: string
export default src
}
@@ -11,6 +11,7 @@ import {
import { computed, onMounted, ref, watch } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import brandLogo from '@brand-logo'
import { useClientRelease } from '../composables/useClientRelease'
import { useDesktopRuntime } from '../composables/useDesktopRuntime'
import { useDesktopSession } from '../composables/useDesktopSession'
@@ -232,8 +233,7 @@ watch(
<aside class="sidebar">
<div class="brand-block">
<div class="brand-mark">
<span class="brand-ring"></span>
<span class="brand-core"></span>
<img :src="brandLogo" alt="省心推" class="brand-mark-img" />
</div>
<div>
<p class="eyebrow">Workspace Cockpit</p>
@@ -444,30 +444,19 @@ watch(
}
.brand-mark {
position: relative;
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: 11px;
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
box-shadow: 0 6px 14px rgba(22, 119, 255, 0.28);
}
.brand-ring,
.brand-core {
position: absolute;
border-radius: 50%;
}
.brand-ring {
inset: 0;
border: 1px solid #91caff;
background: #e6f4ff;
}
.brand-core {
width: 16px;
height: 16px;
background: #1677ff;
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.4);
.brand-mark-img {
width: 62%;
height: 62%;
object-fit: contain;
}
.eyebrow {
+8
View File
@@ -2,6 +2,7 @@ import './console-guard'
import { createApp } from 'vue'
import brandLogo from '@brand-logo'
import type { DesktopObservedRequest } from '../shared/network-debug'
import App from './App.vue'
import { router } from './routes'
@@ -9,6 +10,13 @@ import { router } from './routes'
import 'ant-design-vue/dist/reset.css'
import '../../../../packages/ui-shared/src/tokens/index.css'
// Point the favicon at the real brand mark; the inline SVG in index.html is a
// pre-mount fallback.
const favicon = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
if (favicon) {
favicon.href = brandLogo
}
const applyTheme = (prefersDark: boolean) => {
document.documentElement.dataset.theme = prefersDark ? 'dark' : 'light'
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import brandLogo from '@brand-logo'
import { DEFAULT_API_BASE_URL, useDesktopSession } from '../composables/useDesktopSession'
const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession()
@@ -179,7 +180,7 @@ onBeforeUnmount(() => {
<div class="auth-wrapper">
<header class="brand-header">
<div class="brand-logo">
<span class="brand-core"></span>
<img :src="brandLogo" alt="省心推" class="brand-logo-img" />
</div>
<h1 class="brand-name">省心推</h1>
</header>
@@ -448,21 +449,10 @@ onBeforeUnmount(() => {
inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.brand-core {
width: 26px;
height: 26px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.25);
position: relative;
}
.brand-core::after {
content: '';
position: absolute;
inset: 7px;
border-radius: 3px;
background: linear-gradient(135deg, #1677ff, #69b1ff);
.brand-logo-img {
width: 60%;
height: 60%;
object-fit: contain;
}
.brand-name {
+53 -8
View File
@@ -70,9 +70,19 @@ config_file_path() {
done
}
read_object_storage_provider() {
config_file_path_in_dir() {
local dir="$1" base="$2"
for file in "${dir}/${base}.yaml" "${dir}/${base}.yml"; do
if [[ -f "${file}" ]]; then
printf '%s' "${file}"
return
fi
done
}
read_object_storage_provider_from_files() {
local provider="minio" file
for file in "$(config_file_path config)" "$(config_file_path config.local)"; do
for file in "$@"; do
if [[ -z "${file}" || ! -f "${file}" ]]; then
continue
fi
@@ -110,8 +120,19 @@ read_object_storage_provider() {
printf '%s' "$(printf '%s' "${provider}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
}
use_minio_for_object_storage() {
case "$(read_object_storage_provider)" in
read_object_storage_provider() {
read_object_storage_provider_from_files "$(config_file_path config)" "$(config_file_path config.local)"
}
read_k3s_object_storage_provider() {
read_object_storage_provider_from_files \
"$(config_file_path_in_dir "${SCRIPT_DIR}/k3s/config" config)" \
"$(config_file_path config.local)"
}
use_minio_provider() {
local provider="$1"
case "${provider}" in
""|minio|mino)
return 0
;;
@@ -124,6 +145,10 @@ use_minio_for_object_storage() {
esac
}
use_minio_for_object_storage() {
use_minio_provider "$(read_object_storage_provider)"
}
sync_k3s_config() {
mkdir -p "${SCRIPT_DIR}/k3s/config"
for name in config ops-config prompts; do
@@ -131,6 +156,9 @@ sync_k3s_config() {
if [[ "${name}" == "prompts" ]]; then
output="prompts.yml"
fi
if [[ -f "${SCRIPT_DIR}/k3s/config/${output}" ]]; then
continue
fi
local source=""
for candidate in "${SCRIPT_DIR}/${name}.yaml" "${SCRIPT_DIR}/${name}.yml"; do
if [[ -f "${candidate}" ]]; then
@@ -153,6 +181,18 @@ prepare_k3s_runtime_overlay() {
cp "$(config_file_path config.local)" "${runtime_dir}/config.local.yaml"
cp "$(config_file_path ops-config.local)" "${runtime_dir}/ops-config.local.yaml"
cat > "${runtime_dir}/secret.env" <<EOF
POSTGRES_PASSWORD=$(env_value POSTGRES_PASSWORD geo_dev)
RABBITMQ_DEFAULT_PASS=$(env_value RABBITMQ_DEFAULT_PASS geo_dev)
MINIO_ROOT_USER=$(env_value MINIO_ROOT_USER minioadmin)
MINIO_ROOT_PASSWORD=$(env_value MINIO_ROOT_PASSWORD minioadmin)
JWT_SECRET=$(env_value JWT_SECRET geo-web-change-me-in-production)
OPS_JWT_SECRET=$(env_value OPS_JWT_SECRET geo-ops-change-me-in-production)
OPS_DEFAULT_ADMIN_PASSWORD=$(env_value OPS_DEFAULT_ADMIN_PASSWORD Aa123456)
SCHEDULER_INTERNAL_METRICS_TOKEN=$(env_value SCHEDULER_INTERNAL_METRICS_TOKEN geo-metrics-token)
BROWSER_FETCH_TOKEN=$(env_value BROWSER_FETCH_TOKEN geo-browser-fetch-change-me)
EOF
cat > "${runtime_dir}/kustomization.yaml" <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
@@ -163,6 +203,8 @@ resources:
secretGenerator:
- name: geo-rankly-app-secret
behavior: merge
envs:
- secret.env
files:
- config.local.yaml=config.local.yaml
- ops-config.local.yaml=ops-config.local.yaml
@@ -234,7 +276,11 @@ deploy_k3s() {
ensure_local_config "config.local.yaml" "config.local.yaml.example"
ensure_local_config "ops-config.local.yaml" "ops-config.local.yaml.example"
if ! use_minio_for_object_storage; then
sync_k3s_config
local object_storage_provider
object_storage_provider="$(read_k3s_object_storage_provider)"
if ! use_minio_provider "${object_storage_provider}"; then
kustomize_dir="${SCRIPT_DIR}/k3s-aliyun-oss"
minio_enabled="false"
fi
@@ -252,12 +298,11 @@ deploy_k3s() {
fi
echo "==> [2/3] 应用 k3s 清单..."
sync_k3s_config
kustomize_dir="$(prepare_k3s_runtime_overlay "${kustomize_dir}")"
if [[ "${minio_enabled}" == "true" ]]; then
echo " object_storage.provider=$(read_object_storage_provider),将部署 MinIO。"
echo " object_storage.provider=${object_storage_provider},将部署 MinIO。"
else
echo " object_storage.provider=$(read_object_storage_provider),将跳过 MinIO。"
echo " object_storage.provider=${object_storage_provider},将跳过 MinIO。"
${kubectl_cmd} -n geo-rankly delete statefulset minio --ignore-not-found
${kubectl_cmd} -n geo-rankly delete service minio --ignore-not-found
${kubectl_cmd} -n geo-rankly delete job minio-init --ignore-not-found