Compare commits
10 Commits
fix/EE-674
...
revert-774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09eccc55de | ||
|
|
19c2bc12de | ||
|
|
bc6a43d88b | ||
|
|
6d6632d4e2 | ||
|
|
a32b004fb3 | ||
|
|
ba441da519 | ||
|
|
07f8abe2f3 | ||
|
|
071962de2d | ||
|
|
04fd2a2b44 | ||
|
|
70d89e9a24 |
@@ -20,7 +20,7 @@ Portainer CE is updated regularly. We aim to do an update release every couple o
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
|
||||
- [Deploy Portainer](https://docs.portainer.io/start/install)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
|
||||
|
||||
|
||||
@@ -183,10 +183,12 @@
|
||||
<div ng-show="state.BuildType === 'url'">
|
||||
<div class="col-sm-12 form-section-title"> URL </div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small vertical-center">
|
||||
<span class="col-sm-12 small vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
Specify the URL to a Dockerfile, a tarball or a public Git repository (suffixed by <b>.git</b>). When using a Git repository URL, build contexts can be
|
||||
specified as in the <a href="https://docs.docker.com/engine/reference/commandline/build/#git-repositories">Docker documentation.</a>
|
||||
<span class="text-muted"
|
||||
>Specify the URL to a Dockerfile, a tarball or a public Git repository (suffixed by <b>.git</b>). When using a Git repository URL, build contexts can be
|
||||
specified as in the <a href="https://docs.docker.com/engine/reference/commandline/build/#git-repositories">Docker documentation.</a></span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -27,10 +27,7 @@
|
||||
</button>
|
||||
<button
|
||||
ng-if="
|
||||
!(
|
||||
($ctrl.isDockerConfig || $ctrl.formValues.Type.name === $ctrl.KubernetesSecretTypes.TLS.name || $ctrl.formValues.Type === $ctrl.KubernetesSecretTypes.TLS.value) &&
|
||||
$ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET
|
||||
)
|
||||
!(($ctrl.isDockerConfig || $ctrl.formValues.Type === $ctrl.KubernetesSecretTypeOptions.TLS.value) && $ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET)
|
||||
"
|
||||
type="button"
|
||||
class="btn btn-sm btn-default ml-0"
|
||||
@@ -50,10 +47,7 @@
|
||||
<pr-icon icon="'upload'" feather="true" class="vertical-center"></pr-icon> Upload docker config file
|
||||
</button>
|
||||
<button
|
||||
ng-if="
|
||||
($ctrl.formValues.Type.name === $ctrl.KubernetesSecretTypes.TLS.name || $ctrl.formValues.Type === $ctrl.KubernetesSecretTypes.TLS.value) &&
|
||||
$ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET
|
||||
"
|
||||
ng-if="$ctrl.formValues.Type === $ctrl.KubernetesSecretTypeOptions.TLS.value && $ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET"
|
||||
type="button"
|
||||
class="btn btn-sm btn-default ml-0"
|
||||
ngf-select="$ctrl.addEntryFromFile($file)"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Base64 } from 'js-base64';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
||||
|
||||
class KubernetesConfigurationDataController {
|
||||
/* @ngInject */
|
||||
@@ -20,7 +20,7 @@ class KubernetesConfigurationDataController {
|
||||
this.showSimpleMode = this.showSimpleMode.bind(this);
|
||||
this.showAdvancedMode = this.showAdvancedMode.bind(this);
|
||||
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
|
||||
this.KubernetesSecretTypes = KubernetesSecretTypes;
|
||||
this.KubernetesSecretTypeOptions = KubernetesSecretTypeOptions;
|
||||
}
|
||||
|
||||
onChangeKey(entry) {
|
||||
@@ -41,27 +41,26 @@ class KubernetesConfigurationDataController {
|
||||
// logic for setting required keys for new entries, based on the secret type
|
||||
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
|
||||
const newDataIndex = this.formValues.Data.length - 1;
|
||||
const typeValue = typeof this.formValues.Type === 'string' ? this.formValues.Type : this.formValues.Type.value;
|
||||
switch (typeValue) {
|
||||
case this.KubernetesSecretTypes.DOCKERCFG.value:
|
||||
switch (this.formValues.Type) {
|
||||
case this.KubernetesSecretTypeOptions.DOCKERCFG.value:
|
||||
this.addMissingKeys(['dockercfg'], newDataIndex);
|
||||
break;
|
||||
case this.KubernetesSecretTypes.DOCKERCONFIGJSON.value:
|
||||
case this.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value:
|
||||
this.addMissingKeys(['.dockerconfigjson'], newDataIndex);
|
||||
break;
|
||||
case this.KubernetesSecretTypes.BASICAUTH.value:
|
||||
case this.KubernetesSecretTypeOptions.BASICAUTH.value:
|
||||
// only add a required key if there is no required key out of username and password
|
||||
if (!this.formValues.Data.some((entry) => entry.Key === 'username' || entry.Key === 'password')) {
|
||||
this.addMissingKeys(['username', 'password'], newDataIndex);
|
||||
}
|
||||
break;
|
||||
case this.KubernetesSecretTypes.SSHAUTH.value:
|
||||
case this.KubernetesSecretTypeOptions.SSHAUTH.value:
|
||||
this.addMissingKeys(['ssh-privatekey'], newDataIndex);
|
||||
break;
|
||||
case this.KubernetesSecretTypes.TLS.value:
|
||||
case this.KubernetesSecretTypeOptions.TLS.value:
|
||||
this.addMissingKeys(['tls.crt', 'tls.key'], newDataIndex);
|
||||
break;
|
||||
case this.KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
|
||||
case this.KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value:
|
||||
this.addMissingKeys(['token-id', 'token-secret'], newDataIndex);
|
||||
break;
|
||||
default:
|
||||
@@ -84,29 +83,28 @@ class KubernetesConfigurationDataController {
|
||||
|
||||
isRequiredKey(key) {
|
||||
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
|
||||
const secretTypeValue = typeof this.formValues.Type === 'string' ? this.formValues.Type : this.formValues.Type.value;
|
||||
switch (secretTypeValue) {
|
||||
case this.KubernetesSecretTypes.DOCKERCONFIGJSON.value:
|
||||
switch (this.formValues.Type) {
|
||||
case this.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value:
|
||||
if (key === '.dockerconfigjson') {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case this.KubernetesSecretTypes.DOCKERCFG.value:
|
||||
case this.KubernetesSecretTypeOptions.DOCKERCFG.value:
|
||||
if (key === '.dockercfg') {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case this.KubernetesSecretTypes.SSHAUTH.value:
|
||||
case this.KubernetesSecretTypeOptions.SSHAUTH.value:
|
||||
if (key === 'ssh-privatekey') {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case this.KubernetesSecretTypes.TLS.value:
|
||||
case this.KubernetesSecretTypeOptions.TLS.value:
|
||||
if (key === 'tls.crt' || key === 'tls.key') {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case this.KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
|
||||
case this.KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value:
|
||||
if (key === 'token-id' || key === 'token-secret') {
|
||||
return true;
|
||||
}
|
||||
@@ -168,14 +166,14 @@ class KubernetesConfigurationDataController {
|
||||
|
||||
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
|
||||
if (this.isDockerConfig) {
|
||||
if (this.formValues.Type.name === this.KubernetesSecretTypes.DOCKERCFG.name) {
|
||||
if (this.formValues.Type === this.KubernetesSecretTypeOptions.DOCKERCFG.value) {
|
||||
entry.Key = '.dockercfg';
|
||||
} else {
|
||||
entry.Key = '.dockerconfigjson';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.formValues.Type.name === this.KubernetesSecretTypes.TLS.name) {
|
||||
if (this.formValues.Type === this.KubernetesSecretTypeOptions.TLS.value) {
|
||||
const isCrt = entry.Value.indexOf('BEGIN CERTIFICATE') !== -1;
|
||||
if (isCrt) {
|
||||
entry.Key = 'tls.crt';
|
||||
@@ -200,9 +198,8 @@ class KubernetesConfigurationDataController {
|
||||
|
||||
isEntryRequired() {
|
||||
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
|
||||
const typeValue = typeof this.formValues.Type === 'string' ? this.formValues.Type : this.formValues.Type.value;
|
||||
if (this.formValues.Data.length === 1) {
|
||||
if (typeValue !== this.KubernetesSecretTypes.SERVICEACCOUNTTOKEN.value) {
|
||||
if (this.formValues.Type !== this.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ class KubernetesConfigurationConverter {
|
||||
res.ConfigurationOwner = secret.ConfigurationOwner;
|
||||
res.IsRegistrySecret = secret.IsRegistrySecret;
|
||||
res.SecretType = secret.SecretType;
|
||||
if (secret.Annotations) {
|
||||
res.ServiceAccountName = secret.Annotations['kubernetes.io/service-account.name'];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
|
||||
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
||||
class KubernetesSecretConverter {
|
||||
static createPayload(secret) {
|
||||
const res = new KubernetesSecretCreatePayload();
|
||||
res.metadata.name = secret.Name;
|
||||
res.metadata.namespace = secret.Namespace;
|
||||
res.type = secret.Type.value;
|
||||
res.type = secret.Type;
|
||||
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||
|
||||
@@ -53,6 +53,11 @@ class KubernetesSecretConverter {
|
||||
if (annotation !== '') {
|
||||
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
|
||||
}
|
||||
|
||||
_.forEach(secret.Annotations, (entry) => {
|
||||
res.metadata.annotations[entry.name] = entry.value;
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -64,6 +69,7 @@ class KubernetesSecretConverter {
|
||||
res.Type = payload.type;
|
||||
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||
res.CreationDate = payload.metadata.creationTimestamp;
|
||||
res.Annotations = payload.metadata.annotations;
|
||||
|
||||
res.IsRegistrySecret = payload.metadata.annotations && !!payload.metadata.annotations['portainer.io/registry.id'];
|
||||
|
||||
@@ -96,14 +102,11 @@ class KubernetesSecretConverter {
|
||||
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
||||
res.Data = formValues.Data;
|
||||
|
||||
switch (formValues.Type) {
|
||||
case KubernetesSecretTypes.CUSTOM:
|
||||
res.Type.value = formValues.customType;
|
||||
break;
|
||||
|
||||
case KubernetesSecretTypes.SERVICEACCOUNTTOKEN:
|
||||
res.Annotations = [{ name: 'kubernetes.io/service-account.name', value: formValues.ServiceAccountName }];
|
||||
break;
|
||||
if (formValues.Type === KubernetesSecretTypeOptions.CUSTOM.value) {
|
||||
res.Type = formValues.customType;
|
||||
}
|
||||
if (formValues.Type === KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value) {
|
||||
res.Annotations = [{ name: 'kubernetes.io/service-account.name', value: formValues.ServiceAccountName }];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -94,12 +94,7 @@ class KubeCreateCustomTemplateViewController {
|
||||
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
const formValues = { ...this.formValues, Variables: null };
|
||||
if (this.formValues.Variables.length > 0) {
|
||||
formValues.Variables = JSON.stringify(this.formValues.Variables);
|
||||
}
|
||||
|
||||
const customTemplate = await this.createCustomTemplateByMethod(method, formValues);
|
||||
const customTemplate = await this.createCustomTemplateByMethod(method, this.formValues);
|
||||
|
||||
const accessControlData = this.formValues.AccessControlData;
|
||||
const userDetails = this.Authentication.getUserDetails();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from './models';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypeOptions } from './models';
|
||||
|
||||
/**
|
||||
* KubernetesConfigurationFormValues Model
|
||||
@@ -13,7 +13,7 @@ const _KubernetesConfigurationFormValues = Object.freeze({
|
||||
DataYaml: '',
|
||||
IsSimple: true,
|
||||
ServiceAccountName: '',
|
||||
Type: KubernetesSecretTypes.OPAQUE,
|
||||
Type: KubernetesSecretTypeOptions.OPAQUE.value,
|
||||
});
|
||||
|
||||
export class KubernetesConfigurationFormValues {
|
||||
|
||||
@@ -28,7 +28,7 @@ export const KubernetesConfigurationKinds = Object.freeze({
|
||||
SECRET: 2,
|
||||
});
|
||||
|
||||
export const KubernetesSecretTypes = Object.freeze({
|
||||
export const KubernetesSecretTypeOptions = Object.freeze({
|
||||
OPAQUE: { name: 'Opaque', value: 'Opaque' },
|
||||
SERVICEACCOUNTTOKEN: { name: 'Service account token', value: 'kubernetes.io/service-account-token' },
|
||||
DOCKERCFG: { name: 'Dockercfg', value: 'kubernetes.io/dockercfg' },
|
||||
|
||||
@@ -15,6 +15,7 @@ export const componentsModule = angular
|
||||
'onChangeAvailability',
|
||||
'description',
|
||||
'ingressControllers',
|
||||
'isLoading',
|
||||
'noIngressControllerLabel',
|
||||
'view',
|
||||
])
|
||||
|
||||
@@ -132,13 +132,13 @@
|
||||
class="form-control"
|
||||
id="configuration_data_type"
|
||||
ng-model="ctrl.formValues.Type"
|
||||
ng-options="value.name for (name, value) in ctrl.KubernetesSecretTypes"
|
||||
ng-options="value.value as value.name for (name, value) in ctrl.KubernetesSecretTypeOptions"
|
||||
ng-change="ctrl.onSecretTypeChange()"
|
||||
></select>
|
||||
|
||||
<div class="col-sm-3 col-lg-2"></div>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.SERVICEACCOUNTTOKEN" class="col-sm-12 small text-warning vertical-center pt-5">
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value" class="col-sm-12 small text-warning vertical-center pt-5">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
<span
|
||||
>You should only create a service account token Secret object if you can't use the TokenRequest API to obtain a token, and the security exposure of persisting
|
||||
@@ -147,19 +147,19 @@
|
||||
kubernetes documentation.</span
|
||||
>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.DOCKERCFG" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.DOCKERCFG.value" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
<span>Ensure the Secret data field contains a <code>.dockercfg</code> key whose value is content of a legacy <code>~/.dockercfg</code> file.</span>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.DOCKERCONFIGJSON" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
<span>Ensure the Secret data field contains a <code>.dockerconfigjson</code> key whose value is content of a <code>~/.docker/config.json</code> file.</span>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.TLS" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.TLS.value" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
<span>Ensure the Secret data field contains a <code>tls.key</code> key and a <code>tls.crt</code> key.</span>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.BOOTSTRAPTOKEN" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value" class="col-sm-12 small text-muted vertical-center pt-5">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
<span
|
||||
>Ensure the Secret data field contains a <code>token-id</code> key and a <code>token-secret</code> key. See
|
||||
@@ -168,7 +168,7 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.CUSTOM">
|
||||
<div class="form-group" ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.CUSTOM.value">
|
||||
<label for="configuration_data_customtype" class="col-sm-3 col-lg-2 control-label text-left required">Custom Type</label>
|
||||
<div class="col-sm-8 col-lg-9">
|
||||
<input
|
||||
@@ -193,7 +193,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.SERVICEACCOUNTTOKEN">
|
||||
<div class="form-group" ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value">
|
||||
<label for="service_account" class="col-sm-3 col-lg-2 control-label text-left required">Service Account</label>
|
||||
<div class="col-sm-8 col-lg-9">
|
||||
<select
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { getServiceAccounts } from 'Kubernetes/rest/serviceAccount';
|
||||
@@ -21,7 +21,7 @@ class KubernetesCreateConfigurationController {
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
|
||||
this.KubernetesSecretTypes = KubernetesSecretTypes;
|
||||
this.KubernetesSecretTypeOptions = KubernetesSecretTypeOptions;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.createConfigurationAsync = this.createConfigurationAsync.bind(this);
|
||||
@@ -66,41 +66,41 @@ class KubernetesCreateConfigurationController {
|
||||
}
|
||||
|
||||
onSecretTypeChange() {
|
||||
switch (this.formValues.Type.value) {
|
||||
case KubernetesSecretTypes.OPAQUE.value:
|
||||
case KubernetesSecretTypes.CUSTOM.value:
|
||||
switch (this.formValues.Type) {
|
||||
case KubernetesSecretTypeOptions.OPAQUE.value:
|
||||
case KubernetesSecretTypeOptions.CUSTOM.value:
|
||||
this.formValues.Data = this.formValues.Data.filter((entry) => entry.Value !== '');
|
||||
if (this.formValues.Data.length === 0) {
|
||||
this.addRequiredKeysToForm(['']);
|
||||
}
|
||||
this.state.isDockerConfig = false;
|
||||
break;
|
||||
case KubernetesSecretTypes.SERVICEACCOUNTTOKEN.value:
|
||||
case KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value:
|
||||
// data isn't required for service account tokens, so remove the data fields if they are empty
|
||||
this.addRequiredKeysToForm([]);
|
||||
this.state.isDockerConfig = false;
|
||||
break;
|
||||
case KubernetesSecretTypes.DOCKERCONFIGJSON.value:
|
||||
case KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value:
|
||||
this.addRequiredKeysToForm(['.dockerconfigjson']);
|
||||
this.state.isDockerConfig = true;
|
||||
break;
|
||||
case KubernetesSecretTypes.DOCKERCFG.value:
|
||||
case KubernetesSecretTypeOptions.DOCKERCFG.value:
|
||||
this.addRequiredKeysToForm(['.dockercfg']);
|
||||
this.state.isDockerConfig = true;
|
||||
break;
|
||||
case KubernetesSecretTypes.BASICAUTH.value:
|
||||
case KubernetesSecretTypeOptions.BASICAUTH.value:
|
||||
this.addRequiredKeysToForm(['username', 'password']);
|
||||
this.state.isDockerConfig = false;
|
||||
break;
|
||||
case KubernetesSecretTypes.SSHAUTH.value:
|
||||
case KubernetesSecretTypeOptions.SSHAUTH.value:
|
||||
this.addRequiredKeysToForm(['ssh-privatekey']);
|
||||
this.state.isDockerConfig = false;
|
||||
break;
|
||||
case KubernetesSecretTypes.TLS.value:
|
||||
case KubernetesSecretTypeOptions.TLS.value:
|
||||
this.addRequiredKeysToForm(['tls.crt', 'tls.key']);
|
||||
this.state.isDockerConfig = false;
|
||||
break;
|
||||
case KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
|
||||
case KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value:
|
||||
this.addRequiredKeysToForm(['token-id', 'token-secret']);
|
||||
this.state.isDockerConfig = false;
|
||||
break;
|
||||
|
||||
@@ -2,7 +2,7 @@ import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationKinds, KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
@@ -39,7 +39,7 @@ class KubernetesConfigurationController {
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
|
||||
this.KubernetesSecretTypes = KubernetesSecretTypes;
|
||||
this.KubernetesSecretTypeOptions = KubernetesSecretTypeOptions;
|
||||
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
||||
this.KubernetesSecretService = KubernetesSecretService;
|
||||
|
||||
@@ -147,6 +147,7 @@ class KubernetesConfigurationController {
|
||||
if (secret.status === 'fulfilled') {
|
||||
this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
|
||||
this.formValues.Data = secret.value.Data;
|
||||
// this.formValues.ServiceAccountName = secret.value.ServiceAccountName;
|
||||
} else {
|
||||
this.configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value);
|
||||
this.formValues.Data = configMap.value.Data;
|
||||
@@ -276,19 +277,23 @@ class KubernetesConfigurationController {
|
||||
// after loading the configuration, check if it is a docker config secret type
|
||||
if (
|
||||
this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET &&
|
||||
(this.formValues.Type === this.KubernetesSecretTypes.DOCKERCONFIGJSON.value || this.formValues.Type === this.KubernetesSecretTypes.DOCKERCFG.value)
|
||||
(this.formValues.Type === this.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value || this.formValues.Type === this.KubernetesSecretTypeOptions.DOCKERCFG.value)
|
||||
) {
|
||||
this.state.isDockerConfig = true;
|
||||
}
|
||||
// convert the secret type to a human readable value
|
||||
if (this.formValues.Type) {
|
||||
const secretTypeValues = Object.values(this.KubernetesSecretTypes);
|
||||
const secretTypeValues = Object.values(this.KubernetesSecretTypeOptions);
|
||||
const secretType = secretTypeValues.find((secretType) => secretType.value === this.formValues.Type);
|
||||
this.secretTypeName = secretType ? secretType.name : this.formValues.Type;
|
||||
} else {
|
||||
this.secretTypeName = '';
|
||||
}
|
||||
|
||||
if (this.formValues.Type === this.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value) {
|
||||
this.formValues.ServiceAccountName = configuration.ServiceAccountName;
|
||||
}
|
||||
|
||||
this.tagUsedDataKeys();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KubernetesSecretTypes } from '@/kubernetes/models/configuration/models';
|
||||
import { KubernetesSecretTypeOptions } from '@/kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationKinds } from '@/kubernetes/models/configuration/models';
|
||||
|
||||
export function isConfigurationFormValid(alreadyExist, isDataValid, formValues) {
|
||||
@@ -9,36 +9,35 @@ export function isConfigurationFormValid(alreadyExist, isDataValid, formValues)
|
||||
if (formValues.IsSimple) {
|
||||
if (formValues.Kind === KubernetesConfigurationKinds.SECRET) {
|
||||
let isSecretDataValid = true;
|
||||
const secretTypeValue = typeof formValues.Type === 'string' ? formValues.Type : formValues.Type.value;
|
||||
|
||||
switch (secretTypeValue) {
|
||||
case KubernetesSecretTypes.SERVICEACCOUNTTOKEN.value:
|
||||
switch (formValues.Type) {
|
||||
case KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value:
|
||||
// data isn't required for service account tokens
|
||||
isFormValid = uniqueCheck && formValues.ResourcePool;
|
||||
return [isFormValid, ''];
|
||||
case KubernetesSecretTypes.DOCKERCFG.value:
|
||||
case KubernetesSecretTypeOptions.DOCKERCFG.value:
|
||||
// needs to contain a .dockercfg key
|
||||
isSecretDataValid = formValues.Data.some((entry) => entry.Key === '.dockercfg');
|
||||
secretWarningMessage = isSecretDataValid ? '' : 'A data entry with a .dockercfg key is required.';
|
||||
break;
|
||||
case KubernetesSecretTypes.DOCKERCONFIGJSON.value:
|
||||
case KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value:
|
||||
// needs to contain a .dockerconfigjson key
|
||||
isSecretDataValid = formValues.Data.some((entry) => entry.Key === '.dockerconfigjson');
|
||||
secretWarningMessage = isSecretDataValid ? '' : 'A data entry with a .dockerconfigjson key. is required.';
|
||||
break;
|
||||
case KubernetesSecretTypes.BASICAUTH.value:
|
||||
case KubernetesSecretTypeOptions.BASICAUTH.value:
|
||||
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'username' || entry.Key === 'password');
|
||||
secretWarningMessage = isSecretDataValid ? '' : 'A data entry with a username or password key is required.';
|
||||
break;
|
||||
case KubernetesSecretTypes.SSHAUTH.value:
|
||||
case KubernetesSecretTypeOptions.SSHAUTH.value:
|
||||
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'ssh-privatekey');
|
||||
secretWarningMessage = isSecretDataValid ? '' : `A data entry with a 'ssh-privatekey' key is required.`;
|
||||
break;
|
||||
case KubernetesSecretTypes.TLS.value:
|
||||
case KubernetesSecretTypeOptions.TLS.value:
|
||||
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'tls.crt') && formValues.Data.some((entry) => entry.Key === 'tls.key');
|
||||
secretWarningMessage = isSecretDataValid ? '' : `Data entries containing a 'tls.crt' key and a 'tls.key' key are required.`;
|
||||
break;
|
||||
case KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
|
||||
case KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value:
|
||||
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'token-id') && formValues.Data.some((entry) => entry.Key === 'token-secret');
|
||||
secretWarningMessage = isSecretDataValid ? '' : `Data entries containing a 'token-id' key and a 'token-secret' key are required.`;
|
||||
break;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<ingress-class-datatable
|
||||
on-change-availability="(ctrl.onChangeAvailability)"
|
||||
ingress-controllers="ctrl.originalIngressControllers"
|
||||
is-loading="ctrl.isIngressControllersLoading"
|
||||
description="'Enabling ingress controllers in your cluster allows them to be available in the Portainer UI for users to publish applications over HTTP/HTTPS. A controller must have a class name for it to be included here.'"
|
||||
no-ingress-controller-label="'No supported ingress controllers found.'"
|
||||
view="'cluster'"
|
||||
@@ -202,7 +203,7 @@
|
||||
</tr>
|
||||
<tr ng-repeat="class in ctrl.StorageClasses">
|
||||
<td>
|
||||
<div class="flex-row vertical-center">
|
||||
<div class="flex flex-row items-center h-full">
|
||||
<label class="switch mr-2 mb-0">
|
||||
<input type="checkbox" ng-model="class.selected" /><span class="slider round" data-cy="kubeSetup-storageToggle{{ class.Name }}"></span>
|
||||
</label>
|
||||
@@ -218,7 +219,7 @@
|
||||
></storage-access-mode-selector>
|
||||
</td>
|
||||
<td>
|
||||
<div style="margin: 5px">
|
||||
<div class="flex flex-row items-center h-full">
|
||||
<label class="switch mr-2 mb-0"
|
||||
><input type="checkbox" ng-model="class.AllowVolumeExpansion" /><span
|
||||
class="slider round"
|
||||
|
||||
@@ -198,7 +198,7 @@ class KubernetesConfigureController {
|
||||
this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses);
|
||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||
// updateIngressControllerClassMap must be done after updateEndpoint, as a hacky workaround. A better solution: saving ingresscontrollers somewhere else, is being discussed
|
||||
await updateIngressControllerClassMap(this.state.endpointId, this.ingressControllers);
|
||||
await updateIngressControllerClassMap(this.state.endpointId, this.ingressControllers || []);
|
||||
this.state.isSaving = true;
|
||||
const storagePromises = _.map(storageClasses, (storageClass) => {
|
||||
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
||||
@@ -224,19 +224,7 @@ class KubernetesConfigureController {
|
||||
}
|
||||
|
||||
configure() {
|
||||
const toDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
||||
if (toDel.length) {
|
||||
this.ModalService.confirmUpdate(
|
||||
`Removing ingress controllers may cause applications to be unaccessible. All ingress configurations from affected applications will be removed.<br/><br/>Do you wish to continue?`,
|
||||
(confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -292,6 +280,7 @@ class KubernetesConfigureController {
|
||||
IngressAvailabilityPerNamespace: false,
|
||||
};
|
||||
|
||||
this.isIngressControllersLoading = true;
|
||||
try {
|
||||
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||
|
||||
@@ -307,6 +296,9 @@ class KubernetesConfigureController {
|
||||
if (storage) {
|
||||
item.selected = true;
|
||||
item.AccessModes = storage.AccessModes.map((name) => this.availableAccessModes.find((accessMode) => accessMode.Name === name));
|
||||
} else if (this.availableAccessModes.length) {
|
||||
// set a default access mode if the storage class is not enabled and there are available access modes
|
||||
item.AccessModes = [this.availableAccessModes[0]];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -329,6 +321,7 @@ class KubernetesConfigureController {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve environment configuration');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
this.isIngressControllersLoading = false;
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeOnload);
|
||||
|
||||
@@ -36,7 +36,7 @@ class KubernetesDeployController {
|
||||
{ ...git, value: KubernetesDeployBuildMethods.GIT },
|
||||
{ ...editor, value: KubernetesDeployBuildMethods.WEB_EDITOR },
|
||||
{ ...url, value: KubernetesDeployBuildMethods.URL },
|
||||
{ ...template, value: KubernetesDeployBuildMethods.CUSTOM_TEMPLATE },
|
||||
{ ...template, description: 'Use custom template', value: KubernetesDeployBuildMethods.CUSTOM_TEMPLATE },
|
||||
];
|
||||
|
||||
this.state = {
|
||||
|
||||
@@ -109,7 +109,7 @@ class KubernetesCreateResourcePoolController {
|
||||
this.checkDefaults();
|
||||
this.formValues.Owner = this.Authentication.getUserDetails().username;
|
||||
await this.KubernetesResourcePoolService.create(this.formValues);
|
||||
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers, this.formValues.Name);
|
||||
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers || [], this.formValues.Name);
|
||||
this.Notifications.success('Namespace successfully created', this.formValues.Name);
|
||||
this.$state.go('kubernetes.resourcePools');
|
||||
} catch (err) {
|
||||
|
||||
@@ -144,7 +144,7 @@ class KubernetesResourcePoolController {
|
||||
try {
|
||||
this.checkDefaults();
|
||||
await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues);
|
||||
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers, this.formValues.Name);
|
||||
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers || [], this.formValues.Name);
|
||||
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function (formValues) {
|
||||
if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
|
||||
return [{ action, kind: KubernetesResourceTypes.CONFIGMAP, name: formValues.Name }];
|
||||
} else if (formValues.Kind === KubernetesConfigurationKinds.SECRET) {
|
||||
let type = typeof formValues.Type === 'string' ? formValues.Type : formValues.Type.name;
|
||||
let type = formValues.Type;
|
||||
if (formValues.customType) {
|
||||
type = formValues.customType;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<ng-form name="pathForm">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon> Indicate the path to the {{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} file from the root
|
||||
of your repository. To enable rebuilding of an image if already present on Docker standalone environments, include<code>pull_policy: build</code>in your compose file as per<a
|
||||
href="https://docs.docker.com/compose/compose-file/#pull_policy"
|
||||
>Docker documentation</a
|
||||
>.
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
<span
|
||||
>Indicate the path to the {{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} file from the root of your repository.
|
||||
<span ng-if="$ctrl.isDockerStandalone">
|
||||
To enable rebuilding of an image if already present on Docker standalone environments, include<code>pull_policy: build</code>in your compose file as per
|
||||
<a href="https://docs.docker.com/compose/compose-file/#pull_policy">Docker documentation</a>.</span
|
||||
></span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -4,5 +4,6 @@ export const gitFormComposePathField = {
|
||||
deployMethod: '@',
|
||||
value: '<',
|
||||
onChange: '<',
|
||||
isDockerStandalone: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export default class GitFormController {
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
constructor(StateManager) {
|
||||
this.onChangeField = this.onChangeField.bind(this);
|
||||
this.onChangeURL = this.onChangeField('RepositoryURL');
|
||||
this.onChangeRefName = this.onChangeField('RepositoryReferenceName');
|
||||
this.onChangeComposePath = this.onChangeField('ComposeFilePathInRepository');
|
||||
this.isDockerStandalone = StateManager.getState().endpoint.mode.provider === 'DOCKER_STANDALONE';
|
||||
}
|
||||
|
||||
onChangeField(field) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
value="$ctrl.model.ComposeFilePathInRepository"
|
||||
on-change="($ctrl.onChangeComposePath)"
|
||||
deploy-method="{{ $ctrl.deployMethod }}"
|
||||
is-docker-standalone="$ctrl.isDockerStandalone"
|
||||
></git-form-compose-path-field>
|
||||
|
||||
<git-form-additional-files-panel ng-if="$ctrl.additionalFile" model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-additional-files-panel>
|
||||
|
||||
@@ -38,7 +38,6 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
||||
}
|
||||
|
||||
function onChangeEnvironment(endpointId) {
|
||||
console.log({ endpointId });
|
||||
return $scope.$evalAsync(() => {
|
||||
ctrl.formValues.endpointId = endpointId;
|
||||
});
|
||||
|
||||
@@ -126,8 +126,10 @@
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted vertical-center">
|
||||
<pr-icon class="vertical-center" icon="'alert-circle'" feather="true" aria-hidden="true" mode="'primary'" size="'md'"></pr-icon>
|
||||
Note: non-administrator users who aren't in a team don't have access to any environments by default. Head over to the
|
||||
<a ui-sref="portainer.endpoints">Environments view</a> to manage their accesses.
|
||||
<span
|
||||
>Note: non-administrator users who aren't in a team don't have access to any environments by default. Head over to the
|
||||
<a ui-sref="portainer.endpoints">Environments view</a> to manage their accesses.</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,13 +27,15 @@ export function TextTip({
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-muted small vertical-center">
|
||||
<Icon
|
||||
icon="alert-circle"
|
||||
feather
|
||||
className={clsx(`${iconClass}`, 'space-right')}
|
||||
/>
|
||||
{children}
|
||||
<p className="small vertical-center">
|
||||
<i className="icon-container">
|
||||
<Icon
|
||||
icon="alert-circle"
|
||||
feather
|
||||
className={clsx(`${iconClass}`, 'space-right')}
|
||||
/>
|
||||
</i>
|
||||
<span className="text-muted">{children}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function StorageAccessModeSelector({
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(value) => onChange(storageClassName, value)}
|
||||
inputId={inputId}
|
||||
placeholder="Select one or more teams"
|
||||
placeholder="Not configured"
|
||||
data-cy={`kubeSetup-storageAccessSelect${storageClassName}`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ interface Props {
|
||||
) => void; // angular function to save the ingress class list
|
||||
description: string;
|
||||
ingressControllers: IngressControllerClassMap[] | undefined;
|
||||
isLoading: boolean;
|
||||
noIngressControllerLabel: string;
|
||||
view: string;
|
||||
}
|
||||
@@ -27,6 +28,7 @@ export function IngressClassDatatable({
|
||||
onChangeAvailability,
|
||||
description,
|
||||
ingressControllers,
|
||||
isLoading,
|
||||
noIngressControllerLabel,
|
||||
view,
|
||||
}: Props) {
|
||||
@@ -42,7 +44,7 @@ export function IngressClassDatatable({
|
||||
storageKey="ingressClasses"
|
||||
columns={columns}
|
||||
settingsStore={settings}
|
||||
isLoading={!ingControllerFormValues}
|
||||
isLoading={isLoading}
|
||||
emptyContentLabel={noIngressControllerLabel}
|
||||
titleOptions={{
|
||||
icon: 'database',
|
||||
|
||||
Reference in New Issue
Block a user