Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fccd39a73 |
@@ -46,7 +46,7 @@ spec:
|
|||||||
serviceAccountName: fission-console
|
serviceAccountName: fission-console
|
||||||
containers:
|
containers:
|
||||||
- name: console
|
- name: console
|
||||||
image: naeel/fission-console:v0.8.13
|
image: naeel/fission-console:v0.8.14
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ func main() {
|
|||||||
mux.HandleFunc("/console/api/functions/", auth(s.handleFunctionsAction))
|
mux.HandleFunc("/console/api/functions/", auth(s.handleFunctionsAction))
|
||||||
mux.HandleFunc("/console/api/httptriggers", auth(s.handleList(httpTrigGVR)))
|
mux.HandleFunc("/console/api/httptriggers", auth(s.handleList(httpTrigGVR)))
|
||||||
mux.HandleFunc("/console/api/timetriggers", auth(s.handleList(timeTrigGVR)))
|
mux.HandleFunc("/console/api/timetriggers", auth(s.handleList(timeTrigGVR)))
|
||||||
|
mux.HandleFunc("/console/api/ns/status", auth(s.handleNSStatus))
|
||||||
mux.HandleFunc("/console/api/ai/check", auth(s.handleAICheck))
|
mux.HandleFunc("/console/api/ai/check", auth(s.handleAICheck))
|
||||||
// --- ai/ask feature (удалить строку чтобы выкосить роут) ---
|
// --- ai/ask feature (удалить строку чтобы выкосить роут) ---
|
||||||
mux.HandleFunc("/console/api/ai/ask", auth(s.handleAIAsk))
|
mux.HandleFunc("/console/api/ai/ask", auth(s.handleAIAsk))
|
||||||
@@ -1721,6 +1722,99 @@ func (s *server) ensureUserNamespace(ctx context.Context, ns string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleNSStatus возвращает статус инициализации пользовательского namespace.
|
||||||
|
// Используется UI для отображения прогресса при первом входе.
|
||||||
|
func (s *server) handleNSStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ns := s.userNS(r)
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fissionNS := os.Getenv("FISSION_SYSTEM_NAMESPACE")
|
||||||
|
if fissionNS == "" {
|
||||||
|
fissionNS = "fission"
|
||||||
|
}
|
||||||
|
|
||||||
|
type stageInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Done bool `json:"done"`
|
||||||
|
}
|
||||||
|
stages := []stageInfo{
|
||||||
|
{Name: "Создание пространства имён"},
|
||||||
|
{Name: "Регистрация в Fission"},
|
||||||
|
{Name: "Прогрев окружений"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 1: NS существует и Active (ensureUserNS уже вызван auth middleware)
|
||||||
|
nsObj, err := s.dyn.Resource(namespaceGVR).Get(ctx, ns, metav1.GetOptions{})
|
||||||
|
if err == nil {
|
||||||
|
phase, _, _ := unstructured.NestedString(nsObj.Object, "status", "phase")
|
||||||
|
stages[0].Done = phase == "Active"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 2: NS в FISSION_RESOURCE_NAMESPACES executor deployment
|
||||||
|
if stages[0].Done {
|
||||||
|
execDep, err2 := s.dyn.Resource(deploymentGVR).Namespace(fissionNS).Get(ctx, "executor", metav1.GetOptions{})
|
||||||
|
if err2 == nil {
|
||||||
|
containers, _, _ := unstructured.NestedSlice(execDep.Object, "spec", "template", "spec", "containers")
|
||||||
|
for _, c := range containers {
|
||||||
|
cont, ok := c.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
envs, _, _ := unstructured.NestedSlice(cont, "env")
|
||||||
|
for _, e := range envs {
|
||||||
|
env, ok := e.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if env["name"] == "FISSION_RESOURCE_NAMESPACES" {
|
||||||
|
if v, ok := env["value"].(string); ok {
|
||||||
|
for _, p := range strings.Split(v, ",") {
|
||||||
|
if strings.TrimSpace(p) == ns {
|
||||||
|
stages[1].Done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 3: executor pod Running + Ready
|
||||||
|
if stages[1].Done {
|
||||||
|
podGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||||
|
podList, err3 := s.dyn.Resource(podGVR).Namespace(fissionNS).List(ctx, metav1.ListOptions{
|
||||||
|
LabelSelector: "svc=executor",
|
||||||
|
})
|
||||||
|
if err3 == nil {
|
||||||
|
for _, pod := range podList.Items {
|
||||||
|
phase, _, _ := unstructured.NestedString(pod.Object, "status", "phase")
|
||||||
|
if phase != "Running" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conditions, _, _ := unstructured.NestedSlice(pod.Object, "status", "conditions")
|
||||||
|
for _, c := range conditions {
|
||||||
|
cond, ok := c.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cond["type"] == "Ready" && cond["status"] == "True" {
|
||||||
|
stages[2].Done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ready := stages[0].Done && stages[1].Done && stages[2].Done
|
||||||
|
writeAnyJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"ready": ready,
|
||||||
|
"stages": stages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) validateDeckToken(token, env string) error {
|
func (s *server) validateDeckToken(token, env string) error {
|
||||||
cacheKey := env + ":" + token
|
cacheKey := env + ":" + token
|
||||||
if v, ok := s.tokenCache.Load(cacheKey); ok {
|
if v, ok := s.tokenCache.Load(cacheKey); ok {
|
||||||
|
|||||||
+76
-1
@@ -307,13 +307,26 @@
|
|||||||
<div class="row" style="flex-direction:column; gap:6px; margin-bottom:12px;">
|
<div class="row" style="flex-direction:column; gap:6px; margin-bottom:12px;">
|
||||||
<label style="font-size:12px; color:#999;">Токен</label>
|
<label style="font-size:12px; color:#999;">Токен</label>
|
||||||
<textarea id="l-token" rows="5" style="background:var(--bg-base); border:1px solid var(--border); color:var(--fg); padding:8px 10px; border-radius:6px; font-family:monospace; font-size:12px; resize:vertical; width:100%; box-sizing:border-box;" placeholder="Введите токен..."></textarea>
|
<textarea id="l-token" rows="5" style="background:var(--bg-base); border:1px solid var(--border); color:var(--fg); padding:8px 10px; border-radius:6px; font-family:monospace; font-size:12px; resize:vertical; width:100%; box-sizing:border-box;" placeholder="Введите токен..."></textarea>
|
||||||
|
<div style="font-size:11px; color:var(--text-secondary); margin-top:4px;">Токен: <strong style="color:#8bc7ff;">Личный кабинет → Профиль пользователя → Токены</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="l-error" style="display:none; color:#ff6b6b; font-size:13px; margin-bottom:10px;"></div>
|
<div id="l-error" style="display:none; color:#ff6b6b; font-size:13px; margin-bottom:10px;"></div>
|
||||||
<button id="l-btn" class="btn" style="width:100%;" onclick="doLogin()">Войти</button>
|
<button id="l-btn" class="btn" style="width:100%;" onclick="doLogin()">Войти</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Overlay: инициализация нового окружения -->
|
||||||
|
<div id="init-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.88); z-index:1000; align-items:center; justify-content:center; padding:16px;">
|
||||||
|
<div class="panel" style="width:min(520px,100%);">
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<div style="font-size:16px; font-weight:700; margin-bottom:6px;">⚙️ Инициализация окружения</div>
|
||||||
|
<div style="font-size:13px; color:var(--text-secondary);">Первый вход. Настраиваем ваше окружение — это займёт ~5 минут.</div>
|
||||||
|
</div>
|
||||||
|
<div id="init-log" style="background:#000d1a; border:1px solid var(--border); border-radius:6px; padding:12px; font-family:monospace; font-size:12px; height:160px; overflow-y:auto; color:#4fc3f7; white-space:pre-wrap; line-height:1.6;"></div>
|
||||||
|
<div id="init-stages" style="margin-top:14px; display:flex; flex-direction:column; gap:8px;"></div>
|
||||||
|
<div style="margin-top:14px; font-size:12px; color:var(--text-secondary);">Страница обновится автоматически когда всё будет готово.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="brand">
|
|
||||||
<div class="brand-mark">N</div>
|
<div class="brand-mark">N</div>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<div class="nubes">NUBES</div>
|
<div class="nubes">NUBES</div>
|
||||||
@@ -826,6 +839,15 @@
|
|||||||
localStorage.setItem('auth_token', token);
|
localStorage.setItem('auth_token', token);
|
||||||
localStorage.setItem('auth_env', env);
|
localStorage.setItem('auth_env', env);
|
||||||
hideLoginOverlay();
|
hideLoginOverlay();
|
||||||
|
// Проверяем статус NS — если не ready, показываем init overlay
|
||||||
|
try {
|
||||||
|
var sr = await fetch(API_BASE + '/ns/status', {headers: {'X-Auth-Token': token, 'X-Auth-Env': env}});
|
||||||
|
var sd = await sr.json();
|
||||||
|
if (!sd.ready) {
|
||||||
|
pollNSStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(_) {}
|
||||||
reloadAll();
|
reloadAll();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
errEl.textContent = e.message;
|
errEl.textContent = e.message;
|
||||||
@@ -870,6 +892,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _initPolling = false;
|
||||||
|
|
||||||
|
function initLog(msg) {
|
||||||
|
var el = document.getElementById('init-log');
|
||||||
|
var ts = new Date().toLocaleTimeString('ru-RU');
|
||||||
|
el.textContent += '[' + ts + '] ' + msg + '\n';
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInitStages(stages) {
|
||||||
|
var el = document.getElementById('init-stages');
|
||||||
|
el.innerHTML = stages.map(function(s) {
|
||||||
|
var icon = s.done ? '✅' : '⏳';
|
||||||
|
var color = s.done ? '#4caf50' : '#8bc7ff';
|
||||||
|
return '<div style="font-size:13px; color:' + color + ';">' + icon + ' ' + s.name + '</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollNSStatus() {
|
||||||
|
if (_initPolling) return;
|
||||||
|
_initPolling = true;
|
||||||
|
var overlay = document.getElementById('init-overlay');
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
initLog('Запуск инициализации...');
|
||||||
|
var attempt = 0;
|
||||||
|
var maxAttempts = 60; // 5 минут
|
||||||
|
var interval = setInterval(async function() {
|
||||||
|
attempt++;
|
||||||
|
try {
|
||||||
|
var r = await fetch(API_BASE + '/ns/status', {headers: authHeaders()});
|
||||||
|
var d = await r.json();
|
||||||
|
if (d.stages) renderInitStages(d.stages);
|
||||||
|
var done = d.stages ? d.stages.filter(function(s){return s.done;}).length : 0;
|
||||||
|
initLog('Шагов завершено: ' + done + '/3');
|
||||||
|
if (d.ready) {
|
||||||
|
initLog('✅ Готово! Загружаем консоль...');
|
||||||
|
clearInterval(interval);
|
||||||
|
_initPolling = false;
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
reloadAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
initLog('Ошибка опроса: ' + e.message);
|
||||||
|
}
|
||||||
|
if (attempt >= maxAttempts) {
|
||||||
|
clearInterval(interval);
|
||||||
|
_initPolling = false;
|
||||||
|
initLog('⚠️ Превышено время ожидания. Попробуйте обновить страницу.');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
function checkAuth() {
|
function checkAuth() {
|
||||||
if (!localStorage.getItem('auth_token')) {
|
if (!localStorage.getItem('auth_token')) {
|
||||||
showLoginOverlay();
|
showLoginOverlay();
|
||||||
|
|||||||
Reference in New Issue
Block a user