diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go
index 8e2400512..6e2857363 100644
--- a/api/http/handler/endpoints/endpoint_update.go
+++ b/api/http/handler/endpoints/endpoint_update.go
@@ -4,6 +4,7 @@ import (
"net/http"
"reflect"
"strconv"
+ "strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -246,7 +247,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
- if (payload.URL != nil && *payload.URL != endpoint.URL) || (payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) || endpoint.Type == portainer.AzureEnvironment {
+ if (payload.URL != nil && *payload.URL != endpoint.URL) ||
+ (payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) ||
+ endpoint.Type == portainer.AzureEnvironment ||
+ shouldReloadTLSConfiguration(endpoint, &payload) {
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
@@ -285,3 +289,22 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
return response.JSON(w, endpoint)
}
+
+func shouldReloadTLSConfiguration(endpoint *portainer.Endpoint, payload *endpointUpdatePayload) bool {
+ // When updating Docker API environment, as long as TLS is true and TLSSkipVerify is false,
+ // we assume that new TLS files have been uploaded and we need to reload the TLS configuration.
+ if endpoint.Type != portainer.DockerEnvironment ||
+ !strings.HasPrefix(*payload.URL, "tcp://") ||
+ payload.TLS == nil || !*payload.TLS {
+ return false
+ }
+
+ if payload.TLSSkipVerify != nil && !*payload.TLSSkipVerify {
+ return true
+ }
+
+ if payload.TLSSkipClientVerify != nil && !*payload.TLSSkipClientVerify {
+ return true
+ }
+ return false
+}
diff --git a/app/portainer/components/index.js b/app/portainer/components/index.js
index fb0f89632..033af9f17 100644
--- a/app/portainer/components/index.js
+++ b/app/portainer/components/index.js
@@ -8,8 +8,9 @@ import { boxSelectorModule } from './BoxSelector';
import { beFeatureIndicator } from './BEFeatureIndicator';
import { InformationPanelAngular } from './InformationPanel';
import { gitFormModule } from './forms/git-form';
+import { tlsFieldsetModule } from './tls-fieldset';
export default angular
- .module('portainer.app.components', [boxSelectorModule, widgetModule, gitFormModule, porAccessManagementModule, formComponentsModule])
+ .module('portainer.app.components', [boxSelectorModule, widgetModule, gitFormModule, porAccessManagementModule, formComponentsModule, tlsFieldsetModule])
.component('informationPanel', InformationPanelAngular)
.component('beFeatureIndicator', beFeatureIndicator).name;
diff --git a/app/portainer/components/tls-fieldset/index.ts b/app/portainer/components/tls-fieldset/index.ts
new file mode 100644
index 000000000..32ab1f820
--- /dev/null
+++ b/app/portainer/components/tls-fieldset/index.ts
@@ -0,0 +1,22 @@
+import angular from 'angular';
+
+import {
+ TLSFieldset,
+ tlsConfigValidation,
+} from '@/react/components/TLSFieldset';
+import { withFormValidation } from '@/react-tools/withFormValidation';
+
+export const ngModule = angular.module(
+ 'portainer.app.components.tls-fieldset',
+ []
+);
+
+export const tlsFieldsetModule = ngModule.name;
+
+withFormValidation(
+ ngModule,
+ TLSFieldset,
+ 'tlsFieldset',
+ [],
+ tlsConfigValidation
+);
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html
index 3e32a3cc9..9d969b2b5 100644
--- a/app/portainer/views/endpoints/edit/endpoint.html
+++ b/app/portainer/views/endpoints/edit/endpoint.html
@@ -72,7 +72,7 @@
@@ -40,33 +46,33 @@ export function TLSFieldset() {
setFieldValue('caCertFile', file)}
+ onChange={(file) => handleChange({ caCertFile: file })}
value={values.caCertFile}
/>
setFieldValue('certFile', file)}
+ onChange={(file) => handleChange({ certFile: file })}
value={values.certFile}
/>
setFieldValue('keyFile', file)}
+ onChange={(file) => handleChange({ keyFile: file })}
value={values.keyFile}
/>
@@ -76,21 +82,29 @@ export function TLSFieldset() {
)}
>
);
+
+ function handleChange(partialValue: Partial) {
+ onChange(partialValue);
+ }
}
const MAX_FILE_SIZE = 5_242_880; // 5MB
-function certValidation() {
+function certValidation(optional?: boolean) {
return withFileSize(file(), MAX_FILE_SIZE).when(['tls', 'skipVerify'], {
- is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify,
+ is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify && !optional,
then: (schema) => schema.required('File is required'),
});
}
-export function validation() {
- return {
- caCertFile: certValidation(),
- certFile: certValidation(),
- keyFile: certValidation(),
- };
+export function tlsConfigValidation({
+ optionalCert,
+}: { optionalCert?: boolean } = {}): SchemaOf {
+ return object({
+ tls: boolean().default(false),
+ skipVerify: boolean().default(false),
+ caCertFile: certValidation(optionalCert),
+ certFile: certValidation(optionalCert),
+ keyFile: certValidation(optionalCert),
+ });
}
diff --git a/app/react/components/TLSFieldset/index.ts b/app/react/components/TLSFieldset/index.ts
new file mode 100644
index 000000000..f74cc852c
--- /dev/null
+++ b/app/react/components/TLSFieldset/index.ts
@@ -0,0 +1 @@
+export { TLSFieldset, tlsConfigValidation } from './TLSFieldset';
diff --git a/app/react/components/TLSFieldset/types.ts b/app/react/components/TLSFieldset/types.ts
new file mode 100644
index 000000000..9929cb959
--- /dev/null
+++ b/app/react/components/TLSFieldset/types.ts
@@ -0,0 +1,7 @@
+export interface TLSConfig {
+ tls: boolean;
+ skipVerify?: boolean;
+ caCertFile?: File;
+ certFile?: File;
+ keyFile?: File;
+}
diff --git a/app/react/portainer/environments/utils/index.ts b/app/react/portainer/environments/utils/index.ts
index ca29a7e73..6f0e63513 100644
--- a/app/react/portainer/environments/utils/index.ts
+++ b/app/react/portainer/environments/utils/index.ts
@@ -67,6 +67,13 @@ export function isLocalEnvironment(environment: Environment) {
);
}
+export function isDockerAPIEnvironment(environment: Environment) {
+ return (
+ environment.URL.startsWith('tcp://') &&
+ environment.Type === EnvironmentType.Docker
+ );
+}
+
export function getDashboardRoute(environment: Environment) {
if (isEdgeEnvironment(environment.Type)) {
if (!environment.EdgeID) {
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
index b2c95b0b1..169b2d8bd 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
@@ -8,6 +8,7 @@ import {
Environment,
EnvironmentCreationTypes,
} from '@/react/portainer/environments/types';
+import { TLSFieldset } from '@/react/components/TLSFieldset/TLSFieldset';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { FormControl } from '@@/form-components/FormControl';
@@ -19,7 +20,6 @@ import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { useValidation } from './APIForm.validation';
import { FormValues } from './types';
-import { TLSFieldset } from './TLSFieldset';
interface Props {
onCreate(environment: Environment): void;
@@ -31,7 +31,10 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) {
const initialValues: FormValues = {
url: '',
name: '',
- tls: false,
+ tlsConfig: {
+ tls: false,
+ skipVerify: false,
+ },
meta: {
groupId: 1,
tagIds: [],
@@ -52,7 +55,7 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) {
validateOnMount
key={formKey}
>
- {({ isValid, dirty }) => (
+ {({ values, errors, setFieldValue, isValid, dirty }) => (