mirror of https://github.com/portainer/portainer
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 updatepull/6739/head
parent
9ffaf47741
commit
298e3d263e
|
@ -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")}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -401,8 +401,7 @@ angular
|
|||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/registries/edit/registry.html',
|
||||
controller: 'RegistryController',
|
||||
component: 'editRegistry',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a>
|
||||
<a ui-sref="portainer.registries">Registries</a> > <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>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import angular from 'angular';
|
||||
import controller from './registryController';
|
||||
|
||||
angular.module('portainer.app').component('editRegistry', {
|
||||
templateUrl: './registry.html',
|
||||
controller,
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue