88: Build portainer-automation service #131
@ -5,6 +5,7 @@ dependencies {
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.apache.httpcomponents.client5:httpclient5'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
@ -2,8 +2,12 @@ package com.hithomelabs.portainer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@SpringBootApplication
|
||||
import com.hithomelabs.portainer.config.PortainerAutomationProperties;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = {"com.hithomelabs.portainer", "com.hithomelabs.common"})
|
||||
@EnableConfigurationProperties(PortainerAutomationProperties.class)
|
||||
public class PortainerAutomationApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
package com.hithomelabs.portainer.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "portainer")
|
||||
public class PortainerAutomationProperties {
|
||||
private String baseUrl;
|
||||
private String apiKey;
|
||||
private Long endpointId;
|
||||
private Service service = new Service();
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public Long getEndpointId() {
|
||||
return endpointId;
|
||||
}
|
||||
|
||||
public void setEndpointId(Long endpointId) {
|
||||
this.endpointId = endpointId;
|
||||
}
|
||||
|
||||
public Service getService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
public void setService(Service service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public static class Service {
|
||||
private String apiKey;
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.hithomelabs.portainer.config;
|
||||
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.hc.core5.ssl.SSLContexts;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.hithomelabs.common.portainer.client.PortainerApiClient;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
@Configuration
|
||||
public class PortainerClientConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate portainerRestTemplate() throws Exception {
|
||||
SSLContext sslContext = SSLContexts.custom()
|
||||
.loadTrustMaterial((chain, authType) -> true)
|
||||
.build();
|
||||
SSLConnectionSocketFactory sslSocketFactory =
|
||||
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setSSLSocketFactory(sslSocketFactory)
|
||||
.build();
|
||||
return new RestTemplate(new HttpComponentsClientHttpRequestFactory(
|
||||
HttpClients.custom().setConnectionManager(cm).build()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PortainerApiClient portainerApiClient(PortainerAutomationProperties props,
|
||||
RestTemplate portainerRestTemplate) {
|
||||
PortainerApiClient client = new PortainerApiClient(portainerRestTemplate, props.getBaseUrl());
|
||||
client.authenticateWithApiKey(props.getApiKey());
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.hithomelabs.portainer.controller;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.hithomelabs.portainer.config.PortainerAutomationProperties;
|
||||
import com.hithomelabs.portainer.service.DeployService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/deploy")
|
||||
public class DeployController {
|
||||
|
||||
private final DeployService deployService;
|
||||
private final PortainerAutomationProperties props;
|
||||
|
||||
public DeployController(DeployService deployService, PortainerAutomationProperties props) {
|
||||
this.deployService = deployService;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
@PostMapping("/{stackId}")
|
||||
public ResponseEntity<String> deploy(@PathVariable Long stackId,
|
||||
@RequestHeader(value = "X-API-Key", required = false) String apiKey) {
|
||||
String expectedApiKey = props.getService().getApiKey();
|
||||
if (expectedApiKey == null || !expectedApiKey.equals(apiKey)) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid API key");
|
||||
}
|
||||
try {
|
||||
deployService.redeploy(stackId);
|
||||
return ResponseEntity.ok("Deployment initiated for stack " + stackId);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Deployment failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.hithomelabs.portainer.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.hithomelabs.common.portainer.client.PortainerApiClient;
|
||||
import com.hithomelabs.portainer.config.PortainerAutomationProperties;
|
||||
|
||||
@Service
|
||||
public class DeployService {
|
||||
|
||||
private final PortainerApiClient portainerApiClient;
|
||||
private final PortainerAutomationProperties props;
|
||||
|
||||
public DeployService(PortainerApiClient portainerApiClient,
|
||||
PortainerAutomationProperties props) {
|
||||
this.portainerApiClient = portainerApiClient;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
public void redeploy(Long stackId) {
|
||||
portainerApiClient.redeployGitStack(stackId, props.getEndpointId(), true);
|
||||
}
|
||||
}
|
||||
@ -1 +1,4 @@
|
||||
server.port=8081
|
||||
portainer.base-url=https://192.168.0.100:9442
|
||||
portainer.endpoint-id=2
|
||||
portainer.service.api-key=change-me
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
package com.hithomelabs.portainer.controller;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import com.hithomelabs.portainer.config.PortainerAutomationProperties;
|
||||
import com.hithomelabs.portainer.service.DeployService;
|
||||
|
||||
@WebMvcTest(DeployController.class)
|
||||
class DeployControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private DeployService deployService;
|
||||
|
||||
@MockitoBean
|
||||
private PortainerAutomationProperties props;
|
||||
|
||||
@MockitoBean
|
||||
private PortainerAutomationProperties.Service service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(props.getService()).thenReturn(service);
|
||||
when(service.getApiKey()).thenReturn("valid-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployWithValidKeyReturnsOk() throws Exception {
|
||||
mockMvc.perform(post("/api/deploy/1")
|
||||
.header("X-API-Key", "valid-key"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("Deployment initiated for stack 1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployWithInvalidKeyReturnsUnauthorized() throws Exception {
|
||||
mockMvc.perform(post("/api/deploy/1")
|
||||
.header("X-API-Key", "wrong-key"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(content().string("Invalid API key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployWithMissingKeyReturnsUnauthorized() throws Exception {
|
||||
mockMvc.perform(post("/api/deploy/1"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(content().string("Invalid API key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployWhenServiceFailsReturnsServerError() throws Exception {
|
||||
doThrow(new RuntimeException("Portainer error"))
|
||||
.when(deployService).redeploy(anyLong());
|
||||
mockMvc.perform(post("/api/deploy/1")
|
||||
.header("X-API-Key", "valid-key"))
|
||||
.andExpect(status().isInternalServerError())
|
||||
.andExpect(content().string("Deployment failed: Portainer error"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user