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 @@
-
+
Configuration
@@ -124,6 +124,14 @@
+ + + - -
-
Security
- -
-
Open Active Management Technology
@@ -219,7 +221,7 @@
@@ -30,7 +35,8 @@ export function TLSFieldset() { setFieldValue('skipVerify', checked)} + onChange={(checked) => handleChange({ skipVerify: checked })} + labelClass="col-sm-3 col-lg-2" />
@@ -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 }) => ( @@ -70,7 +73,15 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) { /> - + + Object.entries(value).forEach(([key, value]) => + setFieldValue(`tlsConfig.${key}`, value) + ) + } + errors={errors.tlsConfig} + /> {isDockerStandalone && ( @@ -141,24 +152,24 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) { } ); function getTlsValues() { - if (!values.tls) { + if (!values.tlsConfig.tls) { return undefined; } return { - skipVerify: values.skipVerify, + skipVerify: values.tlsConfig.skipVerify, ...getCertFiles(), }; function getCertFiles() { - if (values.skipVerify) { + if (values.tlsConfig.skipVerify) { return {}; } return { - caCertFile: values.caCertFile, - certFile: values.certFile, - keyFile: values.keyFile, + caCertFile: values.tlsConfig.caCertFile, + certFile: values.tlsConfig.certFile, + keyFile: values.tlsConfig.keyFile, }; } } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx index 89a935735..786129eb3 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx @@ -1,18 +1,17 @@ -import { boolean, object, SchemaOf, string } from 'yup'; +import { object, SchemaOf, string } from 'yup'; + +import { tlsConfigValidation } from '@/react/components/TLSFieldset/TLSFieldset'; import { metadataValidation } from '../../shared/MetadataFieldset/validation'; import { useNameValidation } from '../../shared/NameField'; -import { validation as certsValidation } from './TLSFieldset'; import { FormValues } from './types'; export function useValidation(): SchemaOf { return object({ name: useNameValidation(), url: string().required('This field is required.'), - tls: boolean().default(false), - skipVerify: boolean(), + tlsConfig: tlsConfigValidation(), meta: metadataValidation(), - ...certsValidation(), }); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts index 2d2143ccb..a0c5b45ed 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts @@ -1,12 +1,9 @@ +import { TLSConfig } from '@/react/components/TLSFieldset/types'; import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { name: string; url: string; - tls: boolean; - skipVerify?: boolean; - caCertFile?: File; - certFile?: File; - keyFile?: File; + tlsConfig: TLSConfig; meta: EnvironmentMetadata; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx index b6ed8c920..2ff6bc0e8 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx @@ -126,6 +126,7 @@ function OverrideSocketFieldset() { checked={values.overridePath} onChange={(checked) => setFieldValue('overridePath', checked)} label="Override default socket path" + labelClass="col-sm-3 col-lg-2" />