Skip to content

Commit

Permalink
Add proper ui for inspecting and acknowledging cluster errors #144
Browse files Browse the repository at this point in the history
  • Loading branch information
NHAS committed Nov 24, 2024
1 parent 466a705 commit c8b6426
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 150 deletions.
101 changes: 55 additions & 46 deletions adminui/clustering.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package adminui

import (
"encoding/json"
"errors"
"log"
"net/http"
"time"
Expand Down Expand Up @@ -82,60 +83,67 @@ func (au *AdminUI) members(w http.ResponseWriter, r *http.Request) {
}

func (au *AdminUI) newNode(w http.ResponseWriter, r *http.Request) {
var newNodeReq data.NewNodeRequest
err := json.NewDecoder(r.Body).Decode(&newNodeReq)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
var (
newNodeReq NewNodeRequestDTO
newNodeResp NewNodeResponseDTO
)

token, err := data.AddMember(newNodeReq.NodeName, newNodeReq.ConnectionURL, newNodeReq.ManagerURL)
if err != nil {
log.Println("failed to add member: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
defer func() {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(newNodeResp)
}()

newNodeResp.ErrorMessage = json.NewDecoder(r.Body).Decode(&newNodeReq)
if newNodeResp.ErrorMessage != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

w.Header().Set("Content-Type", "application/json")
newNodeResp := data.NewNodeResponse{
JoinToken: token,
newNodeResp.JoinToken, newNodeResp.ErrorMessage = data.AddMember(newNodeReq.NodeName, newNodeReq.ConnectionURL, newNodeReq.ManagerURL)
if newNodeResp.ErrorMessage != nil {
log.Println("failed to add member: ", newNodeResp.ErrorMessage)
w.WriteHeader(http.StatusInternalServerError)
return
}
b, _ := json.Marshal(newNodeResp)

log.Println("added new node: ", newNodeReq.NodeName, newNodeReq.ConnectionURL)

w.Write(b)
}

func (au *AdminUI) getClusterEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")

events := data.EventsQueue.ReadAll()

var (
es EventsResponseDTO
es = EventsResponseDTO{
EventLog: data.EventsQueue.ReadAll(),
}
err error
)
es.EventLog = events
defer func() {
if err != nil {
au.respond(err, w)
}
}()

es.Errors, err = data.GetAllErrors()
if err != nil {
var e GenericResponseDTO

e.Message = err.Error()
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(e)

return
}

w.Header().Set("content-type", "application/json")
json.NewEncoder(w).Encode(es)
}

func (au *AdminUI) nodeControl(w http.ResponseWriter, r *http.Request) {
var ncR data.NodeControlRequest
err := json.NewDecoder(r.Body).Decode(&ncR)
var (
ncR NodeControlRequestDTO
err error
)

defer func() { au.respond(err, w) }()

err = json.NewDecoder(r.Body).Decode(&ncR)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
w.WriteHeader(http.StatusBadRequest)
return
}

Expand All @@ -146,7 +154,7 @@ func (au *AdminUI) nodeControl(w http.ResponseWriter, r *http.Request) {
err = data.PromoteMember(ncR.Node)
if err != nil {
log.Println("failed to promote member: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
}
case "drain", "restore":
Expand All @@ -155,15 +163,15 @@ func (au *AdminUI) nodeControl(w http.ResponseWriter, r *http.Request) {
err = data.SetDrained(ncR.Node, ncR.Action == "drain")
if err != nil {
log.Println("failed to set/reset node drain: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
}
case "stepdown":
log.Println("node instructed to step down from leadership")
err = data.StepDown()
if err != nil {
log.Println("failed to step down from leadership makenode: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
}
case "remove":
Expand All @@ -172,42 +180,43 @@ func (au *AdminUI) nodeControl(w http.ResponseWriter, r *http.Request) {

if data.GetServerID().String() == ncR.Node {
log.Println("user tried to remove current operating node from cluster")
http.Error(w, "cannot remove current node", http.StatusBadRequest)
err = errors.New("cannot remove current node")
w.WriteHeader(http.StatusBadRequest)
return
}

err = data.RemoveMember(ncR.Node)
if err != nil {
log.Println("failed to remove member from cluster: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
}

default:
http.Error(w, "Bad request", http.StatusBadRequest)
err = errors.New("unknown action")
w.WriteHeader(http.StatusBadRequest)
return
}

w.Write([]byte("OK"))

}

func (au *AdminUI) clusterEventsAcknowledge(w http.ResponseWriter, r *http.Request) {

var acknowledgeError struct {
ErrorID string
}
err := json.NewDecoder(r.Body).Decode(&acknowledgeError)
var (
acknowledgeError AcknowledgeErrorResponseDTO
err error
)

defer func() { au.respond(err, w) }()

err = json.NewDecoder(r.Body).Decode(&acknowledgeError)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
w.WriteHeader(http.StatusBadRequest)
return
}

err = data.ResolveError(acknowledgeError.ErrorID)
if err != nil {
log.Println("failed to resolve error: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
}

w.Write([]byte("Success!"))
}
29 changes: 14 additions & 15 deletions adminui/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import { storeToRefs } from 'pinia'
import { onMounted, watch } from 'vue'
import { RouterView, useRouter } from 'vue-router'
import { POSITION, useToast } from 'vue-toastification'
import { type NotificationDTO } from './api'
import { useAuthStore } from '@/stores/auth'
import { useDevicesStore } from '@/stores/devices'
import { useUsersStore } from '@/stores/users'
import { type NotificationDTO } from './api'
import { POSITION, useToast } from 'vue-toastification'
const router = useRouter()
const authStore = useAuthStore()
Expand All @@ -19,21 +20,20 @@ const { hasCompletedAuth, hasTriedAuth, isLoggedIn } = storeToRefs(authStore)
const toast = useToast()
const httpsEnabled = window.location.protocol == "https:";
const httpsEnabled = window.location.protocol == 'https:'
function connectNotificationsWebsocket() {
const notificationsSocket = new WebSocket((httpsEnabled ? 'wss://' : 'ws://') + window.location.host + "/api/notifications")
notificationsSocket.onmessage = function(msg) {
const notificationsSocket = new WebSocket((httpsEnabled ? 'wss://' : 'ws://') + window.location.host + '/api/notifications')
notificationsSocket.onmessage = function (msg) {
const notf = JSON.parse(msg.data) as NotificationDTO
console.log(notf)
toast(notf.message.join("\n"), {
toast(notf.message.join('\n'), {
position: POSITION.TOP_RIGHT,
pauseOnFocusLoss: true,
onClick: function(){
if(notf.url.length != 0) {
if(notf.openNewTab) {
window.open(notf.url, "_blank")
onClick: function () {
if (notf.url.length != 0) {
if (notf.open_new_tab) {
window.open(notf.url, '_blank')
return
}
router.push(notf.url)
Expand All @@ -43,12 +43,11 @@ function connectNotificationsWebsocket() {
}
notificationsSocket.onerror = function (err) {
console.error('Notifications websocket encountered error: ', err, 'Closing socket');
notificationsSocket.close();
};
console.error('Notifications websocket encountered error: ', err, 'Closing socket')
notificationsSocket.close()
}
}
onMounted(async () => {
await router.isReady()
Expand Down
6 changes: 5 additions & 1 deletion adminui/frontend/src/api/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { ClusterEvents, ClusterMember } from './types'
import type { ClusterEvents, ClusterMember, AcknowledgeErrorResponseDTO, GenericResponseDTO } from './types'

import { client } from '.'

export function getClusterEvents(): Promise<ClusterEvents> {
return client.get('/api/cluster/events').then(res => res.data)
}

export function acknowledgeClusterError(error: AcknowledgeErrorResponseDTO): Promise<GenericResponseDTO> {
return client.put('/api/cluster/events', error).then(res => res.data)
}

export function getClusterMembers(): Promise<ClusterMember[]> {
return client.get('/api/cluster/members').then(res => res.data)
}
2 changes: 0 additions & 2 deletions adminui/frontend/src/api/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export function getUserAcls(user: AclsTestRequestDTO): Promise<AclsTestResponseD
return client.post('/api/diag/acls', user).then(res => res.data)
}


export function testNotifications(dummyNotification: TestNotificationsRequestDTO): Promise<GenericResponseDTO> {
return client.post('/api/diag/notifications', dummyNotification).then(res => res.data)
}

25 changes: 14 additions & 11 deletions adminui/frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export interface LogLinesDTO {
log_lines: string[]
}

export interface EventError {
export interface EventErrorDTO {
node_id: string
error_id: string

Expand All @@ -102,7 +102,7 @@ export interface EventError {

export interface ClusterEvents {
events: string[]
errors: EventError[]
errors: EventErrorDTO[]
}

export interface ClusterMember {
Expand Down Expand Up @@ -228,17 +228,20 @@ export interface AclsTestResponseDTO {
acls: Acl
}


export interface NotificationDTO {
id: string;
heading: string;
message: string[];
url: string;
time: string;
color: string;
open_new_tab: boolean;
id: string
heading: string
message: string[]
url: string
time: string
color: string
open_new_tab: boolean
}

export interface TestNotificationsRequestDTO {
message: string
}
}

export interface AcknowledgeErrorResponseDTO {
error_id: string
}
5 changes: 2 additions & 3 deletions adminui/frontend/src/components/RegistrationToken.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ async function createToken() {
if (!resp.success) {
toast.error(resp.message ?? 'Failed')
return
}
}
toast.success('token ' + resp.message + ' for ' + newToken.value.username + ' created!')
} catch (e) {
catcher(e, 'failed to create token: ')
} finally {
Expand Down
1 change: 0 additions & 1 deletion adminui/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const debugPageLinks = [
{ name: 'Test Rule', icon: Icons.Test, to: '/diagnostics/check' },
{ name: 'User ACLs', icon: Icons.List, to: '/diagnostics/acls' },
{ name: 'Notifications', icon: Icons.Send, to: '/diagnostics/notifications' }
]
async function logout() {
Expand Down
Loading

0 comments on commit c8b6426

Please sign in to comment.