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
Some checks failed
sample gradle build and test / build (pull_request) Has been cancelled
This commit is contained in:
parent
3c47643cd4
commit
d27176a7d5
@ -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.PortainerConnectionException;
|
||||||
import com.hithomelabs.common.portainer.client.exception.PortainerDeploymentException;
|
import com.hithomelabs.common.portainer.client.exception.PortainerDeploymentException;
|
||||||
import com.hithomelabs.common.portainer.client.exception.PortainerResourceNotFoundException;
|
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.PortainerAuthRequest;
|
||||||
import com.hithomelabs.common.portainer.model.PortainerAuthResponse;
|
import com.hithomelabs.common.portainer.model.PortainerAuthResponse;
|
||||||
import com.hithomelabs.common.portainer.model.PortainerRedeployRequest;
|
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.
|
* Redeploy a git-based stack.
|
||||||
* Sends a PUT to {@code /api/stacks/{stackId}/git/redeploy?endpointId={endpointId}}.
|
* 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) {
|
if (stackId == null) {
|
||||||
throw new PortainerDeploymentException("Stack ID must not be 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");
|
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());
|
HttpEntity<PortainerRedeployRequest> entity = new HttpEntity<>(body, buildAuthHeaders());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ package com.hithomelabs.common.portainer.model;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class PortainerRedeployRequest {
|
public class PortainerRedeployRequest {
|
||||||
|
|
||||||
@JsonProperty("PullImage")
|
@JsonProperty("PullImage")
|
||||||
@ -10,6 +12,9 @@ public class PortainerRedeployRequest {
|
|||||||
@JsonProperty("Prune")
|
@JsonProperty("Prune")
|
||||||
private boolean prune;
|
private boolean prune;
|
||||||
|
|
||||||
|
@JsonProperty("Env")
|
||||||
|
private List<EnvVariable> env;
|
||||||
|
|
||||||
public PortainerRedeployRequest() {}
|
public PortainerRedeployRequest() {}
|
||||||
|
|
||||||
public PortainerRedeployRequest(boolean pullImage, boolean prune) {
|
public PortainerRedeployRequest(boolean pullImage, boolean prune) {
|
||||||
@ -17,6 +22,12 @@ public class PortainerRedeployRequest {
|
|||||||
this.prune = prune;
|
this.prune = prune;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PortainerRedeployRequest(boolean pullImage, boolean prune, List<EnvVariable> env) {
|
||||||
|
this.pullImage = pullImage;
|
||||||
|
this.prune = prune;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPullImage() {
|
public boolean isPullImage() {
|
||||||
return pullImage;
|
return pullImage;
|
||||||
}
|
}
|
||||||
@ -32,4 +43,12 @@ public class PortainerRedeployRequest {
|
|||||||
public void setPrune(boolean prune) {
|
public void setPrune(boolean prune) {
|
||||||
this.prune = prune;
|
this.prune = prune;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<EnvVariable> getEnv() {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnv(List<EnvVariable> env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package com.hithomelabs.common.portainer.model;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class PortainerStack {
|
public class PortainerStack {
|
||||||
|
|
||||||
@JsonProperty("Id")
|
@JsonProperty("Id")
|
||||||
@ -17,7 +20,10 @@ public class PortainerStack {
|
|||||||
private int status;
|
private int status;
|
||||||
|
|
||||||
@JsonProperty("ResourceControl")
|
@JsonProperty("ResourceControl")
|
||||||
private String resourceControl;
|
private Map<String, Object> resourceControl;
|
||||||
|
|
||||||
|
@JsonProperty("Env")
|
||||||
|
private List<EnvVariable> env;
|
||||||
|
|
||||||
public PortainerStack() {}
|
public PortainerStack() {}
|
||||||
|
|
||||||
@ -53,11 +59,19 @@ public class PortainerStack {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getResourceControl() {
|
public Map<String, Object> getResourceControl() {
|
||||||
return resourceControl;
|
return resourceControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResourceControl(String resourceControl) {
|
public void setResourceControl(Map<String, Object> resourceControl) {
|
||||||
this.resourceControl = resourceControl;
|
this.resourceControl = resourceControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<EnvVariable> getEnv() {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnv(List<EnvVariable> env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.io.HttpClientConnectionManager;
|
||||||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import com.hithomelabs.common.portainer.client.PortainerApiClient;
|
import com.hithomelabs.common.portainer.client.PortainerApiClient;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class PortainerClientConfig {
|
public class PortainerClientConfig {
|
||||||
|
|
||||||
@Bean
|
/**
|
||||||
public RestTemplate portainerRestTemplate() throws Exception {
|
* Trust-all SSL RestTemplate for the "local" profile.
|
||||||
SSLContext sslContext = SSLContexts.custom()
|
* Used when connecting via Cloudflare Tunnel (self-signed certs).
|
||||||
.loadTrustMaterial((chain, authType) -> true)
|
*/
|
||||||
.build();
|
@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 =
|
SSLConnectionSocketFactory sslSocketFactory =
|
||||||
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||||
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
|
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
|
||||||
@ -32,6 +46,16 @@ public class PortainerClientConfig {
|
|||||||
HttpClients.custom().setConnectionManager(cm).build()));
|
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
|
@Bean
|
||||||
public PortainerApiClient portainerApiClient(PortainerAutomationProperties props,
|
public PortainerApiClient portainerApiClient(PortainerAutomationProperties props,
|
||||||
RestTemplate portainerRestTemplate) {
|
RestTemplate portainerRestTemplate) {
|
||||||
|
|||||||
@ -3,8 +3,12 @@ package com.hithomelabs.portainer.service;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.hithomelabs.common.portainer.client.PortainerApiClient;
|
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 com.hithomelabs.portainer.config.PortainerAutomationProperties;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DeployService {
|
public class DeployService {
|
||||||
|
|
||||||
@ -18,6 +22,8 @@ public class DeployService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void redeploy(Long stackId) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -1,4 +1,5 @@
|
|||||||
server.port=8081
|
server.port=8081
|
||||||
portainer.base-url=https://192.168.0.100:9442
|
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
|
portainer.service.api-key=change-me
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user