diff --git a/api/manager/api.go b/api/manager/api.go index e1a869bf..82b00ed1 100644 --- a/api/manager/api.go +++ b/api/manager/api.go @@ -36,3 +36,7 @@ func (s *Server) RegisterServer(inp RegisterServerRequest) error { return s.db.CreateServerEntry(sinfo) } + +type DeleteServerRequest struct { + Server managertypes.ServerInfo `json:"server"` +} diff --git a/api/manager/server.go b/api/manager/server.go index 6ed166e6..c3f2c5d7 100644 --- a/api/manager/server.go +++ b/api/manager/server.go @@ -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))) @@ -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 + } +} diff --git a/docs/tornjak-ui-api-documentation.md b/docs/tornjak-ui-api-documentation.md index eb98d959..5588afc6 100644 --- a/docs/tornjak-ui-api-documentation.md +++ b/docs/tornjak-ui-api-documentation.md @@ -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 diff --git a/frontend/src/components/tornjak-api-helpers.tsx b/frontend/src/components/tornjak-api-helpers.tsx index 44edb71f..5076093b 100644 --- a/frontend/src/components/tornjak-api-helpers.tsx +++ b/frontend/src/components/tornjak-api-helpers.tsx @@ -10,6 +10,7 @@ import { ServerInfo, EntriesList, ClustersList, + ServersList, DebugServerInfo, FederationsList } from './types'; @@ -344,6 +345,23 @@ class TornjakApi extends Component { 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)) + 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; }, diff --git a/frontend/src/tables/servers-list-table.tsx b/frontend/src/tables/servers-list-table.tsx index f559fea7..5334bdc2 100644 --- a/frontend/src/tables/servers-list-table.tsx +++ b/frontend/src/tables/servers-list-table.tsx @@ -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 @@ -50,7 +52,7 @@ class ServersListTable extends React.Component diff --git a/pkg/manager/db/db.go b/pkg/manager/db/db.go index 8ac1f8cb..ec6b3ee0 100644 --- a/pkg/manager/db/db.go +++ b/pkg/manager/db/db.go @@ -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 } diff --git a/pkg/manager/db/sqlite.go b/pkg/manager/db/sqlite.go index f6c85e51..73fd9534 100644 --- a/pkg/manager/db/sqlite.go +++ b/pkg/manager/db/sqlite.go @@ -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)" ) @@ -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 +} diff --git a/pkg/manager/db/sqlite_test.go b/pkg/manager/db/sqlite_test.go index cf687527..ba77fc2c 100644 --- a/pkg/manager/db/sqlite_test.go +++ b/pkg/manager/db/sqlite_test.go @@ -1,6 +1,7 @@ package db import ( + "fmt" "os" "testing" @@ -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") + } +}