Hithomelabs/CFTunnels#88: Add env variable round-trip, profile-aware SSL, and getStack support
Some checks failed
sample gradle build and test / build (pull_request) Has been cancelled

This commit is contained in:
hitanshu310 2026-07-06 12:29:43 +05:30
parent 3c47643cd4
commit d27176a7d5
8 changed files with 145 additions and 13 deletions

View File

@ -4,6 +4,7 @@ import com.hithomelabs.common.portainer.client.exception.PortainerAuthentication
import com.hithomelabs.common.portainer.client.exception.PortainerConnectionException;
import com.hithomelabs.common.portainer.client.exception.PortainerDeploymentException;
import com.hithomelabs.common.portainer.client.exception.PortainerResourceNotFoundException;
import com.hithomelabs.common.portainer.model.EnvVariable;
import com.hithomelabs.common.portainer.model.PortainerAuthRequest;
import com.hithomelabs.common.portainer.model.PortainerAuthResponse;
import com.hithomelabs.common.portainer.model.PortainerRedeployRequest;
@ -195,11 +196,50 @@ public class PortainerApiClient {
}
}
/**
* Fetch a single stack by its ID.
*/
public PortainerStack getStack(Long stackId) {
if (stackId == null) {
throw new PortainerDeploymentException("Stack ID must not be null");
}
HttpEntity<Void> entity = buildAuthEntity();
try {
ResponseEntity<PortainerStack> response = restTemplate.exchange(
baseUrl + API_STACKS + "/{stackId}",
HttpMethod.GET,
entity,
PortainerStack.class,
stackId);
if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
throw new PortainerResourceNotFoundException(
"Stack with ID " + stackId + " not found");
}
return response.getBody();
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new PortainerAuthenticationException(
"Not authenticated. Please authenticate before fetching stack.", e);
}
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new PortainerResourceNotFoundException(
"Stack with ID " + stackId + " not found", e);
}
throw new PortainerDeploymentException(
"Failed to fetch stack " + stackId + ": " + e.getStatusCode(), e);
} catch (ResourceAccessException e) {
throw new PortainerConnectionException(
"Unable to connect to Portainer at " + baseUrl + ": " + e.getMessage(), e);
}
}
/**
* Redeploy a git-based stack.
* Sends a PUT to {@code /api/stacks/{stackId}/git/redeploy?endpointId={endpointId}}.
*
* @param env optional list of environment variables to pass for compose variable substitution
*/
public void redeployGitStack(Long stackId, Long endpointId, boolean pullImage) {
public void redeployGitStack(Long stackId, Long endpointId, boolean pullImage, List<EnvVariable> env) {
if (stackId == null) {
throw new PortainerDeploymentException("Stack ID must not be null");
}
@ -207,7 +247,7 @@ public class PortainerApiClient {
throw new PortainerDeploymentException("Endpoint ID must not be null");
}
PortainerRedeployRequest body = new PortainerRedeployRequest(pullImage, false);
PortainerRedeployRequest body = new PortainerRedeployRequest(pullImage, false, env);
HttpEntity<PortainerRedeployRequest> entity = new HttpEntity<>(body, buildAuthHeaders());
try {

View File

@ -0,0 +1,24 @@
package com.hithomelabs.common.portainer.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class EnvVariable {
@JsonProperty("name")
private String name;
@JsonProperty("value")
private String value;
public EnvVariable() {}
public EnvVariable(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}

View File

@ -2,6 +2,8 @@ package com.hithomelabs.common.portainer.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class PortainerRedeployRequest {
@JsonProperty("PullImage")
@ -10,6 +12,9 @@ public class PortainerRedeployRequest {
@JsonProperty("Prune")
private boolean prune;
@JsonProperty("Env")
private List<EnvVariable> env;
public PortainerRedeployRequest() {}
public PortainerRedeployRequest(boolean pullImage, boolean prune) {
@ -17,6 +22,12 @@ public class PortainerRedeployRequest {
this.prune = prune;
}
public PortainerRedeployRequest(boolean pullImage, boolean prune, List<EnvVariable> env) {
this.pullImage = pullImage;
this.prune = prune;
this.env = env;
}
public boolean isPullImage() {
return pullImage;
}
@ -32,4 +43,12 @@ public class PortainerRedeployRequest {
public void setPrune(boolean prune) {
this.prune = prune;
}
public List<EnvVariable> getEnv() {
return env;
}
public void setEnv(List<EnvVariable> env) {
this.env = env;
}
}

View File

@ -2,6 +2,9 @@ package com.hithomelabs.common.portainer.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
public class PortainerStack {
@JsonProperty("Id")
@ -17,7 +20,10 @@ public class PortainerStack {
private int status;
@JsonProperty("ResourceControl")
private String resourceControl;
private Map<String, Object> resourceControl;
@JsonProperty("Env")
private List<EnvVariable> env;
public PortainerStack() {}
@ -53,11 +59,19 @@ public class PortainerStack {
this.status = status;
}
public String getResourceControl() {
public Map<String, Object> getResourceControl() {
return resourceControl;
}
public void setResourceControl(String resourceControl) {
public void setResourceControl(Map<String, Object> resourceControl) {
this.resourceControl = resourceControl;
}
public List<EnvVariable> getEnv() {
return env;
}
public void setEnv(List<EnvVariable> env) {
this.env = env;
}
}

View File

@ -5,24 +5,38 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil
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.context.annotation.Profile;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.hithomelabs.common.portainer.client.PortainerApiClient;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
@Configuration
public class PortainerClientConfig {
@Bean
public RestTemplate portainerRestTemplate() throws Exception {
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial((chain, authType) -> true)
.build();
/**
* Trust-all SSL RestTemplate for the "local" profile.
* Used when connecting via Cloudflare Tunnel (self-signed certs).
*/
@Profile("local")
@Bean(name = "portainerRestTemplate")
public RestTemplate portainerRestTemplateLocal() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
SSLConnectionSocketFactory sslSocketFactory =
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
@ -32,6 +46,16 @@ public class PortainerClientConfig {
HttpClients.custom().setConnectionManager(cm).build()));
}
/**
* Standard validating SSL RestTemplate for non-local profiles
* (CI, test, prod internal Docker network).
*/
@Profile("!local")
@Bean(name = "portainerRestTemplate")
public RestTemplate portainerRestTemplate() {
return new RestTemplate();
}
@Bean
public PortainerApiClient portainerApiClient(PortainerAutomationProperties props,
RestTemplate portainerRestTemplate) {

View File

@ -3,8 +3,12 @@ package com.hithomelabs.portainer.service;
import org.springframework.stereotype.Service;
import com.hithomelabs.common.portainer.client.PortainerApiClient;
import com.hithomelabs.common.portainer.model.EnvVariable;
import com.hithomelabs.common.portainer.model.PortainerStack;
import com.hithomelabs.portainer.config.PortainerAutomationProperties;
import java.util.List;
@Service
public class DeployService {
@ -18,6 +22,8 @@ public class DeployService {
}
public void redeploy(Long stackId) {
portainerApiClient.redeployGitStack(stackId, props.getEndpointId(), true);
PortainerStack stack = portainerApiClient.getStack(stackId);
List<EnvVariable> env = stack.getEnv();
portainerApiClient.redeployGitStack(stackId, props.getEndpointId(), true, env);
}
}

View File

@ -0,0 +1,4 @@
portainer.base-url=https://devdocker.hithomelabs.com
portainer.api-key=${PORTAINER_API_KEY:dev-test-key}
portainer.endpoint-id=2
portainer.service.api-key=dev-test-key

View File

@ -1,4 +1,5 @@
server.port=8081
portainer.base-url=https://192.168.0.100:9442
portainer.endpoint-id=2
portainer.api-key=${PORTAINER_API_KEY:}
portainer.endpoint-id=1
portainer.service.api-key=change-me