Compare commits

...

7 Commits

Author SHA1 Message Date
Matt Hook
d3d4361850 bump version to 2.16.1 (#8018)
Some checks failed
Test / test-client (push) Has been cancelled
2022-11-09 14:28:40 +13:00
andres-portainer
1b237151a9 fix(snapshots): remove snapshots when removing endpoints EE-4527 (#7971)
* fix(snapshots): remove snapshots when removing endpoints EE-4527

* Fix nil pointer dereference.

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-11-07 20:28:06 -03:00
Chaim Lev-Ari
0ddf31f3e2 fix(stack): validate original containers names [EE-4520] (#7977) 2022-11-06 10:40:30 +02:00
LP B
abd513801f fix(app/logs): change pattern to detect double serialized JSON logs [EE-4525] (#7961)
* fix(app/logs): change pattern to detect double serialized JSON logs

* fix(app/logs): fallback to raw display when parsing fails + include timestamp for Zerolog logs
2022-11-04 13:58:05 +01:00
Ali
354eb8c3c0 fix(slider): use and update react slider EE-4522 (#7988) 2022-11-04 14:13:02 +13:00
congs
539e7fe422 fix(registry): EE-4526 Registry Manage access link broken (#7976) 2022-11-04 12:10:32 +13:00
Ali
11e42f54ba fix(app): fix external app edit EE-4529 (#7964) 2022-11-03 13:04:53 +13:00
25 changed files with 289 additions and 87 deletions

View File

@@ -111,6 +111,9 @@ func (m *Migrator) Migrate() error {
// Portainer 2.16
newMigration(70, m.migrateDBVersionToDB70),
// Portainer 2.16.1
newMigration(71, m.migrateDBVersionToDB71),
}
var lastDbVersion int

View File

@@ -0,0 +1,36 @@
package migrator
import (
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
func (m *Migrator) migrateDBVersionToDB71() error {
log.Info().Msg("removing orphaned snapshots")
snapshots, err := m.snapshotService.Snapshots()
if err != nil {
return err
}
for _, s := range snapshots {
_, err := m.endpointService.Endpoint(s.EndpointID)
if err == nil {
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("keeping snapshot")
continue
} else if err != errors.ErrObjectNotFound {
log.Debug().Int("endpoint_id", int(s.EndpointID)).Err(err).Msg("database error")
return err
}
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("removing snapshot")
err = m.snapshotService.DeleteSnapshot(s.EndpointID)
if err != nil {
return err
}
}
return nil
}

View File

@@ -931,7 +931,7 @@
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "70",
"DB_VERSION": "71",
"INSTANCE_ID": "null"
}
}

View File

@@ -49,6 +49,11 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
}
}
err = handler.DataStore.Snapshot().DeleteSnapshot(portainer.EndpointID(endpointID))
if err != nil {
return httperror.InternalServerError("Unable to remove the snapshot from the database", err)
}
err = handler.DataStore.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
return httperror.InternalServerError("Unable to remove environment from the database", err)

View File

@@ -84,7 +84,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.16.0
// @version 2.16.1
// @description.markdown api-description.md
// @termsOfService

View File

@@ -1451,9 +1451,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.16.0"
APIVersion = "2.16.1"
// DBVersion is the version number of the Portainer database
DBVersion = 70
DBVersion = 71
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server

View File

@@ -31,7 +31,7 @@ export function formatJSONLine(
if (withTimestamps) {
const timestamp = rawText.substring(0, TIMESTAMP_LENGTH);
spans.push({ text: timestamp });
line += `${timestamp}`;
line += `${timestamp} `;
}
line += formatTime(time, spans, line);
line += formatLevel(level, spans, line);

View File

@@ -33,8 +33,14 @@ export function formatLogs(
if (stripHeaders) {
logs = stripHeadersFunc(logs);
}
if (logs.includes('\\n')) {
logs = JSON.parse(logs);
// if JSON logs come serialized 2 times, parse them once to unwrap them
// for example when retrieving Edge Agent logs on Nomad
if (logs.startsWith('"')) {
try {
logs = JSON.parse(logs);
} catch (error) {
// noop, throw error away if logs cannot be parsed
}
}
const tokens: Token[][] = tokenize(logs);
@@ -83,16 +89,26 @@ export function formatLogs(
}
const text = stripEscapeCodes(tokenLine);
if (
(!withTimestamps && text.startsWith('{')) ||
(withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))
) {
const lines = formatJSONLine(text, withTimestamps);
formattedLogs.push(...lines);
} else if (ZerologRegex.test(text)) {
const lines = formatZerologLogs(text, withTimestamps);
formattedLogs.push(...lines);
} else {
try {
if (
(!withTimestamps && text.startsWith('{')) ||
(withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))
) {
const lines = formatJSONLine(text, withTimestamps);
formattedLogs.push(...lines);
} else if (
(!withTimestamps && ZerologRegex.test(text)) ||
(withTimestamps &&
ZerologRegex.test(text.substring(TIMESTAMP_LENGTH)))
) {
const lines = formatZerologLogs(text, withTimestamps);
formattedLogs.push(...lines);
} else {
spans.push({ fgColor, bgColor, text, fontWeight });
line += text;
}
} catch (error) {
// in case parsing fails for whatever reason, push the raw logs and continue
spans.push({ fgColor, bgColor, text, fontWeight });
line += text;
}

View File

@@ -55,6 +55,12 @@ export function formatZerologLogs(rawText: string, withTimestamps?: boolean) {
const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText;
if (withTimestamps) {
const timestamp = rawText.substring(0, TIMESTAMP_LENGTH);
spans.push({ text: timestamp });
line += `${timestamp} `;
}
const [, date, level, caller, messageAndPairs] =
text.match(ZerologRegex) || [];

View File

@@ -45,8 +45,6 @@ class DockerRegistryAccessController {
$onInit() {
return this.$async(async () => {
this.Authentication.redirectIfUnauthorized(['PortainerRegistryUpdateAccess']);
this.registryTo = window.location.hash.match(/#!\/\d+\/docker\/swarm\/registries/) ? 'docker.swarm.registries' : 'docker.host.registries';
try {

View File

@@ -1339,7 +1339,12 @@
</div>
</div>
<!-- kubernetes services options -->
<kube-services-view form-values="ctrl.formValues" is-edit="ctrl.state.isEdit" loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"></kube-services-view>
<kube-services-view
namespaces="ctrl.allNamespaces"
form-values="ctrl.formValues"
is-edit="ctrl.state.isEdit"
loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
></kube-services-view>
<!-- kubernetes services options -->
</div>

View File

@@ -93,17 +93,18 @@
<div class="form-group flex flex-row !mb-0">
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left"> Memory limit (MB) </label>
<div class="col-xs-6">
<slider
model="$ctrl.formValues.MemoryLimit"
floor="$ctrl.defaults.MemoryLimit"
ceil="$ctrl.state.sliderMaxMemory"
<por-slider
min="$ctrl.defaults.MemoryLimit"
max="$ctrl.state.sliderMaxMemory"
step="128"
ng-if="$ctrl.state.sliderMaxMemory"
value="$ctrl.formValues.MemoryLimit"
on-change="($ctrl.handleMemoryLimitChange)"
visible-tooltip="true"
data-cy="k8sNamespaceCreate-memoryLimitSlider"
>
</slider>
></por-slider>
</div>
<div class="col-sm-2 vertical-center">
<div class="col-sm-2 vertical-center pt-6">
<input
name="memory_limit"
type="number"
@@ -138,16 +139,16 @@
<div class="form-group flex flex-row">
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left"> CPU limit </label>
<div class="col-xs-8">
<slider
model="$ctrl.formValues.CpuLimit"
floor="$ctrl.defaults.CpuLimit"
ceil="$ctrl.state.sliderMaxCpu"
<por-slider
min="$ctrl.defaults.CpuLimit"
max="$ctrl.state.sliderMaxCpu"
step="0.1"
precision="2"
ng-if="$ctrl.state.sliderMaxCpu"
value="$ctrl.formValues.CpuLimit"
on-change="($ctrl.handleCpuLimitChange)"
data-cy="k8sNamespaceCreate-cpuLimitSlider"
>
</slider>
visible-tooltip="true"
></por-slider>
</div>
</div>
<!-- !cpu-limit-input -->

View File

@@ -33,6 +33,8 @@ class KubernetesCreateResourcePoolController {
this.onToggleResourceQuota = this.onToggleResourceQuota.bind(this);
this.onChangeIngressControllerAvailability = this.onChangeIngressControllerAvailability.bind(this);
this.onRegistriesChange = this.onRegistriesChange.bind(this);
this.handleMemoryLimitChange = this.handleMemoryLimitChange.bind(this);
this.handleCpuLimitChange = this.handleCpuLimitChange.bind(this);
}
/* #endregion */
@@ -101,6 +103,18 @@ class KubernetesCreateResourcePoolController {
}
}
handleMemoryLimitChange(memoryLimit) {
return this.$async(async () => {
this.formValues.MemoryLimit = memoryLimit;
});
}
handleCpuLimitChange(cpuLimit) {
return this.$async(async () => {
this.formValues.CpuLimit = cpuLimit;
});
}
/* #region CREATE NAMESPACE */
createResourcePool() {
return this.$async(async () => {

View File

@@ -80,15 +80,18 @@
<div class="form-group flex">
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left vertical-center"> Memory limit (MB) </label>
<div class="col-sm-6">
<slider
model="ctrl.formValues.MemoryLimit"
floor="ctrl.ResourceQuotaDefaults.MemoryLimit"
ceil="ctrl.state.sliderMaxMemory"
<por-slider
min="ctrl.ResourceQuotaDefaults.MemoryLimit"
max="ctrl.state.sliderMaxMemory"
step="128"
ng-if="ctrl.state.sliderMaxMemory"
></slider>
value="ctrl.formValues.MemoryLimit"
on-change="(ctrl.handleMemoryLimitChange)"
visible-tooltip="true"
data-cy="k8sNamespaceEdit-memoryLimitSlider"
></por-slider>
</div>
<div class="col-sm-2 vertical-center">
<div class="col-sm-2 vertical-center pt-6">
<input
name="memory_limit"
type="number"
@@ -97,6 +100,7 @@
class="form-control"
ng-model="ctrl.formValues.MemoryLimit"
id="memory-limit"
data-cy="k8sNamespaceEdit-memoryLimitInput"
required
/>
</div>
@@ -117,14 +121,16 @@
<div class="form-group">
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px"> CPU limit </label>
<div class="col-sm-8">
<slider
model="ctrl.formValues.CpuLimit"
floor="ctrl.ResourceQuotaDefaults.CpuLimit"
ceil="ctrl.state.sliderMaxCpu"
<por-slider
min="ctrl.ResourceQuotaDefaults.CpuLimit"
max="ctrl.state.sliderMaxCpu"
step="0.1"
precision="2"
ng-if="ctrl.state.sliderMaxCpu"
></slider>
value="ctrl.formValues.CpuLimit"
on-change="(ctrl.handleCpuLimitChange)"
data-cy="k8sNamespaceEdit-cpuLimitSlider"
visible-tooltip="true"
></por-slider>
</div>
</div>
<!-- !cpu-limit-input -->

View File

@@ -69,6 +69,8 @@ class KubernetesResourcePoolController {
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
this.onChangeIngressControllerAvailability = this.onChangeIngressControllerAvailability.bind(this);
this.onRegistriesChange = this.onRegistriesChange.bind(this);
this.handleMemoryLimitChange = this.handleMemoryLimitChange.bind(this);
this.handleCpuLimitChange = this.handleCpuLimitChange.bind(this);
}
/* #endregion */
@@ -122,6 +124,18 @@ class KubernetesResourcePoolController {
}
}
handleMemoryLimitChange(memoryLimit) {
return this.$async(async () => {
this.formValues.MemoryLimit = memoryLimit;
});
}
handleCpuLimitChange(cpuLimit) {
return this.$async(async () => {
this.formValues.CpuLimit = cpuLimit;
});
}
showEditor() {
this.state.showEditorTab = true;
this.selectTab(2);

View File

@@ -23,28 +23,45 @@ angular.module('portainer.app').factory('StackHelper', [
);
}
helper.validateYAML = function (yaml, containerNames) {
let yamlObject;
try {
yamlObject = YAML.parse(yaml);
} catch (err) {
return 'There is an error in the yaml syntax: ' + err;
}
const names = _.uniq(GenericHelper.findDeepAll(yamlObject, 'container_name'));
const duplicateContainers = _.intersection(containerNames, names);
if (duplicateContainers.length === 0) return;
return (
(duplicateContainers.length === 1 ? 'This container name is' : 'These container names are') +
' already used by another container running in this environment: ' +
_.join(duplicateContainers, ', ') +
'.'
);
};
helper.validateYAML = validateYAML;
return helper;
},
]);
function validateYAML(yaml, containerNames, originalContainersNames = []) {
let yamlObject;
try {
yamlObject = YAML.parse(yaml);
} catch (err) {
return 'There is an error in the yaml syntax: ' + err;
}
const names = _.uniq(GenericHelper.findDeepAll(yamlObject, 'container_name'));
const duplicateContainers = _.intersection(_.difference(containerNames, originalContainersNames), names);
if (duplicateContainers.length === 0) {
return '';
}
return (
(duplicateContainers.length === 1 ? 'This container name is' : 'These container names are') +
' already used by another container running in this environment: ' +
_.join(duplicateContainers, ', ') +
'.'
);
}
export function extractContainerNames(yaml = '') {
let yamlObject;
try {
yamlObject = YAML.parse(yaml);
} catch (err) {
return [];
}
return _.uniq(GenericHelper.findDeepAll(yamlObject, 'container_name'));
}

View File

@@ -33,6 +33,7 @@ import { FallbackImage } from '@@/FallbackImage';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
import { TeamsSelector } from '@@/TeamsSelector';
import { PortainerSelect } from '@@/form-components/PortainerSelect';
import { Slider } from '@@/form-components/Slider';
import { fileUploadField } from './file-upload-field';
import { switchField } from './switch-field';
@@ -185,6 +186,18 @@ export const componentsModule = angular
'isClearable',
])
)
.component(
'porSlider',
r2a(Slider, [
'min',
'max',
'step',
'value',
'onChange',
'visibleTooltip',
'dataCy',
])
)
.component(
'porAccessManagementUsersSelector',
r2a(PorAccessManagementUsersSelector, ['onChange', 'options', 'value'])

View File

@@ -141,6 +141,9 @@
<span class="col-sm-12 text-muted small">
You can get more information about Compose file format in the <a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</span>
<div class="col-sm-12" ng-if="state.yamlError">
<span class="text-danger small">{{ state.yamlError }}</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">

View File

@@ -3,6 +3,7 @@ import { AccessControlFormData } from 'Portainer/components/accessControlForm/po
import { FeatureId } from 'Portainer/feature-flags/enums';
import { getEnvironments } from '@/portainer/environments/environment.service';
import { StackStatus, StackType } from '@/react/docker/stacks/types';
import { extractContainerNames } from '@/portainer/helpers/stackHelper';
angular.module('portainer.app').controller('StackController', [
'$async',
@@ -281,7 +282,7 @@ angular.module('portainer.app').controller('StackController', [
if ($scope.stackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
$scope.state.isEditorDirty = true;
$scope.stackFileContent = cm.getValue();
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames, $scope.state.originalContainerNames);
}
};
@@ -371,8 +372,9 @@ angular.module('portainer.app').controller('StackController', [
if (isSwarm && $scope.stack.Status === StackStatus.Active) {
assignSwarmStackResources(data.resources, agentProxy);
}
$scope.state.originalContainerNames = extractContainerNames($scope.stackFileContent);
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames, $scope.state.originalContainerNames);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');

View File

@@ -7,26 +7,50 @@
}
.slider :global .rc-slider-handle {
width: 32px;
height: 32px;
margin-top: -14px;
@apply border-blue-8 border-2;
width: 24px;
height: 24px;
margin-top: -8px;
border-radius: 16px;
cursor: pointer;
background-color: #0db9f0;
background-color: #ffffff;
}
.slider :global .rc-slider-track {
@apply bg-blue-8;
}
.slider :global .rc-slider-handle:after {
position: absolute;
top: 10px;
left: 10px;
width: 8px;
height: 8px;
top: 8px;
left: 8px;
width: 9px;
height: 9px;
background: #ffffff;
border-radius: 4px;
border-radius: 5px;
content: '';
}
.slider :global .rc-slider-mark-text,
.slider :global .rc-slider-tooltip-inner {
font-family: Inter, serif;
.slider :global .rc-slider-mark-text {
font-size: 14px;
color: var(--text-body-color);
}
.slider :global .rc-slider-tooltip-arrow {
bottom: 2px;
border-top-color: var(--bg-tooltip-color);
}
.slider :global .rc-slider-tooltip-placement-top {
padding: 6px 0px;
}
.slider :global .rc-slider-tooltip-inner {
font-size: 14px;
color: var(--text-tooltip-color);
height: fit-content;
background-color: var(--bg-tooltip-color);
box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%);
padding: 8px 12px;
text-align: center;
}

View File

@@ -8,7 +8,14 @@ export default {
title: 'Components/Form/Slider',
} as Meta;
function Template({ value, min, max, step }: JSX.IntrinsicAttributes & Props) {
function Template({
value,
min,
max,
step,
dataCy,
visibleTooltip,
}: JSX.IntrinsicAttributes & Props) {
const [sliderValue, setSliderValue] = useState(min);
useEffect(() => {
@@ -22,6 +29,8 @@ function Template({ value, min, max, step }: JSX.IntrinsicAttributes & Props) {
step={step}
value={sliderValue}
onChange={setSliderValue}
dataCy={dataCy}
visibleTooltip={visibleTooltip}
/>
);
}
@@ -32,4 +41,6 @@ Primary.args = {
max: 100,
step: 1,
value: 5,
visibleTooltip: true,
dataCy: 'someView-coolSlider',
};

View File

@@ -8,9 +8,19 @@ function renderDefault({
step = 1,
value = min,
onChange = () => {},
dataCy = 'someView-coolSlider',
visibleTooltip = true,
}: Partial<Props> = {}) {
return render(
<Slider min={min} max={max} step={step} onChange={onChange} value={value} />
<Slider
min={min}
max={max}
step={step}
onChange={onChange}
value={value}
visibleTooltip={visibleTooltip}
dataCy={dataCy}
/>
);
}

View File

@@ -9,13 +9,25 @@ export interface Props {
step: number;
value: number;
onChange: (value: number) => void;
// true if you want to always show the tooltip
dataCy: string;
visibleTooltip?: boolean;
}
export function Slider({ min, max, step, value, onChange }: Props) {
export function Slider({
min,
max,
step,
value,
onChange,
dataCy,
visibleTooltip: visible,
}: Props) {
const SliderWithTooltip = RcSlider.createSliderWithTooltip(RcSlider);
// if the tooltip is always visible, hide the marks when tooltip value gets close to the edges
const marks = {
[min]: translateMinValue(min),
[max]: max.toString(),
[min]: visible && value / max < 0.1 ? '' : translateMinValue(min),
[max]: visible && value / max > 0.9 ? '' : max.toString(),
};
return (
@@ -29,6 +41,11 @@ export function Slider({ min, max, step, value, onChange }: Props) {
defaultValue={value}
onAfterChange={onChange}
className={styles.slider}
tipProps={{ visible }}
railStyle={{ height: 8 }}
trackStyle={{ height: 8 }}
dotStyle={{ visibility: 'hidden' }}
data-cy={dataCy}
/>
</div>
);

View File

@@ -0,0 +1 @@
export { Slider } from './Slider';

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.16.0",
"version": "2.16.1",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"