From f3652611ec064c8edb90addef03b0ed5447ff35b Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:01:56 +0000 Subject: [PATCH 01/10] 88: Add httpclient5 dependency for portainer-automation --- portainer-automation/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/portainer-automation/build.gradle b/portainer-automation/build.gradle index bbe3892..0cfaa3e 100644 --- a/portainer-automation/build.gradle +++ b/portainer-automation/build.gradle @@ -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.45.2 From 9e3207dc30cec8c4614c6247cbf4dfd72cf0574c Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:01:58 +0000 Subject: [PATCH 02/10] 88: Enable configuration properties and scan common package --- .../portainer/PortainerAutomationApplication.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/PortainerAutomationApplication.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/PortainerAutomationApplication.java index a848ca2..d190259 100644 --- a/portainer-automation/src/main/java/com/hithomelabs/portainer/PortainerAutomationApplication.java +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/PortainerAutomationApplication.java @@ -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) { -- 2.45.2 From 62fd2ea48aa29ebd48bc5fcb28611e3b3af75c45 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:01:59 +0000 Subject: [PATCH 03/10] 88: Add portainer configuration properties --- portainer-automation/src/main/resources/application.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/portainer-automation/src/main/resources/application.properties b/portainer-automation/src/main/resources/application.properties index 4d360de..680422a 100644 --- a/portainer-automation/src/main/resources/application.properties +++ b/portainer-automation/src/main/resources/application.properties @@ -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 -- 2.45.2 From cf24922bb4f646a7a497f27854fcc9e1bc6c8272 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:02:12 +0000 Subject: [PATCH 04/10] 88: Add PortainerAutomationProperties configuration --- .../config/PortainerAutomationProperties.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerAutomationProperties.java diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerAutomationProperties.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerAutomationProperties.java new file mode 100644 index 0000000..8628909 --- /dev/null +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerAutomationProperties.java @@ -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; + } + } +} -- 2.45.2 From 5ea62d6bafa3825dd98c96dd4e3be2c93fa1d851 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:02:15 +0000 Subject: [PATCH 05/10] 88: Add SSL-friendly Portainer API client config --- .../config/PortainerClientConfig.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java new file mode 100644 index 0000000..13d56dc --- /dev/null +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java @@ -0,0 +1,41 @@ +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.apache.hc.core5.ssl.TrustAllStrategy; +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.client.PortainerApiClient; + +import javax.net.ssl.SSLContext; + +@Configuration +public class PortainerClientConfig { + + @Bean + public RestTemplate portainerRestTemplate() throws Exception { + SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(TrustAllStrategy.INSTANCE) + .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) { + return new PortainerApiClient(props.getBaseUrl(), props.getApiKey(), portainerRestTemplate); + } +} -- 2.45.2 From 21e67dadeadb60ce2aba02167f1e545ac7b788d7 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:02:16 +0000 Subject: [PATCH 06/10] 88: Add DeployService for Portainer stack redeployment --- .../portainer/service/DeployService.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java new file mode 100644 index 0000000..a0d9bf6 --- /dev/null +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java @@ -0,0 +1,23 @@ +package com.hithomelabs.portainer.service; + +import org.springframework.stereotype.Service; + +import com.hithomelabs.common.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); + } +} -- 2.45.2 From 76dd43c7b272047b4550f2350a47416ee09624c4 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:02:19 +0000 Subject: [PATCH 07/10] 88: Add DeployController with X-API-Key auth --- .../controller/DeployController.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java new file mode 100644 index 0000000..afd1e34 --- /dev/null +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java @@ -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 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()); + } + } +} -- 2.45.2 From d0e1ea938fd797e6f22f1e0435d5b1ab5db0bc68 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:02:23 +0000 Subject: [PATCH 08/10] 88: Add DeployControllerTest with 4 test cases --- .../controller/DeployControllerTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 portainer-automation/src/test/java/com/hithomelabs/portainer/controller/DeployControllerTest.java diff --git a/portainer-automation/src/test/java/com/hithomelabs/portainer/controller/DeployControllerTest.java b/portainer-automation/src/test/java/com/hithomelabs/portainer/controller/DeployControllerTest.java new file mode 100644 index 0000000..e31c025 --- /dev/null +++ b/portainer-automation/src/test/java/com/hithomelabs/portainer/controller/DeployControllerTest.java @@ -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")); + } +} -- 2.45.2 From 93e9baa5d416ecadffdcb7c92ed4af36952ca3f4 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:10:20 +0000 Subject: [PATCH 09/10] 88: Fix PortainerApiClient import/constructor, replace TrustAllStrategy with lambda --- .../portainer/config/PortainerClientConfig.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java index 13d56dc..065ee2d 100644 --- a/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/config/PortainerClientConfig.java @@ -6,13 +6,12 @@ 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.apache.hc.core5.ssl.TrustAllStrategy; 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.client.PortainerApiClient; +import com.hithomelabs.common.portainer.client.PortainerApiClient; import javax.net.ssl.SSLContext; @@ -22,7 +21,7 @@ public class PortainerClientConfig { @Bean public RestTemplate portainerRestTemplate() throws Exception { SSLContext sslContext = SSLContexts.custom() - .loadTrustMaterial(TrustAllStrategy.INSTANCE) + .loadTrustMaterial((chain, authType) -> true) .build(); SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); @@ -36,6 +35,8 @@ public class PortainerClientConfig { @Bean public PortainerApiClient portainerApiClient(PortainerAutomationProperties props, RestTemplate portainerRestTemplate) { - return new PortainerApiClient(props.getBaseUrl(), props.getApiKey(), portainerRestTemplate); + PortainerApiClient client = new PortainerApiClient(portainerRestTemplate, props.getBaseUrl()); + client.authenticateWithApiKey(props.getApiKey()); + return client; } } -- 2.45.2 From 3c47643cd49b3c5d27bfffb9fb476b2d0f8f199d Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Mon, 6 Jul 2026 05:10:21 +0000 Subject: [PATCH 10/10] 88: Fix PortainerApiClient import path --- .../java/com/hithomelabs/portainer/service/DeployService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java index a0d9bf6..aedf306 100644 --- a/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java @@ -2,7 +2,7 @@ package com.hithomelabs.portainer.service; import org.springframework.stereotype.Service; -import com.hithomelabs.common.client.PortainerApiClient; +import com.hithomelabs.common.portainer.client.PortainerApiClient; import com.hithomelabs.portainer.config.PortainerAutomationProperties; @Service -- 2.45.2