Skip to content

Commit a8d4b45

Browse files
committed
Fix issues with killing agents
1 parent af13eaf commit a8d4b45

File tree

9 files changed

+275
-20
lines changed

9 files changed

+275
-20
lines changed

agent-launcher/src/main/java/io/sentrius/agent/launcher/api/AgentLauncherController.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import io.sentrius.agent.launcher.service.PodLauncherService;
55
import io.sentrius.sso.config.ApiPaths;
66
import io.sentrius.sso.core.annotations.LimitAccess;
7-
import io.sentrius.sso.core.dto.AgentDTO;
87
import io.sentrius.sso.core.dto.AgentRegistrationDTO;
98
import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum;
109
import io.sentrius.sso.core.services.security.KeycloakService;
@@ -13,10 +12,13 @@
1312
import lombok.extern.slf4j.Slf4j;
1413
import org.apache.http.HttpStatus;
1514
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.DeleteMapping;
16+
import org.springframework.web.bind.annotation.GetMapping;
1617
import org.springframework.web.bind.annotation.PostMapping;
1718
import org.springframework.web.bind.annotation.RequestBody;
1819
import org.springframework.web.bind.annotation.RequestHeader;
1920
import org.springframework.web.bind.annotation.RequestMapping;
21+
import org.springframework.web.bind.annotation.RequestParam;
2022
import org.springframework.web.bind.annotation.RestController;
2123

2224
@Slf4j
@@ -54,4 +56,15 @@ public ResponseEntity<?> createPod(
5456
return ResponseEntity.ok(Map.of("status", "success"));
5557
}
5658

59+
@GetMapping("/kill")
60+
public ResponseEntity<String> deleteAgent(@RequestParam(name="agentId") String agentId) {
61+
try {
62+
podLauncherService.deleteAgentById(agentId);
63+
return ResponseEntity.ok("Shutdown triggered");
64+
} catch (Exception e) {
65+
e.printStackTrace();
66+
return ResponseEntity.status(500).body("Shutdown failed: " + e.getMessage());
67+
}
68+
}
69+
5770
}
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,56 @@
11
package io.sentrius.agent.launcher.service;
22

3+
import java.util.List;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.beans.factory.annotation.Value;
36
import org.springframework.context.annotation.Bean;
47
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.http.HttpMethod;
59
import org.springframework.security.config.Customizer;
610
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
711
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
812
import org.springframework.security.web.SecurityFilterChain;
13+
import org.springframework.web.cors.CorsConfiguration;
14+
import org.springframework.web.cors.CorsConfigurationSource; // ← correct package
15+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; // ← correct package
916

1017
@Configuration
1118
@EnableWebSecurity
19+
@Slf4j
1220
public class LauncherSecurityConfig {
1321

22+
@Value("${agent.api.url:http://localhost:8080}")
23+
private String agentApiUrl;
24+
1425
@Bean
1526
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
1627
http
28+
.cors(Customizer.withDefaults()) // ✅ make sure this is here
1729
.authorizeHttpRequests(auth -> auth
1830
.requestMatchers("/actuator/**").permitAll()
31+
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
1932
.anyRequest().authenticated()
2033
)
2134
.oauth2ResourceServer(oauth2 -> oauth2
2235
.jwt(Customizer.withDefaults())
2336
)
24-
.csrf(csrf -> csrf.disable()); // Compliant disable for CSRF
37+
.csrf(csrf -> csrf.disable());
2538

2639
return http.build();
2740
}
41+
42+
@Bean
43+
public CorsConfigurationSource corsConfigurationSource() {
44+
CorsConfiguration config = new CorsConfiguration();
45+
46+
log.info("Configuring CORS for agent API URL: {}", agentApiUrl);
47+
config.setAllowedOrigins(List.of(agentApiUrl)); // e.g., https://frontend.local
48+
config.setAllowedMethods(List.of("GET", "POST", "OPTIONS", "DELETE", "PUT"));
49+
config.setAllowedHeaders(List.of("*"));
50+
config.setAllowCredentials(true);
51+
52+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
53+
source.registerCorsConfiguration("/**", config);
54+
return source;
55+
}
2856
}

