feat(registry): enforce name uniqueness for registries (#6709)

* feat(app/registries): add name uniqueness validation on registry creation

* feat(api/registry): enforce name uniqueness on registry creation

* feat(api/registry): enforce name uniqueness on registry update

* feat(app/registry): enforce name uniqueness on registry update
pull/6739/head
LP B 2022-04-07 22:58:26 +02:00 committed by GitHub
parent 9ffaf47741
commit 298e3d263e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 265 additions and 143 deletions

View File

@ -126,6 +126,9 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
for _, r := range registries {
if r.Name == registry.Name {
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same name already exists", errors.New("A registry is already defined with this name")}
}
if handler.registriesHaveSameURLAndCredentials(&r, registry) {
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
}

View File

@ -77,6 +77,11 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
registries, err := handler.DataStore.Registry().Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
var payload registryUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@ -86,6 +91,15 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
if payload.Name != nil {
registry.Name = *payload.Name
}
// enforce name uniqueness across registries
// check is performed even if Name didn't change (Name not in payload) as we need
// to enforce this rule on updates not performed with frontend (e.g. on direct API requests)
// see https://portainer.atlassian.net/browse/EE-2706 for more details
for _, r := range registries {
if r.ID != registry.ID && r.Name == registry.Name {
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same name already exists", errors.New("A registry is already defined with this name")}
}
}
if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
registry.BaseURL = *payload.BaseURL
@ -129,10 +143,6 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
shouldUpdateSecrets = shouldUpdateSecrets || (*payload.URL != registry.URL)
registry.URL = *payload.URL
registries, err := handler.DataStore.Registry().Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
for _, r := range registries {
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {

View File

@ -401,8 +401,7 @@ angular
url: '/:id',
views: {
'content@': {
templateUrl: './views/registries/edit/registry.html',
controller: 'RegistryController',
component: 'editRegistry',
},
},
};

View File

@ -1,4 +1,4 @@
<form class="form-horizontal" name="registryFormEcr" ng-submit="$ctrl.formAction()">
<form class="form-horizontal" name="$ctrl.registryFormEcr" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title"> Important notice </div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
@ -16,10 +16,11 @@
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="my-ecr-registry" required auto-focus />
</div>
</div>
<div class="form-group" ng-show="registryFormEcr.registry_name.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormEcr.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormEcr.registry_name.$error">
<div ng-messages="$ctrl.registryFormEcr.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="used"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
@ -43,9 +44,9 @@
/>
</div>
</div>
<div class="form-group" ng-show="registryFormEcr.registry_url.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormEcr.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormEcr.registry_url.$error">
<div ng-messages="$ctrl.registryFormEcr.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -72,9 +73,9 @@
<input type="text" class="form-control" id="registry_access_key" name="registry_access_key" ng-model="$ctrl.model.Username" required />
</div>
</div>
<div class="form-group" ng-show="registryFormEcr.registry_access_key.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormEcr.registry_access_key.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormEcr.registry_access_key.$error">
<div ng-messages="$ctrl.registryFormEcr.registry_access_key.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -88,9 +89,9 @@
<input type="password" class="form-control" id="registry_secret_access_key" name="registry_secret_access_key" ng-model="$ctrl.model.Password" required />
</div>
</div>
<div class="form-group" ng-show="registryFormEcr.registry_secret_access_key.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormEcr.registry_secret_access_key.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormEcr.registry_secret_access_key.$error">
<div ng-messages="$ctrl.registryFormEcr.registry_secret_access_key.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -104,10 +105,11 @@
<input type="text" class="form-control" id="registry_region" name="registry_region" placeholder="us-west-1" ng-model="$ctrl.model.Ecr.Region" required />
</div>
</div>
<div class="form-group" ng-show="registryFormEcr.registry_region.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormEcr.registry_region.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormEcr.registry_region.$error">
<div ng-messages="$ctrl.registryFormEcr.registry_region.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="used"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
@ -121,7 +123,7 @@
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormEcr.$valid"
ng-disabled="$ctrl.actionInProgress || !$ctrl.registryFormEcr.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"

View File

@ -1,3 +1,9 @@
class controller {
$postLink() {
this.registryFormEcr.registry_name.$validators.used = (modelValue) => !this.nameIsUsed(modelValue);
}
}
angular.module('portainer.app').component('registryFormEcr', {
templateUrl: './registry-form-ecr.html',
bindings: {
@ -5,5 +11,7 @@ angular.module('portainer.app').component('registryFormEcr', {
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
nameIsUsed: '<',
},
controller,
});

View File

@ -1,4 +1,4 @@
<form class="form-horizontal" name="registryFormAzure" ng-submit="$ctrl.formAction()">
<form class="form-horizontal" name="$ctrl.registryFormAzure" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title"> Azure registry details </div>
<!-- name-input -->
<div class="form-group">
@ -7,10 +7,11 @@
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="my-azure-registry" required auto-focus />
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_name.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormAzure.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_name.$error">
<div ng-messages="$ctrl.registryFormAzure.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="used"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
@ -25,9 +26,9 @@
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="myproject.azurecr.io" required />
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_url.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormAzure.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_url.$error">
<div ng-messages="$ctrl.registryFormAzure.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -40,9 +41,9 @@
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required />
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_username.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormAzure.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_username.$error">
<div ng-messages="$ctrl.registryFormAzure.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -55,9 +56,9 @@
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required />
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_password.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormAzure.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_password.$error">
<div ng-messages="$ctrl.registryFormAzure.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -70,7 +71,7 @@
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid"
ng-disabled="$ctrl.actionInProgress || !$ctrl.registryFormAzure.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"

View File

@ -1,3 +1,9 @@
class controller {
$postLink() {
this.registryFormAzure.registry_name.$validators.used = (modelValue) => !this.nameIsUsed(modelValue);
}
}
angular.module('portainer.app').component('registryFormAzure', {
templateUrl: './registry-form-azure.html',
bindings: {
@ -5,5 +11,7 @@ angular.module('portainer.app').component('registryFormAzure', {
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
nameIsUsed: '<',
},
controller,
});

View File

@ -1,4 +1,4 @@
<form class="form-horizontal" name="registryFormCustom" ng-submit="$ctrl.formAction()">
<form class="form-horizontal" name="$ctrl.registryFormCustom" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title"> Important notice </div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
@ -14,10 +14,11 @@
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="my-custom-registry" required auto-focus />
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_name.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormCustom.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_name.$error">
<div ng-messages="$ctrl.registryFormCustom.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="used"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
@ -32,9 +33,9 @@
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="10.0.0.10:5000 or myregistry.domain.tld" required />
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_url.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormCustom.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_url.$error">
<div ng-messages="$ctrl.registryFormCustom.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -59,9 +60,9 @@
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required />
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_username.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormCustom.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_username.$error">
<div ng-messages="$ctrl.registryFormCustom.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -74,9 +75,9 @@
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required />
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_password.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormCustom.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_password.$error">
<div ng-messages="$ctrl.registryFormCustom.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -90,7 +91,7 @@
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid"
ng-disabled="$ctrl.actionInProgress || !$ctrl.registryFormCustom.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"

View File

@ -1,3 +1,9 @@
class controller {
$postLink() {
this.registryFormCustom.registry_name.$validators.used = (modelValue) => !this.nameIsUsed(modelValue);
}
}
angular.module('portainer.app').component('registryFormCustom', {
templateUrl: './registry-form-custom.html',
bindings: {
@ -5,5 +11,7 @@ angular.module('portainer.app').component('registryFormCustom', {
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
nameIsUsed: '<',
},
controller,
});

View File

@ -1,4 +1,4 @@
<form class="form-horizontal" name="registryFormDockerhub" ng-submit="$ctrl.formAction()">
<form class="form-horizontal" name="$ctrl.registryFormDockerhub" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title"> Important notice </div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
@ -16,10 +16,11 @@
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="dockerhub-prod-us" required />
</div>
</div>
<div class="form-group" ng-show="registryFormDockerhub.registry_name.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormDockerhub.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormDockerhub.registry_name.$error">
<div ng-messages="$ctrl.registryFormDockerhub.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="used"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
@ -31,9 +32,9 @@
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required />
</div>
</div>
<div class="form-group" ng-show="registryFormDockerhub.registry_username.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormDockerhub.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormDockerhub.registry_username.$error">
<div ng-messages="$ctrl.registryFormDockerhub.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -46,9 +47,9 @@
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required />
</div>
</div>
<div class="form-group" ng-show="registryFormDockerhub.registry_password.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormDockerhub.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormDockerhub.registry_password.$error">
<div ng-messages="$ctrl.registryFormDockerhub.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -62,7 +63,7 @@
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid"
ng-disabled="$ctrl.actionInProgress || !$ctrl.registryFormDockerhub.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"

View File

@ -1,3 +1,9 @@
class controller {
$postLink() {
this.registryFormDockerhub.registry_name.$validators.used = (modelValue) => !this.nameIsUsed(modelValue);
}
}
angular.module('portainer.app').component('registryFormDockerhub', {
templateUrl: './registry-form-dockerhub.html',
bindings: {
@ -5,5 +11,7 @@ angular.module('portainer.app').component('registryFormDockerhub', {
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
nameIsUsed: '<',
},
controller,
});

View File

@ -1,4 +1,4 @@
<form class="form-horizontal" name="registryFormProGet" ng-submit="$ctrl.formAction()">
<form class="form-horizontal" name="$ctrl.registryFormProGet" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title"> Important notice </div>
<div class="form-group">
<div class="col-sm-12 text-muted small">
@ -14,10 +14,11 @@
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="proget-registry" required auto-focus />
</div>
</div>
<div class="form-group" ng-show="registryFormProGet.registry_name.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormProGet.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormProGet.registry_name.$error">
<div ng-messages="$ctrl.registryFormProGet.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i>This field is required.</p>
<p ng-message="used"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
@ -32,9 +33,9 @@
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="proget.example.com/example-registry" required />
</div>
</div>
<div class="form-group" ng-show="registryFormProGet.registry_url.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormProGet.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormProGet.registry_url.$error">
<div ng-messages="$ctrl.registryFormProGet.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -50,9 +51,9 @@
<input type="text" class="form-control" id="registry_base_url" name="registry_base_url" ng-model="$ctrl.model.BaseURL" placeholder="proget.example.com" required />
</div>
</div>
<div class="form-group" ng-show="registryFormProGet.registry_base_url.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormProGet.registry_base_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormProGet.registry_base_url.$error">
<div ng-messages="$ctrl.registryFormProGet.registry_base_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -66,9 +67,9 @@
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required />
</div>
</div>
<div class="form-group" ng-show="registryFormProGet.registry_username.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormProGet.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormProGet.registry_username.$error">
<div ng-messages="$ctrl.registryFormProGet.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -81,9 +82,9 @@
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required />
</div>
</div>
<div class="form-group" ng-show="registryFormProGet.registry_password.$invalid">
<div class="form-group" ng-show="$ctrl.registryFormProGet.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormProGet.registry_password.$error">
<div ng-messages="$ctrl.registryFormProGet.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
@ -97,7 +98,7 @@
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid"
ng-disabled="$ctrl.actionInProgress || !$ctrl.registryFormProGet.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"

View File

@ -1,3 +1,9 @@
class controller {
$postLink() {
this.registryFormProGet.registry_name.$validators.used = (modelValue) => !this.nameIsUsed(modelValue);
}
}
angular.module('portainer.app').component('registryFormProget', {
templateUrl: './registry-form-proget.html',
bindings: {
@ -5,5 +11,7 @@ angular.module('portainer.app').component('registryFormProget', {
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
nameIsUsed: '<',
},
controller,
});

View File

@ -101,6 +101,7 @@
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="$ctrl.state.actionInProgress"
name-is-used="($ctrl.nameIsUsed)"
></registry-form-azure>
<registry-form-custom
@ -109,6 +110,7 @@
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="$ctrl.state.actionInProgress"
name-is-used="($ctrl.nameIsUsed)"
></registry-form-custom>
<registry-form-ecr
@ -117,6 +119,7 @@
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="$ctrl.state.actionInProgress"
name-is-used="($ctrl.nameIsUsed)"
></registry-form-ecr>
<registry-form-proget
@ -125,6 +128,7 @@
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="$ctrl.state.actionInProgress"
name-is-used="($ctrl.nameIsUsed)"
></registry-form-proget>
<registry-form-gitlab
@ -144,6 +148,7 @@
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="$ctrl.state.actionInProgress"
name-is-used="($ctrl.nameIsUsed)"
></registry-form-dockerhub>
</form>
</rd-widget-body>

View File

@ -1,3 +1,4 @@
import _ from 'lodash';
import { RegistryTypes } from 'Portainer/models/registryTypes';
import { RegistryCreateFormValues } from 'Portainer/models/registry';
@ -21,6 +22,8 @@ class CreateRegistryController {
};
this.createRegistry = this.createRegistry.bind(this);
this.getRegistries = this.getRegistries.bind(this);
this.nameIsUsed = this.nameIsUsed.bind(this);
this.retrieveGitlabRegistries = this.retrieveGitlabRegistries.bind(this);
this.createGitlabRegistries = this.createGitlabRegistries.bind(this);
}
@ -128,16 +131,34 @@ class CreateRegistryController {
});
}
nameIsUsed(name) {
return _.includes(this.registriesNames, name);
}
getRegistries() {
return this.$async(async () => {
try {
const registries = await this.RegistryService.registries();
this.registriesNames = _.map(registries, 'Name');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to fetch existing registries');
}
});
}
$onInit() {
this.model = new RegistryCreateFormValues();
return this.$async(async () => {
this.model = new RegistryCreateFormValues();
const from = this.$transition$.from();
const params = this.$transition$.params('from');
const from = this.$transition$.from();
const params = this.$transition$.params('from');
if (from.name && /^[a-z]+\.registries$/.test(from.name)) {
this.state.originViewReference = from;
this.state.originalEndpointId = params.endpointId || null;
}
if (from.name && /^[a-z]+\.registries$/.test(from.name)) {
this.state.originViewReference = from;
this.state.originalEndpointId = params.endpointId || null;
}
await this.getRegistries();
});
}
}

View File

@ -1,11 +1,11 @@
<rd-header>
<rd-header-title title-text="Registry details"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry({id: $ctrl.registry.Id})">{{ $ctrl.registry.Name }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="row" ng-if="!$ctrl.state.loading">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
@ -14,7 +14,14 @@
<div class="form-group">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" ng-model="registry.Name" placeholder="e.g. my-registry" />
<input type="text" class="form-control" id="registry_name" ng-model="$ctrl.formValues.Name" placeholder="e.g. my-registry" ng-change="$ctrl.onChangeName()" />
</div>
</div>
<div class="form-group" ng-show="$ctrl.state.nameAlreadyExists">
<div class="col-sm-12 small text-warning">
<div>
<p><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A registry with the same name already exists.</p>
</div>
</div>
</div>
<!-- !name-input -->
@ -22,81 +29,79 @@
<div class="form-group">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip
position="bottom"
message="URL or IP address of a Docker registry. Any protocol or trailing slash will be stripped if present."
></portainer-tooltip>
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol or trailing slash will be stripped if present.">
</portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" ng-model="registry.URL" placeholder="e.g. 10.0.0.10:5000 or myregistry.domain.tld" />
<input type="text" class="form-control" id="registry_url" ng-model="$ctrl.registry.URL" placeholder="e.g. 10.0.0.10:5000 or myregistry.domain.tld" />
</div>
</div>
<!-- !registry-url-input -->
<!-- authentication-checkbox -->
<div class="form-group" ng-if="registry.Type !== RegistryTypes.PROGET">
<div class="form-group" ng-if="$ctrl.registry.Type !== $ctrl.RegistryTypes.PROGET">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."> </portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px"> <input type="checkbox" ng-model="registry.Authentication" /><i></i> </label>
<label class="switch" style="margin-left: 20px"> <input type="checkbox" ng-model="$ctrl.registry.Authentication" /><i></i> </label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="registry.Authentication">
<div ng-if="$ctrl.registry.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">
{{ registry.Type === RegistryTypes.ECR ? 'AWS Access Key' : 'Username' }}
{{ $ctrl.registry.Type === $ctrl.RegistryTypes.ECR ? 'AWS Access Key' : 'Username' }}
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="credentials_username" ng-model="registry.Username" />
<input type="text" class="form-control" id="credentials_username" ng-model="$ctrl.registry.Username" />
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">
{{ passwordLabel() }}
{{ $ctrl.passwordLabel() }}
</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password" placeholder="*******" />
<input type="password" class="form-control" id="credentials_password" ng-model="$ctrl.formValues.Password" placeholder="*******" />
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div ng-if="registry.Type == RegistryTypes.QUAY">
<div ng-if="$ctrl.registry.Type == $ctrl.RegistryTypes.QUAY">
<!-- organisation-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left"> Use organisation registry </label>
<label class="switch" style="margin-left: 20px"> <input type="checkbox" ng-model="registry.Quay.UseOrganisation" /><i></i> </label>
<label class="switch" style="margin-left: 20px"> <input type="checkbox" ng-model="$ctrl.registry.Quay.UseOrganisation" /><i></i> </label>
</div>
</div>
<!-- !organisation-checkbox -->
<div ng-if="registry.Quay.UseOrganisation">
<div ng-if="$ctrl.registry.Quay.UseOrganisation">
<!-- organisation_name -->
<div class="form-group">
<label for="organisation_name" class="col-sm-3 col-lg-2 control-label text-left">Organisation name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="organisation_name" name="organisation_name" ng-model="registry.Quay.OrganisationName" required />
<input type="text" class="form-control" id="organisation_name" name="organisation_name" ng-model="$ctrl.registry.Quay.OrganisationName" required />
</div>
</div>
<!-- !organisation_name -->
</div>
</div>
<div ng-if="registry.Type == RegistryTypes.ECR">
<div ng-if="$ctrl.registry.Type == $ctrl.RegistryTypes.ECR">
<!-- region -->
<div class="form-group">
<label for="registry_region" class="col-sm-3 col-lg-2 control-label text-left">Region</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_region" name="registry_region" placeholder="us-west-1" ng-model="registry.Ecr.Region" required />
<input type="text" class="form-control" id="registry_region" name="registry_region" placeholder="us-west-1" ng-model="$ctrl.registry.Ecr.Region" required />
</div>
</div>
<div class="form-group" ng-show="registryFormEcr.registry_region.$invalid">
@ -114,12 +119,12 @@
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !registry.Name || !registry.URL || (registry.Type == RegistryTypes.QUAY && registry.Quay.UseOrganisation && !registry.Quay.OrganisationName)"
ng-click="updateRegistry()"
button-spinner="state.actionInProgress"
ng-disabled="$ctrl.isUpdateButtonDisabled()"
ng-click="$ctrl.updateRegistry()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Update registry</span>
<span ng-show="state.actionInProgress">Updating registry...</span>
<span ng-hide="$ctrl.state.actionInProgress">Update registry</span>
<span ng-show="$ctrl.state.actionInProgress">Updating registry...</span>
</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.registries">Cancel</a>
</div>

View File

@ -0,0 +1,10 @@
import angular from 'angular';
import controller from './registryController';
angular.module('portainer.app').component('editRegistry', {
templateUrl: './registry.html',
controller,
bindings: {
$transition$: '<',
},
});

View File

@ -1,61 +1,84 @@
import _ from 'lodash';
import { RegistryTypes } from '@/portainer/models/registryTypes';
angular.module('portainer.app').controller('RegistryController', [
'$scope',
'$state',
'RegistryService',
'Notifications',
function ($scope, $state, RegistryService, Notifications) {
$scope.state = {
export default class RegistryController {
/* @ngInject */
constructor($async, $state, RegistryService, Notifications) {
Object.assign(this, { $async, $state, RegistryService, Notifications });
this.RegistryTypes = RegistryTypes;
this.state = {
actionInProgress: false,
loading: false,
};
$scope.formValues = {
this.formValues = {
Password: '',
};
}
$scope.RegistryTypes = RegistryTypes;
$scope.passwordLabel = () => {
const type = $scope.registry.Type;
switch (type) {
case RegistryTypes.ECR:
return 'AWS Secret Access Key';
case RegistryTypes.DOCKERHUB:
return 'Access token';
default:
return 'Password';
}
};
$scope.updateRegistry = function () {
var registry = $scope.registry;
registry.Password = $scope.formValues.Password;
$scope.state.actionInProgress = true;
RegistryService.updateRegistry(registry)
.then(function success() {
Notifications.success('Registry successfully updated');
$state.go('portainer.registries');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update registry');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
function initView() {
var registryID = $state.params.id;
RegistryService.registry(registryID)
.then(function success(data) {
$scope.registry = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
passwordLabel() {
const type = this.registry.Type;
switch (type) {
case RegistryTypes.ECR:
return 'AWS Secret Access Key';
case RegistryTypes.DOCKERHUB:
return 'Access token';
default:
return 'Password';
}
};
initView();
},
]);
updateRegistry() {
return this.$async(async () => {
try {
this.state.actionInProgress = true;
const registry = this.registry;
registry.Password = this.formValues.Password;
registry.Name = this.formValues.Name;
await this.RegistryService.updateRegistry(registry);
this.Notifications.success('Registry successfully updated');
this.$state.go('portainer.registries');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update registry');
} finally {
this.state.actionInProgress = false;
}
});
}
onChangeName() {
this.state.nameAlreadyExists = _.includes(this.registriesNames, this.formValues.Name);
}
isUpdateButtonDisabled() {
return (
this.state.actionInProgress ||
this.state.nameAlreadyExists ||
!this.registry.Name ||
!this.registry.URL ||
(this.registry.Type == this.RegistryTypes.QUAY && this.registry.Quay.UseOrganisation && !this.registry.Quay.OrganisationName)
);
}
async $onInit() {
try {
this.state.loading = true;
const registryId = this.$state.params.id;
const registry = await this.RegistryService.registry(registryId);
this.registry = registry;
this.formValues.Name = registry.Name;
const registries = await this.RegistryService.registries();
_.pullAllBy(registries, [registry], 'Id');
this.registriesNames = _.map(registries, 'Name');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registry details');
} finally {
this.state.loading = false;
}
}
}