Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/manager/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ func (s *Server) RegisterServer(inp RegisterServerRequest) error {

return s.db.CreateServerEntry(sinfo)
}

type DeleteServerRequest struct {
Server managertypes.ServerInfo `json:"server"`
}
48 changes: 48 additions & 0 deletions api/manager/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func (s *Server) HandleRequests() {
// Manger-specific
rtr.HandleFunc("/manager-api/server/list", corsHandler(s.serverList))
rtr.HandleFunc("/manager-api/server/register", corsHandler(s.serverRegister))
rtr.HandleFunc("/manager-api/server/delete/", corsHandler(s.serverDelete))

// SPIRE server info calls
rtr.HandleFunc("/manager-api/healthcheck/{server:.*}", corsHandler(s.apiServerProxyFunc("/api/v1/spire/healthcheck", http.MethodGet)))
Expand Down Expand Up @@ -330,3 +331,50 @@ func (s *Server) serverRegister(w http.ResponseWriter, r *http.Request) {
return
}
}

func (s *Server) serverDelete(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: Server Delete")

buf := new(strings.Builder)

n, err := io.Copy(buf, r.Body)
if err != nil {
emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
retError(w, emsg, http.StatusBadRequest)
return
}
data := buf.String()

var input DeleteServerRequest
if n == 0 {
input = DeleteServerRequest{}
} else {
err := json.Unmarshal([]byte(data), &input)
if err != nil {
emsg := fmt.Sprintf("Error parsing data: %v", err.Error())
retError(w, emsg, http.StatusBadRequest)
return
}
}

if input.Server.Name == "" {
retError(w, "Server name is missing", http.StatusBadRequest)
return
}

err = s.db.DeleteServer(input.Server.Name)
if err != nil {
emsg := fmt.Sprintf("Error: %v", err.Error())
retError(w, emsg, http.StatusInternalServerError)
return
}

cors(w, r)
_, err = w.Write([]byte("SUCCESS"))

if err != nil {
emsg := fmt.Sprintf("Error: %v", err.Error())
retError(w, emsg, http.StatusInternalServerError)
return
}
}
18 changes: 18 additions & 0 deletions docs/tornjak-ui-api-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,24 @@ Example response:
SUCCESS
```

##### /manager-api/server/delete

```
Request
manager-api/server/delete
Example request payload:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"server": {
"name": "server1"
}
}
Example response:
SUCCESS
```

![tornjak-agent-list](rsrc/tornjak-agent-list.png)
Figure 1. Agent List/ Home Page UI-API Interactions

Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/tornjak-api-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ServerInfo,
EntriesList,
ClustersList,
ServersList,
DebugServerInfo,
FederationsList
} from './types';
Expand Down Expand Up @@ -344,6 +345,23 @@ class TornjakApi extends Component<TornjakApiProp, TornjakApiState> {
return response.data;
}

async serverDelete(inputData: { server: { name: string; }; }, serversListUpdateFunc: { (globalServersList: ServersList[]): void }, globalServersList: any[]) {
const response = await axios.post(GetApiServerUri("/manager-api/server/delete/"), inputData,
{
crossdomain: true,
})
.then(function (response) {
serversListUpdateFunc(globalServersList.filter(el =>
el.name !== inputData))
Comment on lines +354 to +355
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter logic is incorrect. It compares el.name (a string) with inputData (an object containing { server: { name: string } }). This should be el.name !== inputData.server.name to correctly filter out the deleted server.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@atpugtihsrah atpugtihsrah Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works either/both ways. Should we stick to the current codebase pattern of checking el.name !== inputData?

return response.data;
})
.catch(function (error) {
return error.message;
})
return response.data;
}


// populateClustersUpdate returns the list of clusters with their info in manager mode for the selected server
populateClustersUpdate = (serverName: string,
clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void; },
Expand Down
51 changes: 24 additions & 27 deletions frontend/src/tables/servers-list-table.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from "react";
import { connect } from 'react-redux';
// import IsManager from 'components/is_manager';
import IsManager from 'components/is_manager';
import {
serversListUpdateFunc
} from 'redux/actions';
import Table from './list-table';
import { ServersList } from "components/types";
// import { DenormalizedRow } from "carbon-components-react";
import { DenormalizedRow } from "carbon-components-react";
import { RootState } from "redux/reducers";
import TornjakApi from 'components/tornjak-api-helpers';



// ServersListTable takes in
// listTableData: servers data to be rendered on table
// returns servers data inside a carbon component table with specified functions
Expand Down Expand Up @@ -50,7 +52,7 @@ class ServersListTable extends React.Component<ServersListTableProp, ServersList
listTableData: [],
};
this.prepareTableData = this.prepareTableData.bind(this);
// this.deleteServer = this.deleteServer.bind(this);
this.deleteServer = this.deleteServer.bind(this);
}

componentDidMount() {
Expand Down Expand Up @@ -85,30 +87,25 @@ class ServersListTable extends React.Component<ServersListTableProp, ServersList
})
}

//Note: future implementation - server delete function
// keep code
// deleteServer(selectedRows: readonly DenormalizedRow[]) {
// if (!selectedRows || selectedRows.length === 0) return "";
// let server: { name: string }[] = [], successMessage
deleteServer(selectedRows: readonly DenormalizedRow[]) {
if (!selectedRows || selectedRows.length === 0) return "";
if (!IsManager) return "";
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsManager should be called as a function IsManager(), not checked as a value. Based on the codebase pattern in other files like agent-list.tsx, IsManager is used in conditional expressions like if (IsManager) {...}, suggesting it's a function or constant that should be invoked.

Suggested change
if (!IsManager) return "";
if (!IsManager()) return "";

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect. agent-list.tsx also uses IsManager as a value, not as a function. The type of IsManager is a boolean, its not a function.

let server: { name: string }[] = [], successMessage

// for (let i = 0; i < selectedRows.length; i++) {
// server[i] = { name: selectedRows[i].cells[1].value };
// if (IsManager) {
// successMessage = this.TornjakApi.serverDelete(this.props.globalServerSelected, { server: server[i] }, this.props.serversListUpdateFunc, this.props.globalServersList);
// } else {
// successMessage = this.TornjakApi.localServerDelete({ server: server[i] }, this.props.serversListUpdateFunc, this.props.globalServersList);
// }
// successMessage.then(function (result) {
// if (result === "SUCCESS") {
// window.alert(`CLUSTER "${server[i].name}" DELETED SUCCESSFULLY!`);
// window.location.reload();
// } else {
// window.alert(`Error deleting server "${server[i].name}": ` + result);
// }
// return;
// })
// }
// }
for (let i = 0; i < selectedRows.length; i++) {
server[i] = { name: selectedRows[i].cells[1].value };
successMessage = this.TornjakApi.serverDelete({ server: server[i] }, this.props.serversListUpdateFunc, this.props.globalServersList);
successMessage.then(function (result) {
if (result === "SUCCESS") {
window.alert(`SERVER "${server[i].name}" DELETED SUCCESSFULLY!`);
window.location.reload();
} else {
window.alert(`Error deleting server "${server[i].name}": ` + result);
Comment on lines +97 to +103
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable server[i] may have incorrect value in async callback. Since this is inside an async callback and i is captured by reference, by the time the callback executes, i will likely equal selectedRows.length. Consider storing server[i].name in a local constant before the async call or use forEach to properly capture the value.

Suggested change
successMessage = this.TornjakApi.serverDelete({ server: server[i] }, this.props.serversListUpdateFunc, this.props.globalServersList);
successMessage.then(function (result) {
if (result === "SUCCESS") {
window.alert(`SERVER "${server[i].name}" DELETED SUCCESSFULLY!`);
window.location.reload();
} else {
window.alert(`Error deleting server "${server[i].name}": ` + result);
const serverName = server[i].name;
successMessage = this.TornjakApi.serverDelete({ server: server[i] }, this.props.serversListUpdateFunc, this.props.globalServersList);
successMessage.then(function (result) {
if (result === "SUCCESS") {
window.alert(`SERVER "${serverName}" DELETED SUCCESSFULLY!`);
window.location.reload();
} else {
window.alert(`Error deleting server "${serverName}": ` + result);

Copilot uses AI. Check for mistakes.
}
return;
})
}
}


render() {
Expand Down Expand Up @@ -141,7 +138,7 @@ class ServersListTable extends React.Component<ServersListTableProp, ServersList
entityType={"Server"}
listTableData={listTableData}
headerData={headerData}
deleteEntity={undefined}
deleteEntity={this.deleteServer}
banEntity={undefined}
downloadEntity={undefined} />
</div>
Expand Down
1 change: 1 addition & 0 deletions pkg/manager/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ type ManagerDB interface {
CreateServerEntry(sinfo types.ServerInfo) error
GetServers() (types.ServerInfoList, error)
GetServer(name string) (types.ServerInfo, error)
DeleteServer(name string) error
}
24 changes: 23 additions & 1 deletion pkg/manager/db/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/spiffe/tornjak/pkg/manager/types"
)

// TO DO: Add DELETE servers option from the data base
const (
initServersTable = "CREATE TABLE IF NOT EXISTS servers (servername TEXT PRIMARY KEY, address TEXT, tls bool, mtls bool, ca varBinary, cert varBinary, key varBinary)"
)
Expand Down Expand Up @@ -96,3 +95,26 @@ func (db *LocalSqliteDb) GetServer(name string) (types.ServerInfo, error) {

return sinfo, nil
}

func (db *LocalSqliteDb) DeleteServer(servername string) error {
statement, err := db.database.Prepare("DELETE FROM servers WHERE servername=?")
if err != nil {
return errors.Errorf("Unable to prepare SQL delete statement: %v", err)
}

result, err := statement.Exec(servername)
if err != nil {
return errors.Errorf("Unable to execute SQL delete statement: %v", err)
}

rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.Errorf("Error checking affected rows: %v", err)
}

if rowsAffected == 0 {
return errors.Errorf("No server found with name: %s", servername)
}

return nil
}
56 changes: 56 additions & 0 deletions pkg/manager/db/sqlite_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package db

import (
"fmt"
"os"
"testing"

Expand Down Expand Up @@ -47,3 +48,58 @@ func TestServerCreate(t *testing.T) {
t.Fatal("Server list should initially be empty")
}
}

func TestServerDelete(t *testing.T) {
defer cleanup()
db, err := NewLocalSqliteDB("./local-test-db")
if err != nil {
t.Fatal(err)
}

sList, err := db.GetServers()
if err != nil {
t.Fatal(err)
}
if len(sList.Servers) > 0 {
t.Fatal("Server list should initially be empty")
}

sinfo1 := types.ServerInfo{
Name: "my-server-1",
Address: "http://localhost:10000",
}

sinfo2 := types.ServerInfo{
Name: "my-server-2",
Address: "http://localhost:10000",
}

err = db.CreateServerEntry(sinfo1)
if err != nil {
t.Fatal(err)
}

err = db.CreateServerEntry(sinfo2)
if err != nil {
t.Fatal(err)
}

err = db.DeleteServer(fmt.Sprintf("%s-%s", sinfo1.Name, "server-does-not-exist-in-the-database"))
if err == nil {
t.Fatal("Deleting a server which does not exist in the database should throw an error")
}

err = db.DeleteServer(sinfo1.Name)
if err != nil {
t.Fatal(err)
}

sList, err = db.GetServers()
if err != nil {
t.Fatal(err)
}

if len(sList.Servers) != 1 || sList.Servers[0].Name != sinfo2.Name {
t.Fatal("Deleting server failed")
}
}