agent-launcher/src/main/java/io/sentrius/agent/launcher/service/PodLauncherService.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.io.IOException;
1414
import java.util.List;
1515
import java.util.Map;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
1618

1719
@Slf4j
1820
@Service
@@ -32,6 +34,10 @@ public class PodLauncherService {
3234
@Value("${sentrius.agent.callback.format.url:http://sentrius-agent-%s.%s.svc.cluster.local:8090}")
3335
private String callbackFormatUrl;
3436

37+
38+
Pattern pattern = Pattern.compile("^service-account-(.*?)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
39+
40+
3541
public PodLauncherService() throws IOException {
3642
ApiClient client = Config.defaultClient(); // in-cluster or kubeconfig
3743
this.coreV1Api = new CoreV1Api(client);
@@ -41,6 +47,74 @@ private String buildAgentCallbackUrl(String agentId) {
4147
return String.format(callbackFormatUrl, agentId, agentNamespace);
4248
}
4349

50+
public List<V1Pod> listAgentPods() throws Exception {
51+
var response = coreV1Api.listNamespacedPod(
52+
agentNamespace
53+
);
54+
return response.execute().getItems();
55+
}
56+
57+
public void deleteAllAgentPods() throws Exception {
58+
var pods = listAgentPods();
59+
for (V1Pod pod : pods) {
60+
String podName = pod.getMetadata().getName();
61+
String agentId = pod.getMetadata().getLabels().get("agentId");
62+
63+
log.info("Deleting agent pod: {}", podName);
64+
coreV1Api.deleteNamespacedPod(podName, agentNamespace).execute();
65+
66+
String serviceName = "sentrius-agent-" + agentId;
67+
log.info("Deleting agent service: {}", serviceName);
68+
try {
69+
coreV1Api.deleteNamespacedService(serviceName, agentNamespace).execute();
70+
} catch (Exception ex) {
71+
log.warn("Could not delete service {}: {}", serviceName, ex.getMessage());
72+
}
73+
}
74+
}
75+
76+
public void deleteAgentById(String agentId) throws Exception {
77+
// Delete all pods with this agentId label
78+
var pods = coreV1Api.listNamespacedPod(
79+
agentNamespace
80+
).execute().getItems();
81+
82+
for (V1Pod pod : pods) {
83+
84+
var labels = pod.getMetadata().getLabels();
85+
var podName = pod.getMetadata().getName();
86+
87+
Matcher matcher = pattern.matcher(agentId);
88+
89+
if (matcher.matches() && labels != null && labels.containsKey("agentId")) {
90+
String name = matcher.group(1);
91+
92+
var value = labels.get("agentId");
93+
if (value.equals(name)) {
94+
log.info("Deleting pod: {}", podName);
95+
coreV1Api.deleteNamespacedPod(podName, agentNamespace).execute();
96+
String serviceName = "sentrius-agent-" + agentId;
97+
log.info("Deleting service: {}", serviceName);
98+
try {
99+
coreV1Api.deleteNamespacedService(serviceName, agentNamespace).execute();
100+
} catch (Exception ex) {
101+
log.warn("Service not found or already deleted: {}", ex.getMessage());
102+
}
103+
}else {
104+
log.info("Not Deleting pod: {}", podName);
105+
}
106+
107+
108+
} else {
109+
log.info("Pod {} does not match agentId pattern or has no agentId label, skipping deletion", podName);
110+
}
111+
112+
113+
}
114+
115+
116+
}
117+
44118

45119
public V1Pod launchAgentPod(String agentId, String callbackUrl) throws Exception {
46120
var myAgentRegistry = "";

ai-agent/src/main/java/io/sentrius/agent/analysis/agents/verbs/ChatVerbs.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,27 @@ public TerminalResponse interpretUserData(
112112
} else {
113113
InputStream terminalHelperStream = getClass().getClassLoader().getResourceAsStream("terminal-helper.json");
114114
if (terminalHelperStream == null) {
115+
116+
117+
118+
119+
120+
121+
122+
123+
124+
125+
126+
127+
128+
129+
130+
131+
132+
133+
134+
135+
115136
throw new RuntimeException("assessor-config.yaml not found on classpath");
116137

117138
}

api/src/main/java/io/sentrius/sso/controllers/api/AgentBootstrapController.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import java.io.IOException;
44
import java.io.InputStream;
55
import java.security.GeneralSecurityException;
6+
import java.util.ArrayList;
67
import java.util.List;
78
import java.util.Map;
89
import com.fasterxml.jackson.databind.ObjectMapper;
910
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
11+
import com.google.common.collect.Maps;
1012
import io.sentrius.sso.config.ApiPaths;
1113
import io.sentrius.sso.config.AppConfig;
1214
import io.sentrius.sso.core.annotations.LimitAccess;
@@ -37,10 +39,12 @@
3739
import lombok.extern.slf4j.Slf4j;
3840
import org.springframework.beans.factory.annotation.Value;
3941
import org.springframework.http.ResponseEntity;
42+
import org.springframework.web.bind.annotation.DeleteMapping;
4043
import org.springframework.web.bind.annotation.PostMapping;
4144
import org.springframework.web.bind.annotation.RequestBody;
4245
import org.springframework.web.bind.annotation.RequestHeader;
4346
import org.springframework.web.bind.annotation.RequestMapping;
47+
import org.springframework.web.bind.annotation.RequestParam;
4448
import org.springframework.web.bind.annotation.RestController;
4549

4650
@Slf4j
@@ -172,6 +176,21 @@ public ResponseEntity<String> launchPod(
172176
return ResponseEntity.ok("{\"status\": \"success\"}");
173177
}
174178

179+
@PostMapping("/launcher/kill")
180+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
181+
public ResponseEntity<String> deletePod(
182+
@RequestParam(name="agentId") String agentName, HttpServletRequest request, HttpServletResponse response
183+
) throws GeneralSecurityException, IOException, ZtatException {
184+
185+
186+
var operatingUser = getOperatingUser(request, response );
187+
188+
zeroTrustClientService.callAuthenticatedGetOnApi(appConfig.getSentriusLauncherService(), "agent/launcher" +
189+
"/kill", Maps.immutableEntry("agentId", List.of(agentName)) );
190+
// bootstrap with a default policy
191+
return ResponseEntity.ok("{\"status\": \"success\"}");
192+
}
193+
175194

176195

177196

api/src/main/java/io/sentrius/sso/controllers/api/ChatApiController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public Map<String, String> getChatConfig() {
8888
config.put("agentProxyWsUrl", wssUrl);
8989
}
9090
}
91+
config.put("sentriusLauncherService", appConfig.getSentriusLauncherService());
9192
return config;
9293
}
9394

api/src/main/resources/templates/sso/enclaves/list_servers.html

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,12 @@
7474
<div id="chat-messages"></div>
7575
<input type="text" id="chat-input" placeholder="Type a message..." onkeydown="sendMessage(event)">
7676
</div>
77+
7778
<div class="navbar navbar-dark fixed-top navbar-expand-md" role="navigation" xmlns:th="http://www.thymeleaf.org">
7879
<div class="container bg-dark px-3 py-2" >
7980
<!-- Brand Logo -->
80-
<a class="navbar-brand" href="#" style="padding-left: 45px;">
81-
<a href="/sso/v1/dashboard" class="nav-link">
82-
<img th:src="${systemOptions.systemLogoPathSmall}" alt="Logo" class="nav-img">
83-
</a>
81+
<a class="navbar-brand nav-link" href="/sso/v1/dashboard" style="padding-left: 45px;">
82+
<img th:src="${systemOptions.systemLogoPathSmall}" alt="Logo" class="nav-img">
8483
</a>
8584

8685
<!-- Toggler for Mobile -->
@@ -159,12 +158,20 @@ <h3>Available Agents</h3>
159158
</div>
160159
<script type="module" th:src="@{/js/add_system.js}" defer></script>
161160
<script type="module" th:src="@{/js/functions.js}"></script>
162-
<script>
161+
<script th:inline="javascript">
162+
163+
let config;
164+
fetch("/api/v1/chat/config")
165+
.then(r => r.json())
166+
.then(data => {
167+
config = data;
168+
// proceed with code using config here
169+
});
163170

164171
function deleteServer(groupId,data) {
165172
// The API URL you want to call
166-
var csrf = "[[${_csrf.token}]]"
167-
let terminalsInNewTab = [[${systemOptions.terminalsInNewTab != null ? systemOptions.terminalsInNewTab : false}]];
173+
var csrf = /*[[${_csrf.token}]]*/ "";
174+
let terminalsInNewTab = /*[[${systemOptions.terminalsInNewTab != null ? systemOptions.terminalsInNewTab : false}]]*/ false;
168175
console.log("Data: ", data);
169176
const apiUrl = `/api/v1/enclaves/hosts/delete/${groupId}/${data}`;
170177

@@ -197,9 +204,45 @@ <h3>Available Agents</h3>
197204
});
198205
}
199206

200-
function connectToServer(groupId,data) {
207+
function deleteAgent(data) {
201208
// The API URL you want to call
202-
let terminalsInNewTab = [[${systemOptions.terminalsInNewTab != null ? systemOptions.terminalsInNewTab : false}]];
209+
var csrf = /*[[${_csrf.token}]]*/ "";
210+
console.log("Data: ", data);
211+
const apiUrl = `/api/v1/agent/bootstrap/launcher/kill?agentId=${data}`;
212+
213+
// Perform the API call
214+
fetch(apiUrl, {
215+
method: 'POST',
216+
headers: {
217+
'Content-Type': 'application/json',
218+
'X-CSRF-TOKEN': csrf // Include the CSRF token in the headers
219+
}
220+
})
221+
.then(response => {
222+
if (response.ok) {
223+
// Assuming the API returns a URL to redirect to
224+
return response.json();
225+
} else {
226+
throw new Error('Failed to connect to server');
227+
}
228+
})
229+
.then(data => {
230+
231+
$("#alertTopError").hide();
232+
$("#alertTop").text("Agent Deleted").show().delay(3000).fadeOut();
233+
$('#agent-table').DataTable().ajax.reload();
234+
})
235+
.catch(error => {
236+
console.log(error);
237+
$("#alertTop").hide();
238+
$("#alertTopError").text("An unknown error occurred").show().delay(3000).fadeOut();
239+
});
240+
}
241+
242+
function connectToServer(groupId,data) {
243+
// The API URL yo
244+
// u want to call
245+
let terminalsInNewTab = /*[[${systemOptions.terminalsInNewTab != null ? systemOptions.terminalsInNewTab : false}]]*/ false;
203246
console.log("Data: ", data);
204247
const apiUrl = `/api/v1/enclaves/hosts/connect/${groupId}/${data}`;
205248

@@ -326,15 +369,13 @@ <h3>Available Agents</h3>
326369
{
327370
data: null,
328371
render: function(data, type, row) {
329-
/*
330-
const groupId = row.group ? row.group.groupId : -1; // Access group.id
331-
const id = row.id;
332-
let
333-
ret=`<button class="btn btn-primary" onclick="connectToServer(${groupId}, ${id})">Disconnect</button> `;
372+
373+
const id = row.agentName;
374+
let ret=``;
334375
if (canDelete) {
335-
ret += `<button class="btn btn-primary" onclick="deleteServer(${groupId}, ${id})">Delete</button>`;
336-
}*/
337-
return "";
376+
ret += `<button class="btn btn-danger" onclick="deleteAgent('${id}')">Kill Agent</button>`;
377+
}
378+
return ret;
338379
}
339380
}
340381

0 commit comments

Comments
 (0)