Compare commits
10 Commits
6fc44a6969
...
3c47643cd4
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c47643cd4 | |||
| 93e9baa5d4 | |||
| d0e1ea938f | |||
| 76dd43c7b2 | |||
| 21e67dadea | |||
| 5ea62d6baf | |||
| cf24922bb4 | |||
| 62fd2ea48a | |||
| 9e3207dc30 | |||
| f3652611ec |
@ -5,6 +5,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
implementation 'org.apache.httpcomponents.client5:httpclient5'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|||||||
@ -2,8 +2,12 @@ package com.hithomelabs.portainer;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
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 class PortainerAutomationApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
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
|
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