Compare commits

..

13 Commits
2.9.1 ... 2.9.2

Author SHA1 Message Date
Platforms Team
caced72ec1 Merge branch 'ado-release' 2021-10-26 03:57:28 +00:00
cong meng
0d72896b6b fix(image) EE-1955 unable to tag image (#5973)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-26 15:22:42 +13:00
Platforms Team
48b69852eb Merge branch 'ado-release' 2021-10-25 20:49:52 +00:00
Richard Wei
40a6645e23 fix user not able to get nodes (#5950) 2021-10-21 11:55:37 +13:00
Stéphane Busso
90a18b5ded Bump dbversion 2021-10-20 20:35:18 +13:00
Hui
d17e7c8160 fix(stack): auto update breaks after restarting Portainer EE-1915 2021-10-20 16:00:40 +13:00
Matt Hook
f0efc4f904 bump to 2.9.2 2021-10-19 15:51:16 +13:00
cong meng
4f350ab6f5 fix(registry) EE-1861 improve registry selection (#5921)
* fix(registry) EE-1861 fail to select registry with same name

* fix(registry) EE-1861 show registry modal when pull and push image

* fix(registry) EE-1861 cleanup code

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-19 14:54:44 +13:00
fhanportainer
1ff5f25e40 fix(registry): ignore pull limit in non-docker hub registry. (#5917) 2021-10-19 13:21:57 +13:00
Matt Hook
006634e007 fix(helm): allow settings to be saved offline EE-1907 (#5908)
* skip validating default helm repo to allow offline saving of settings. Default repo is hardcoded and correct.

* dont validate the helm repo if the repo hasn't changed or is the default

* fix logic
2021-10-18 15:08:38 +13:00
cong meng
9dcd5651e8 fix(registry) EE-1861 improve registry selection (#5899)
* fix(registry) EE-1861 hide anonymous dockerhub registry if user has an authenticated one

* fix(registry) EE-1861 pick up a best match dockerhub registry

* fix(registry) EE-1861 set the anonymous registry as default if it is shown

* fix(registry) EE-1861 refactor how to match registry

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-15 21:42:46 +13:00
andres-portainer
dfe0b3f69d fix(namespaces): remove the stacks from the data store when deleting their corresponding Kubernetes namespace EE-1872 (#5885)
* fix(namespaces): remove the stacks from the data store when deleting their corresponding Kubernetes namespace EE-1872

* add endpoint ID checking

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: ArrisLee <arris_li@hotmail.com>
2021-10-14 19:15:04 -03:00
cong meng
f544d4447c fix(rbac) EE-1867 regular user unable to access pod and node stats view (#5886)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-14 17:00:31 +13:00
16 changed files with 186 additions and 38 deletions

View File

@@ -192,8 +192,8 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
var stack portainer.Stack
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
stack := portainer.Stack{}
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err

View File

@@ -74,7 +74,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.9.1
// @version 2.9.2
// @description.markdown api-description.md
// @termsOfService

View File

@@ -54,11 +54,8 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
}
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" {
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
if err != nil {
return errors.Wrap(err, "Invalid Helm repository URL. Must correspond to a valid URL format")
}
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) {
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
}
if payload.UserSessionTimeout != nil {
_, err := time.ParseDuration(*payload.UserSessionTimeout)
@@ -114,7 +111,16 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.HelmRepositoryURL != nil {
settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Helm repository URL. Must correspond to a valid URL format", err}
}
}
settings.HelmRepositoryURL = newHelmRepo
}
if payload.BlackListedLabels != nil {

View File

@@ -46,5 +46,19 @@ func (transport *baseTransport) proxyNamespaceDeleteOperation(request *http.Requ
}
}
}
stacks, err := transport.dataStore.Stack().Stacks()
if err != nil {
return nil, err
}
for _, s := range stacks {
if s.Namespace == namespace && s.EndpointID == transport.endpoint.ID {
if err := transport.dataStore.Stack().DeleteStack(s.ID); err != nil {
return nil, err
}
}
}
return transport.executeKubernetesRequest(request)
}

View File

@@ -9,7 +9,7 @@ import (
func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
return []rbacv1.PolicyRule{
{
Verbs: []string{"list"},
Verbs: []string{"list", "get"},
Resources: []string{"namespaces", "nodes"},
APIGroups: []string{""},
},
@@ -18,6 +18,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
Resources: []string{"storageclasses"},
APIGroups: []string{"storage.k8s.io"},
},
{
Verbs: []string{"list", "get"},
Resources: []string{"namespaces", "pods", "nodes"},
APIGroups: []string{"metrics.k8s.io"},
},
}
}

View File

@@ -1470,9 +1470,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.9.1"
APIVersion = "2.9.2"
// DBVersion is the version number of the Portainer database
DBVersion = 32
DBVersion = 33
// 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

@@ -69,13 +69,19 @@ class porImageRegistryController {
async reloadRegistries() {
return this.$async(async () => {
try {
const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
this.registries = _.concat(this.defaultRegistry, registries);
let showDefaultRegistry = false;
this.registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
// hide default(anonymous) dockerhub registry if user has an authenticated one
if (!this.registries.some((registry) => registry.Type === RegistryTypes.DOCKERHUB)) {
showDefaultRegistry = true;
this.registries.push(this.defaultRegistry);
}
const id = this.model.Registry.Id;
const registry = _.find(this.registries, { Id: id });
if (!registry) {
this.model.Registry = this.defaultRegistry;
this.model.Registry = showDefaultRegistry ? this.defaultRegistry : this.registries[0];
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');

View File

@@ -6,7 +6,7 @@
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
ng-model="$ctrl.model.Registry"
id="image_registry"
class="form-control"

View File

@@ -82,7 +82,7 @@
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.pullImageValidity || !formValues.RegistryModel.Image" ng-click="tagImage()">Tag</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.RegistryModel.Image" ng-click="tagImage()">Tag</button>
</div>
</div>
</form>

View File

@@ -17,6 +17,8 @@ angular.module('portainer.docker').controller('ImageController', [
'FileSaver',
'Blob',
'endpoint',
'EndpointService',
'RegistryModalService',
function (
$async,
$q,
@@ -32,7 +34,9 @@ angular.module('portainer.docker').controller('ImageController', [
ModalService,
FileSaver,
Blob,
endpoint
endpoint,
EndpointService,
RegistryModalService
) {
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
@@ -84,11 +88,13 @@ angular.module('portainer.docker').controller('ImageController', [
async function pushTag(repository) {
return $async(async () => {
$('#uploadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pushImage(registryModel);
Notifications.success('Image successfully pushed', repository);
const registryModel = await RegistryModalService.registryModal(repository, $scope.registries);
if (registryModel) {
$('#uploadResourceHint').show();
await ImageService.pushImage(registryModel);
Notifications.success('Image successfully pushed', repository);
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
} finally {
@@ -100,11 +106,13 @@ angular.module('portainer.docker').controller('ImageController', [
$scope.pullTag = pullTag;
async function pullTag(repository) {
return $async(async () => {
$('#downloadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pullImage(registryModel);
Notifications.success('Image successfully pulled', repository);
const registryModel = await RegistryModalService.registryModal(repository, $scope.registries);
if (registryModel) {
$('#downloadResourceHint').show();
await ImageService.pullImage(registryModel);
Notifications.success('Image successfully pulled', repository);
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to pull image from repository');
} finally {
@@ -171,8 +179,15 @@ angular.module('portainer.docker').controller('ImageController', [
});
};
function initView() {
async function initView() {
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
try {
$scope.registries = await RegistryService.loadRegistriesForDropdown(endpoint.Id);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load registries');
}
$q.all({
image: ImageService.image($transition$.params().id),
history: ImageService.history($transition$.params().id),

View File

@@ -1771,7 +1771,7 @@
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid()"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"

View File

@@ -2,6 +2,7 @@ import angular from 'angular';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import * as JsonPatch from 'fast-json-patch';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import {
KubernetesApplicationDataAccessPolicies,
@@ -193,6 +194,10 @@ class KubernetesCreateApplicationController {
this.state.pullImageValidity = validity;
}
imageValidityIsValid() {
return this.state.pullImageValidity || this.formValues.ImageModel.Registry.Type !== RegistryTypes.DOCKERHUB;
}
onChangeName() {
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);

View File

@@ -22,6 +22,8 @@ angular.module('portainer.app').factory('RegistryService', [
createRegistry,
createGitlabRegistries,
retrievePorRegistryModelFromRepository,
retrievePorRegistryModelFromRepositoryWithRegistries,
loadRegistriesForDropdown,
};
function registries() {
@@ -107,17 +109,45 @@ angular.module('portainer.app').factory('RegistryService', [
return url;
}
// findBestMatchRegistry finds out the best match registry for repository
// matching precedence:
// 1. registryId matched
// 2. both domain name and username matched (for dockerhub only)
// 3. only URL matched
// 4. pick up the first dockerhub registry
function findBestMatchRegistry(repository, registries, registryId) {
let match2, match3, match4;
for (const registry of registries) {
if (registry.Id == registryId) {
return registry;
}
if (registry.Type === RegistryTypes.DOCKERHUB) {
// try to match repository examples:
// <USERNAME>/nginx:latest
// docker.io/<USERNAME>/nginx:latest
if (repository.startsWith(registry.Username + '/') || repository.startsWith(getURL(registry) + '/' + registry.Username + '/')) {
match2 = registry;
}
// try to match repository examples:
// portainer/portainer-ee:latest
// <NON-USERNAME>/portainer-ee:latest
match4 = match4 || registry;
}
if (_.includes(repository, getURL(registry))) {
match3 = registry;
}
}
return match2 || match3 || match4;
}
function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId) {
const model = new PorImageRegistryModel();
const registry = registries.find((reg) => {
if (registryId) {
return reg.Id === registryId;
}
if (reg.Type === RegistryTypes.DOCKERHUB) {
return _.includes(repository, reg.Username);
}
return _.includes(repository, getURL(reg));
});
const registry = findBestMatchRegistry(repository, registries, registryId);
if (registry) {
const url = getURL(registry);
let lastIndex = repository.lastIndexOf(url);
@@ -148,5 +178,22 @@ angular.module('portainer.app').factory('RegistryService', [
}
});
}
function loadRegistriesForDropdown(endpointId, namespace) {
return $async(async () => {
try {
const registries = await EndpointService.registries(endpointId, namespace);
// hide default(anonymous) dockerhub registry if user has an authenticated one
if (!registries.some((registry) => registry.Type === RegistryTypes.DOCKERHUB)) {
registries.push(new DockerHubViewModel());
}
return registries;
} catch (err) {
throw { msg: 'Unable to retrieve the registries', err: err };
}
});
}
},
]);

View File

@@ -308,6 +308,17 @@ angular.module('portainer.app').factory('ModalService', [
);
};
service.selectRegistry = function (options) {
var box = bootbox.prompt({
title: 'Which registry do you want to use?',
inputType: 'select',
value: options.defaultValue,
inputOptions: options.options,
callback: options.callback,
});
applyBoxCSS(box);
};
return service;
},
]);

View File

@@ -0,0 +1,39 @@
import _ from 'lodash';
angular.module('portainer.app').factory('RegistryModalService', ModalServiceFactory);
function ModalServiceFactory($q, ModalService, RegistryService) {
const service = {};
function registries2Options(registries) {
return registries.map((r) => ({
text: r.Name,
value: String(r.Id),
}));
}
service.registryModal = async function (repository, registries) {
const deferred = $q.defer();
const options = registries2Options(registries);
const registryModel = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries);
const defaultValue = String(_.get(registryModel, 'Registry.Id', '0'));
ModalService.selectRegistry({
options,
defaultValue,
callback: (registryId) => {
if (registryId) {
const registryModel = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId);
deferred.resolve(registryModel);
} else {
deferred.resolve(null);
}
},
});
return deferred.promise;
};
return service;
}

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.9.1",
"version": "2.9.2",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@@ -176,4 +176,4 @@
"*.js": "eslint --cache --fix",
"*.{js,css,md,html}": "prettier --write"
}
}
}