mirror of https://github.com/portainer/portainer
feat(registry): gitlab support (#3107)
* feat(api): gitlab registry type * feat(registries): early support for gitlab registries * feat(app): registry service selector * feat(registry): gitlab support : list repositories and tags - remove features missing * feat(registry): gitlab registry remove features * feat(registry): gitlab switch to registry V2 API for repositories and tags * feat(api): use development extension binary * fix(registry): avoid 401 on gitlab retrieve to disconnect the user * feat(registry): gitlab browse projects without extension * style(app): code cleaning * refactor(app): PR review changes + refactor on types * fix(gitlab): remove gitlab info from registrymanagementconfig and force gitlab type * style(api): go fmt * feat(api): update APIVersion and ExtensionDefinitionsURL * fix(api): fix invalid RM extension URL * feat(registry): PAT scope help * feat(registry): defaults on registry creation * style(registry-creation): update layout and text for Gitlab registry * feat(registry-creation): update gitlab noticepull/3349/head
parent
03d9d6afbb
commit
198e92c734
|
@ -46,6 +46,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||||
|
h.PathPrefix("/registries/{id}/proxies/gitlab").Handler(
|
||||||
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithRegistry)))
|
||||||
|
h.PathPrefix("/registries/proxies/gitlab").Handler(
|
||||||
|
bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry)))
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register registry proxy", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package registries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// request on /api/registries/proxies/gitlab
|
||||||
|
func (handler *Handler) proxyRequestsToGitlabAPIWithoutRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
domain := r.Header.Get("X-Gitlab-Domain")
|
||||||
|
if domain == "" {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "No Gitlab domain provided", nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, err := handler.ProxyManager.CreateGitlabProxy(domain)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create gitlab proxy", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.StripPrefix("/registries/proxies/gitlab", proxy).ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package registries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// request on /api/registries/{id}/proxies/gitlab
|
||||||
|
func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxy http.Handler
|
||||||
|
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
|
||||||
|
if proxy == nil {
|
||||||
|
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &portainer.RegistryManagementConfiguration{
|
||||||
|
Type: portainer.GitlabRegistry,
|
||||||
|
Password: registry.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedConfiguration, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
id := strconv.Itoa(int(registryID))
|
||||||
|
r.Header.Set("X-RegistryManagement-Key", id+"-gitlab")
|
||||||
|
r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL)
|
||||||
|
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
|
||||||
|
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
|
||||||
|
|
||||||
|
http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -12,11 +12,12 @@ import (
|
||||||
|
|
||||||
type registryCreatePayload struct {
|
type registryCreatePayload struct {
|
||||||
Name string
|
Name string
|
||||||
Type int
|
Type portainer.RegistryType
|
||||||
URL string
|
URL string
|
||||||
Authentication bool
|
Authentication bool
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
Gitlab portainer.GitlabRegistryData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -29,8 +30,8 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
||||||
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
|
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||||
}
|
}
|
||||||
if payload.Type != 1 && payload.Type != 2 && payload.Type != 3 {
|
if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry {
|
||||||
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)")
|
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -42,16 +43,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
registries, err := handler.RegistryService.Registries()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
|
||||||
}
|
|
||||||
for _, r := range registries {
|
|
||||||
if r.URL == payload.URL {
|
|
||||||
return &httperror.HandlerError{http.StatusConflict, "A registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry := &portainer.Registry{
|
registry := &portainer.Registry{
|
||||||
Type: portainer.RegistryType(payload.Type),
|
Type: portainer.RegistryType(payload.Type),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
|
@ -61,6 +52,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
Password: payload.Password,
|
Password: payload.Password,
|
||||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
|
Gitlab: payload.Gitlab,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.RegistryService.CreateRegistry(registry)
|
err = handler.RegistryService.CreateRegistry(registry)
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gitlabTransport struct {
|
||||||
|
httpTransport *http.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGitlabProxy(uri string) (http.Handler, error) {
|
||||||
|
url, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
||||||
|
proxy.Transport = &gitlabTransport{
|
||||||
|
httpTransport: &http.Transport{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *gitlabTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
token := request.Header.Get("Private-Token")
|
||||||
|
if token == "" {
|
||||||
|
return nil, errors.New("No gitlab token provided")
|
||||||
|
}
|
||||||
|
r, err := http.NewRequest(request.Method, request.URL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.Header.Set("Private-Token", token)
|
||||||
|
return transport.httpTransport.RoundTrip(r)
|
||||||
|
}
|
|
@ -182,3 +182,8 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler,
|
||||||
|
|
||||||
return manager.createDockerProxy(endpoint)
|
return manager.createDockerProxy(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API..
|
||||||
|
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
|
||||||
|
return newGitlabProxy(url)
|
||||||
|
}
|
||||||
|
|
|
@ -191,6 +191,12 @@ type (
|
||||||
// RegistryType represents a type of registry
|
// RegistryType represents a type of registry
|
||||||
RegistryType int
|
RegistryType int
|
||||||
|
|
||||||
|
// GitlabRegistryData represents data required for gitlab registry to work
|
||||||
|
GitlabRegistryData struct {
|
||||||
|
ProjectID int `json:"ProjectId"`
|
||||||
|
InstanceURL string `json:"InstanceURL"`
|
||||||
|
}
|
||||||
|
|
||||||
// Registry represents a Docker registry with all the info required
|
// Registry represents a Docker registry with all the info required
|
||||||
// to connect to it
|
// to connect to it
|
||||||
Registry struct {
|
Registry struct {
|
||||||
|
@ -202,6 +208,7 @@ type (
|
||||||
Username string `json:"Username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"Password,omitempty"`
|
Password string `json:"Password,omitempty"`
|
||||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||||
|
Gitlab GitlabRegistryData `json:"Gitlab"`
|
||||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
|
|
||||||
|
@ -903,7 +910,7 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.22.1"
|
APIVersion = "1.23.0-dev"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 21
|
DBVersion = 21
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
|
@ -913,7 +920,7 @@ const (
|
||||||
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
||||||
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
||||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.1.json"
|
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
|
||||||
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
||||||
SupportProductsURL = AssetsServerURL + "/support.json"
|
SupportProductsURL = AssetsServerURL + "/support.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
|
@ -1076,6 +1083,8 @@ const (
|
||||||
AzureRegistry
|
AzureRegistry
|
||||||
// CustomRegistry represents a custom registry
|
// CustomRegistry represents a custom registry
|
||||||
CustomRegistry
|
CustomRegistry
|
||||||
|
// GitlabRegistry represents a gitlab registry
|
||||||
|
GitlabRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -68,7 +68,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||||
// authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector
|
// authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector
|
||||||
// to have more controls on which URL should trigger the unauthenticated state.
|
// to have more controls on which URL should trigger the unauthenticated state.
|
||||||
$rootScope.$on('unauthenticated', function (event, data) {
|
$rootScope.$on('unauthenticated', function (event, data) {
|
||||||
if (!_.includes(data.config.url, '/v2/')) {
|
if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) {
|
||||||
$state.go('portainer.auth', { error: 'Your session has expired' });
|
$state.go('portainer.auth', { error: 'Your session has expired' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -121,7 +121,7 @@ angular.module('portainer.docker')
|
||||||
if (!portBinding.containerPort) {
|
if (!portBinding.containerPort) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hostPort = portBinding.hostPort;
|
let hostPort = portBinding.hostPort;
|
||||||
const containerPortRange = parsePortRange(portBinding.containerPort);
|
const containerPortRange = parsePortRange(portBinding.containerPort);
|
||||||
if (!isValidPortRange(containerPortRange)) {
|
if (!isValidPortRange(containerPortRange)) {
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<tr ng-hide="$ctrl.loading" dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
<tr ng-hide="$ctrl.loading" dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
||||||
ng-class="{active: item.Checked}">
|
ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})" class="monospaced"
|
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})"
|
||||||
title="{{ item.Name }}">{{ item.Name }}</a>
|
title="{{ item.Name }}">{{ item.Name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.TagsCount }}</td>
|
<td>{{ item.TagsCount }}</td>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
import { RepositoryTagViewModel } from '../models/repositoryTag';
|
import { RepositoryTagViewModel } from '../models/repositoryTag';
|
||||||
|
|
||||||
angular.module('portainer.extensions.registrymanagement')
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
|
@ -7,7 +8,7 @@ angular.module('portainer.extensions.registrymanagement')
|
||||||
var helper = {};
|
var helper = {};
|
||||||
|
|
||||||
function historyRawToParsed(rawHistory) {
|
function historyRawToParsed(rawHistory) {
|
||||||
return angular.fromJson(rawHistory[0].v1Compatibility);
|
return _.map(rawHistory, (item) => angular.fromJson(item.v1Compatibility));
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.manifestsToTag = function (manifests) {
|
helper.manifestsToTag = function (manifests) {
|
||||||
|
@ -16,7 +17,7 @@ angular.module('portainer.extensions.registrymanagement')
|
||||||
|
|
||||||
var history = historyRawToParsed(v1.history);
|
var history = historyRawToParsed(v1.history);
|
||||||
var name = v1.tag;
|
var name = v1.tag;
|
||||||
var os = history.os;
|
var os = history[0].os;
|
||||||
var arch = v1.architecture;
|
var arch = v1.architecture;
|
||||||
var size = v2.layers.reduce(function (a, b) {
|
var size = v2.layers.reduce(function (a, b) {
|
||||||
return {
|
return {
|
||||||
|
@ -26,7 +27,7 @@ angular.module('portainer.extensions.registrymanagement')
|
||||||
var imageId = v2.config.digest;
|
var imageId = v2.config.digest;
|
||||||
var imageDigest = v2.digest;
|
var imageDigest = v2.digest;
|
||||||
|
|
||||||
return new RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2);
|
return new RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history);
|
||||||
};
|
};
|
||||||
|
|
||||||
return helper;
|
return helper;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function RegistryGitlabProject(project) {
|
||||||
|
this.Id = project.id;
|
||||||
|
this.Description = project.description;
|
||||||
|
this.Name = project.name;
|
||||||
|
this.Namespace = project.namespace ? project.namespace.name : '';
|
||||||
|
this.PathWithNamespace = project.path_with_namespace;
|
||||||
|
this.RegistryEnabled = project.container_registry_enabled;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
export default function RegistryRepositoryViewModel(item) {
|
export function RegistryRepositoryViewModel(item) {
|
||||||
if (item.name && item.tags) {
|
if (item.name && item.tags) {
|
||||||
this.Name = item.name;
|
this.Name = item.name;
|
||||||
this.TagsCount = _.without(item.tags, null).length;
|
this.TagsCount = _.without(item.tags, null).length;
|
||||||
|
@ -7,4 +7,9 @@ export default function RegistryRepositoryViewModel(item) {
|
||||||
this.Name = item;
|
this.Name = item;
|
||||||
this.TagsCount = 0;
|
this.TagsCount = 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegistryRepositoryGitlabViewModel(data) {
|
||||||
|
this.Name = data.path;
|
||||||
|
this.TagsCount = data.tags.length;
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const RegistryTypes = Object.freeze({
|
||||||
|
'QUAY': 1,
|
||||||
|
'AZURE': 2,
|
||||||
|
'CUSTOM': 3,
|
||||||
|
'GITLAB': 4
|
||||||
|
})
|
|
@ -1,4 +1,4 @@
|
||||||
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2) {
|
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history) {
|
||||||
this.Name = name;
|
this.Name = name;
|
||||||
this.Os = os || '';
|
this.Os = os || '';
|
||||||
this.Architecture = arch || '';
|
this.Architecture = arch || '';
|
||||||
|
@ -6,6 +6,7 @@ export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageI
|
||||||
this.ImageDigest = imageDigest || '';
|
this.ImageDigest = imageDigest || '';
|
||||||
this.ImageId = imageId || '';
|
this.ImageId = imageId || '';
|
||||||
this.ManifestV2 = v2 || {};
|
this.ManifestV2 = v2 || {};
|
||||||
|
this.History = history || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
|
export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
|
||||||
|
@ -13,4 +14,9 @@ export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
|
||||||
this.ImageId = imageId;
|
this.ImageId = imageId;
|
||||||
this.ImageDigest = imageDigest;
|
this.ImageDigest = imageDigest;
|
||||||
this.ManifestV2 = manifest;
|
this.ManifestV2 = manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RepositoryAddTagPayload(tag, manifest) {
|
||||||
|
this.Tag = tag;
|
||||||
|
this.Manifest = manifest;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import gitlabResponseGetLink from './transform/gitlabResponseGetLink'
|
||||||
|
|
||||||
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
|
.factory('Gitlab', ['$resource', 'API_ENDPOINT_REGISTRIES',
|
||||||
|
function GitlabFactory($resource, API_ENDPOINT_REGISTRIES) {
|
||||||
|
'use strict';
|
||||||
|
return function(env) {
|
||||||
|
const headers = {};
|
||||||
|
if (env) {
|
||||||
|
headers['Private-Token'] = env.token;
|
||||||
|
headers['X-Gitlab-Domain'] = env.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = API_ENDPOINT_REGISTRIES + '/:id/proxies/gitlab/api/v4/projects';
|
||||||
|
|
||||||
|
return $resource(baseUrl, {id:'@id'},
|
||||||
|
{
|
||||||
|
projects: {
|
||||||
|
method: 'GET',
|
||||||
|
params: { membership: 'true' },
|
||||||
|
transformResponse: gitlabResponseGetLink,
|
||||||
|
headers: headers
|
||||||
|
},
|
||||||
|
repositories :{
|
||||||
|
method: 'GET',
|
||||||
|
url: baseUrl + '/:projectId/registry/repositories',
|
||||||
|
params: { tags: true },
|
||||||
|
headers: headers,
|
||||||
|
transformResponse: gitlabResponseGetLink
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default function gitlabResponseGetLink(data, headers) {
|
||||||
|
let response = {};
|
||||||
|
try {
|
||||||
|
response.data = angular.fromJson(data);
|
||||||
|
response.next = headers('X-Next-Page');
|
||||||
|
} catch (error) {
|
||||||
|
response = data;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
import { RegistryGitlabProject } from '../models/gitlabRegistry';
|
||||||
|
import { RegistryRepositoryGitlabViewModel } from '../models/registryRepository';
|
||||||
|
|
||||||
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
|
.factory('RegistryGitlabService', ['$async', 'Gitlab',
|
||||||
|
function RegistryGitlabServiceFactory($async, Gitlab) {
|
||||||
|
'use strict';
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PROJECTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function _getProjectsPage(env, params, projects) {
|
||||||
|
const response = await Gitlab(env).projects(params).$promise;
|
||||||
|
projects = _.concat(projects, response.data);
|
||||||
|
if (response.next) {
|
||||||
|
params.page = response.next;
|
||||||
|
projects = await _getProjectsPage(env, params, projects);
|
||||||
|
}
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function projectsAsync(url, token) {
|
||||||
|
try {
|
||||||
|
const data = await _getProjectsPage({url: url, token: token}, {page: 1}, []);
|
||||||
|
return _.map(data, (project) => new RegistryGitlabProject(project));
|
||||||
|
} catch (error) {
|
||||||
|
throw {msg: 'Unable to retrieve projects', err: error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* END PROJECTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REPOSITORIES
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function _getRepositoriesPage(params, repositories) {
|
||||||
|
const response = await Gitlab().repositories(params).$promise;
|
||||||
|
repositories = _.concat(repositories, response.data);
|
||||||
|
if (response.next) {
|
||||||
|
params.page = response.next;
|
||||||
|
repositories = await _getRepositoriesPage(params, repositories);
|
||||||
|
}
|
||||||
|
return repositories;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repositoriesAsync(registry) {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
id: registry.Id,
|
||||||
|
projectId: registry.Gitlab.ProjectId,
|
||||||
|
page: 1
|
||||||
|
};
|
||||||
|
const data = await _getRepositoriesPage(params, []);
|
||||||
|
return _.map(data, (r) => new RegistryRepositoryGitlabViewModel(r));
|
||||||
|
} catch (error) {
|
||||||
|
throw {msg: 'Unable to retrieve repositories', err: error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* END REPOSITORIES
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SERVICE FUNCTIONS DECLARATION
|
||||||
|
*/
|
||||||
|
|
||||||
|
function projects(url, token) {
|
||||||
|
return $async(projectsAsync, url, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositories(registry) {
|
||||||
|
return $async(repositoriesAsync, registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
service.projects = projects;
|
||||||
|
service.repositories = repositories;
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
]);
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
|
||||||
|
|
||||||
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
|
.factory('RegistryServiceSelector', ['$q', 'RegistryV2Service', 'RegistryGitlabService',
|
||||||
|
function RegistryServiceSelector($q, RegistryV2Service, RegistryGitlabService) {
|
||||||
|
'use strict';
|
||||||
|
const service = {};
|
||||||
|
|
||||||
|
service.ping = ping;
|
||||||
|
service.repositories = repositories;
|
||||||
|
service.getRepositoriesDetails = getRepositoriesDetails;
|
||||||
|
service.tags = tags;
|
||||||
|
service.getTagsDetails = getTagsDetails;
|
||||||
|
service.tag = tag;
|
||||||
|
service.addTag = addTag;
|
||||||
|
service.deleteManifest = deleteManifest;
|
||||||
|
|
||||||
|
service.shortTagsWithProgress = shortTagsWithProgress;
|
||||||
|
service.deleteTagsWithProgress = deleteTagsWithProgress;
|
||||||
|
service.retagWithProgress = retagWithProgress;
|
||||||
|
|
||||||
|
function ping(registry, forceNewConfig) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.ping(registry, forceNewConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositories(registry) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
if (registry.Type === RegistryTypes.GITLAB) {
|
||||||
|
service = RegistryGitlabService;
|
||||||
|
}
|
||||||
|
return service.repositories(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepositoriesDetails(registry, repositories) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.getRepositoriesDetails(registry, repositories);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tags(registry, repository) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.tags(registry, repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagsDetails(registry, repository, tags) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.getTagsDetails(registry, repository, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag(registry, repository, tag) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.tag(registry, repository, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag(registry, repository, tag, manifest) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.addTag(registry, repository, tag, manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteManifest(registry, repository, digest) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.deleteManifest(registry, repository, digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortTagsWithProgress(registry, repository, tagsList) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.shortTagsWithProgress(registry, repository, tagsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
function retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) {
|
||||||
|
let service = RegistryV2Service;
|
||||||
|
return service.retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
]);
|
|
@ -1,6 +1,7 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { RepositoryShortTag } from '../models/repositoryTag';
|
import { RepositoryShortTag } from '../models/repositoryTag';
|
||||||
import RegistryRepositoryViewModel from '../models/registryRepository';
|
import { RepositoryAddTagPayload } from '../models/repositoryTag'
|
||||||
|
import { RegistryRepositoryViewModel } from '../models/registryRepository';
|
||||||
import genericAsyncGenerator from './genericAsyncGenerator';
|
import genericAsyncGenerator from './genericAsyncGenerator';
|
||||||
|
|
||||||
angular.module('portainer.extensions.registrymanagement')
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
|
@ -9,12 +10,24 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.ping = function(id, forceNewConfig) {
|
/**
|
||||||
|
* PING
|
||||||
|
*/
|
||||||
|
function ping(registry, forceNewConfig) {
|
||||||
|
const id = registry.Id;
|
||||||
if (forceNewConfig) {
|
if (forceNewConfig) {
|
||||||
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
|
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
|
||||||
}
|
}
|
||||||
return RegistryCatalog.ping({ id: id }).$promise;
|
return RegistryCatalog.ping({ id: id }).$promise;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* END PING
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REPOSITORIES
|
||||||
|
*/
|
||||||
|
|
||||||
function _getCatalogPage(params, deferred, repositories) {
|
function _getCatalogPage(params, deferred, repositories) {
|
||||||
RegistryCatalog.get(params).$promise.then(function(data) {
|
RegistryCatalog.get(params).$promise.then(function(data) {
|
||||||
|
@ -27,7 +40,7 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCatalog(id) {
|
function _getCatalog(id) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
var repositories = [];
|
var repositories = [];
|
||||||
|
|
||||||
|
@ -35,13 +48,12 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
service.catalog = function (id) {
|
function repositories(registry) {
|
||||||
var deferred = $q.defer();
|
const deferred = $q.defer();
|
||||||
|
const id = registry.Id;
|
||||||
|
|
||||||
getCatalog(id).then(function success(data) {
|
_getCatalog(id).then(function success(data) {
|
||||||
var repositories = data.map(function (repositoryName) {
|
const repositories = _.map(data, (repositoryName) => new RegistryRepositoryViewModel(repositoryName));
|
||||||
return new RegistryRepositoryViewModel(repositoryName);
|
|
||||||
});
|
|
||||||
deferred.resolve(repositories);
|
deferred.resolve(repositories);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -52,14 +64,37 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
});
|
});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
}
|
||||||
|
|
||||||
service.tags = function (id, repository) {
|
function getRepositoriesDetails(registry, repositories) {
|
||||||
var deferred = $q.defer();
|
const deferred = $q.defer();
|
||||||
|
const promises = _.map(repositories, (repository) => tags(registry, repository.Name));
|
||||||
|
|
||||||
|
$q.all(promises)
|
||||||
|
.then(function success(data) {
|
||||||
|
var repositories = data.map(function (item) {
|
||||||
|
return new RegistryRepositoryViewModel(item);
|
||||||
|
});
|
||||||
|
repositories = _.without(repositories, undefined);
|
||||||
|
deferred.resolve(repositories);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({
|
||||||
|
msg: 'Unable to retrieve repositories',
|
||||||
|
err: err
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
_getTagsPage({id: id, repository: repository}, deferred, {tags:[]});
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* END REPOSITORIES
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TAGS
|
||||||
|
*/
|
||||||
|
|
||||||
function _getTagsPage(params, deferred, previousTags) {
|
function _getTagsPage(params, deferred, previousTags) {
|
||||||
RegistryTags.get(params).$promise.then(function(data) {
|
RegistryTags.get(params).$promise.then(function(data) {
|
||||||
|
@ -78,45 +113,23 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
service.getRepositoriesDetails = function (id, repositories) {
|
function tags(registry, repository) {
|
||||||
var deferred = $q.defer();
|
const deferred = $q.defer();
|
||||||
var promises = [];
|
const id = registry.Id;
|
||||||
for (var i = 0; i < repositories.length; i++) {
|
|
||||||
var repository = repositories[i].Name;
|
|
||||||
promises.push(service.tags(id, repository));
|
|
||||||
}
|
|
||||||
|
|
||||||
$q.all(promises)
|
|
||||||
.then(function success(data) {
|
|
||||||
var repositories = data.map(function (item) {
|
|
||||||
return new RegistryRepositoryViewModel(item);
|
|
||||||
});
|
|
||||||
repositories = _.without(repositories, undefined);
|
|
||||||
deferred.resolve(repositories);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
deferred.reject({
|
|
||||||
msg: 'Unable to retrieve repositories',
|
|
||||||
err: err
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
_getTagsPage({id: id, repository: repository}, deferred, {tags:[]});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
}
|
||||||
|
|
||||||
service.getTagsDetails = function (id, repository, tags) {
|
function getTagsDetails(registry, repository, tags) {
|
||||||
var promises = [];
|
const promises = _.map(tags, (t) => tag(registry, repository, t.Name));
|
||||||
|
|
||||||
for (var i = 0; i < tags.length; i++) {
|
|
||||||
var tag = tags[i].Name;
|
|
||||||
promises.push(service.tag(id, repository, tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $q.all(promises);
|
return $q.all(promises);
|
||||||
};
|
}
|
||||||
|
|
||||||
service.tag = function (id, repository, tag) {
|
function tag(registry, repository, tag) {
|
||||||
var deferred = $q.defer();
|
const deferred = $q.defer();
|
||||||
|
const id = registry.Id;
|
||||||
|
|
||||||
var promises = {
|
var promises = {
|
||||||
v1: RegistryManifestsJquery.get({
|
v1: RegistryManifestsJquery.get({
|
||||||
|
@ -142,35 +155,33 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
});
|
});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
}
|
||||||
|
|
||||||
service.addTag = function (id, repository, {tag, manifest}) {
|
/**
|
||||||
|
* END TAGS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADD TAG
|
||||||
|
*/
|
||||||
|
|
||||||
|
// tag: RepositoryAddTagPayload
|
||||||
|
function _addTagFromGenerator(registry, repository, tag) {
|
||||||
|
return addTag(registry, repository, tag.Tag, tag.Manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag(registry, repository, tag, manifest) {
|
||||||
|
const id = registry.Id;
|
||||||
delete manifest.digest;
|
delete manifest.digest;
|
||||||
return RegistryManifestsJquery.put({
|
return RegistryManifestsJquery.put({
|
||||||
id: id,
|
id: id,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
tag: tag
|
tag: tag
|
||||||
}, manifest);
|
}, manifest);
|
||||||
};
|
}
|
||||||
|
|
||||||
service.deleteManifest = function (id, repository, imageDigest) {
|
async function* _addTagsWithProgress(registry, repository, tagsList, progression = 0) {
|
||||||
return RegistryManifestsJquery.delete({
|
for await (const partialResult of genericAsyncGenerator($q, tagsList, _addTagFromGenerator, [registry, repository])) {
|
||||||
id: id,
|
|
||||||
repository: repository,
|
|
||||||
tag: imageDigest
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.shortTag = function(id, repository, tag) {
|
|
||||||
return new Promise ((resolve, reject) => {
|
|
||||||
RegistryManifestsJquery.getV2({id:id, repository: repository, tag: tag})
|
|
||||||
.then((data) => resolve(new RepositoryShortTag(tag, data.config.digest, data.digest, data)))
|
|
||||||
.catch((err) => reject(err))
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
async function* addTagsWithProgress(id, repository, tagsList, progression = 0) {
|
|
||||||
for await (const partialResult of genericAsyncGenerator($q, tagsList, service.addTag, [id, repository])) {
|
|
||||||
if (typeof partialResult === 'number') {
|
if (typeof partialResult === 'number') {
|
||||||
yield progression + partialResult;
|
yield progression + partialResult;
|
||||||
} else {
|
} else {
|
||||||
|
@ -179,36 +190,110 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.shortTagsWithProgress = async function* (id, repository, tagsList) {
|
/**
|
||||||
yield* genericAsyncGenerator($q, tagsList, service.shortTag, [id, repository]);
|
* END ADD TAG
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE MANIFEST
|
||||||
|
*/
|
||||||
|
|
||||||
|
function deleteManifest(registry, repository, imageDigest) {
|
||||||
|
const id = registry.Id;
|
||||||
|
return RegistryManifestsJquery.delete({
|
||||||
|
id: id,
|
||||||
|
repository: repository,
|
||||||
|
tag: imageDigest
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function* deleteManifestsWithProgress(id, repository, manifests) {
|
async function* _deleteManifestsWithProgress(registry, repository, manifests) {
|
||||||
for await (const partialResult of genericAsyncGenerator($q, manifests, service.deleteManifest, [id, repository])) {
|
for await (const partialResult of genericAsyncGenerator($q, manifests, deleteManifest, [registry, repository])) {
|
||||||
yield partialResult;
|
yield partialResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.retagWithProgress = async function* (id, repository, modifiedTags, modifiedDigests, impactedTags){
|
/**
|
||||||
yield* deleteManifestsWithProgress(id, repository, modifiedDigests);
|
* END DELETE MANIFEST
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHORT TAG
|
||||||
|
*/
|
||||||
|
|
||||||
|
function _shortTagFromGenerator(id, repository, tag) {
|
||||||
|
return new Promise ((resolve, reject) => {
|
||||||
|
RegistryManifestsJquery.getV2({id:id, repository: repository, tag: tag})
|
||||||
|
.then((data) => resolve(new RepositoryShortTag(tag, data.config.digest, data.digest, data)))
|
||||||
|
.catch((err) => reject(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* shortTagsWithProgress(registry, repository, tagsList) {
|
||||||
|
const id = registry.Id;
|
||||||
|
yield* genericAsyncGenerator($q, tagsList, _shortTagFromGenerator, [id, repository]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* END SHORT TAG
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RETAG
|
||||||
|
*/
|
||||||
|
async function* retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags){
|
||||||
|
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
|
||||||
|
|
||||||
const newTags = _.map(impactedTags, (item) => {
|
const newTags = _.map(impactedTags, (item) => {
|
||||||
const tagFromTable = _.find(modifiedTags, { 'Name': item.Name });
|
const tagFromTable = _.find(modifiedTags, { 'Name': item.Name });
|
||||||
const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name;
|
const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name;
|
||||||
return { tag: name, manifest: item.ManifestV2 };
|
return new RepositoryAddTagPayload(name, item.ManifestV2);
|
||||||
});
|
});
|
||||||
|
|
||||||
yield* addTagsWithProgress(id, repository, newTags, modifiedDigests.length);
|
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
service.deleteTagsWithProgress = async function* (id, repository, modifiedDigests, impactedTags) {
|
/**
|
||||||
yield* deleteManifestsWithProgress(id, repository, modifiedDigests);
|
* END RETAG
|
||||||
|
*/
|
||||||
|
|
||||||
const newTags = _.map(impactedTags, (item) => {return {tag: item.Name, manifest: item.ManifestV2}})
|
/**
|
||||||
|
* DELETE TAGS
|
||||||
|
*/
|
||||||
|
|
||||||
yield* addTagsWithProgress(id, repository, newTags, modifiedDigests.length);
|
async function* deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
|
||||||
|
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
|
||||||
|
|
||||||
|
const newTags = _.map(impactedTags, (item) => new RepositoryAddTagPayload(item.Name, item.ManifestV2));
|
||||||
|
|
||||||
|
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* END DELETE TAGS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SERVICE FUNCTIONS DECLARATION
|
||||||
|
*/
|
||||||
|
|
||||||
|
service.ping = ping;
|
||||||
|
|
||||||
|
service.repositories = repositories;
|
||||||
|
service.getRepositoriesDetails = getRepositoriesDetails;
|
||||||
|
|
||||||
|
service.tags = tags;
|
||||||
|
service.tag = tag;
|
||||||
|
service.getTagsDetails = getTagsDetails;
|
||||||
|
|
||||||
|
service.shortTagsWithProgress = shortTagsWithProgress;
|
||||||
|
|
||||||
|
service.addTag = addTag;
|
||||||
|
service.deleteManifest = deleteManifest;
|
||||||
|
|
||||||
|
service.deleteTagsWithProgress = deleteTagsWithProgress;
|
||||||
|
service.retagWithProgress = retagWithProgress;
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { RegistryManagementConfigurationDefaultModel } from '../../../../portainer/models/registry';
|
import { RegistryManagementConfigurationDefaultModel } from '../../../../portainer/models/registry';
|
||||||
|
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
|
||||||
|
|
||||||
angular.module('portainer.extensions.registrymanagement')
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
.controller('ConfigureRegistryController', ['$scope', '$state', '$transition$', 'RegistryService', 'RegistryV2Service', 'Notifications',
|
.controller('ConfigureRegistryController', ['$scope', '$state', '$transition$', 'RegistryService', 'RegistryServiceSelector', 'Notifications',
|
||||||
function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Notifications) {
|
function ($scope, $state, $transition$, RegistryService, RegistryServiceSelector, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
testInProgress: false,
|
testInProgress: false,
|
||||||
|
@ -18,7 +19,7 @@ function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Noti
|
||||||
|
|
||||||
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
|
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
return RegistryV2Service.ping($scope.registry.Id, true);
|
return RegistryServiceSelector.ping($scope.registry, true);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Success', 'Valid management configuration');
|
Notifications.success('Success', 'Valid management configuration');
|
||||||
|
@ -50,6 +51,7 @@ function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Noti
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
var registryId = $transition$.params().id;
|
var registryId = $transition$.params().id;
|
||||||
|
$scope.RegistryTypes = RegistryTypes;
|
||||||
|
|
||||||
RegistryService.registry(registryId)
|
RegistryService.registry(registryId)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !registry-url-input -->
|
<!-- !registry-url-input -->
|
||||||
<!-- authentication-checkbox -->
|
<!-- authentication-checkbox -->
|
||||||
<div class="form-group" ng-if="registry.Type === 3">
|
<div class="form-group" ng-if="registry.Type === RegistryTypes.CUSTOM || registry.Type === RegistryTypes.GITLAB">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="registry_auth" class="control-label text-left">
|
<label for="registry_auth" class="control-label text-left">
|
||||||
Authentication
|
Authentication
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !authentication-credentials -->
|
<!-- !authentication-credentials -->
|
||||||
<!-- tls -->
|
<!-- tls -->
|
||||||
<div ng-if="registry.Type === 3">
|
<div ng-if="registry.Type === RegistryTypes.CUSTOM || registry.Type === RegistryTypes.GITLAB">
|
||||||
<!-- tls-checkbox -->
|
<!-- tls-checkbox -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -48,6 +48,8 @@
|
||||||
<td>Repository</td>
|
<td>Repository</td>
|
||||||
<td>
|
<td>
|
||||||
{{ repository.Name }}
|
{{ repository.Name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<button class="btn btn-xs btn-danger" ng-if="!state.tagsRetrieval.running && state.tagsRetrieval.progression !== 0" ng-click="removeRepository()">
|
<button class="btn btn-xs btn-danger" ng-if="!state.tagsRetrieval.running && state.tagsRetrieval.progression !== 0" ng-click="removeRepository()">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this repository
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this repository
|
||||||
</button>
|
</button>
|
||||||
|
@ -56,10 +58,12 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tags count</td>
|
<td>Tags count</td>
|
||||||
<td>{{ repository.Tags.length }}</td>
|
<td>{{ repository.Tags.length }}</td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="short.Images.length">
|
<tr ng-if="short.Images.length">
|
||||||
<td>Images count</td>
|
<td>Images count</td>
|
||||||
<td>{{ short.Images.length }}</td>
|
<td>{{ short.Images.length }}</td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -2,8 +2,8 @@ import _ from 'lodash-es';
|
||||||
import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag';
|
import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('RegistryRepositoryController', ['$q', '$async', '$scope', '$uibModal', '$interval', '$transition$', '$state', 'RegistryV2Service', 'RegistryService', 'ModalService', 'Notifications', 'ImageHelper',
|
.controller('RegistryRepositoryController', ['$q', '$async', '$scope', '$uibModal', '$interval', '$transition$', '$state', 'RegistryServiceSelector', 'RegistryService', 'ModalService', 'Notifications', 'ImageHelper',
|
||||||
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryV2Service, RegistryService, ModalService, Notifications, ImageHelper) {
|
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryServiceSelector, RegistryService, ModalService, Notifications, ImageHelper) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
@ -63,7 +63,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
$scope.paginationAction = function (tags) {
|
$scope.paginationAction = function (tags) {
|
||||||
$scope.state.loading = true;
|
$scope.state.loading = true;
|
||||||
RegistryV2Service.getTagsDetails($scope.registryId, $scope.repository.Name, tags)
|
RegistryServiceSelector.getTagsDetails($scope.registry, $scope.repository.Name, tags)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
var idx = _.findIndex($scope.tags, {'Name': data[i].Name});
|
var idx = _.findIndex($scope.tags, {'Name': data[i].Name});
|
||||||
|
@ -86,7 +86,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
function createRetrieveAsyncGenerator() {
|
function createRetrieveAsyncGenerator() {
|
||||||
$scope.state.tagsRetrieval.asyncGenerator =
|
$scope.state.tagsRetrieval.asyncGenerator =
|
||||||
RegistryV2Service.shortTagsWithProgress($scope.registryId, $scope.repository.Name, $scope.repository.Tags);
|
RegistryServiceSelector.shortTagsWithProgress($scope.registry, $scope.repository.Name, $scope.repository.Tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetTagsRetrievalState() {
|
function resetTagsRetrievalState() {
|
||||||
|
@ -151,7 +151,7 @@ angular.module('portainer.app')
|
||||||
}
|
}
|
||||||
const tag = $scope.short.Tags.find((item) => item.ImageId === $scope.formValues.SelectedImage);
|
const tag = $scope.short.Tags.find((item) => item.ImageId === $scope.formValues.SelectedImage);
|
||||||
const manifest = tag.ManifestV2;
|
const manifest = tag.ManifestV2;
|
||||||
await RegistryV2Service.addTag($scope.registryId, $scope.repository.Name, {tag: $scope.formValues.Tag, manifest: manifest})
|
await RegistryServiceSelector.addTag($scope.registry, $scope.repository.Name, $scope.formValues.Tag, manifest)
|
||||||
|
|
||||||
Notifications.success('Success', 'Tag successfully added');
|
Notifications.success('Success', 'Tag successfully added');
|
||||||
$scope.short.Tags.push(new RepositoryShortTag($scope.formValues.Tag, tag.ImageId, tag.ImageDigest, tag.ManifestV2));
|
$scope.short.Tags.push(new RepositoryShortTag($scope.formValues.Tag, tag.ImageId, tag.ImageDigest, tag.ManifestV2));
|
||||||
|
@ -182,7 +182,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
|
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
|
||||||
$scope.state.tagsRetag.asyncGenerator =
|
$scope.state.tagsRetag.asyncGenerator =
|
||||||
RegistryV2Service.retagWithProgress($scope.registryId, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags);
|
RegistryServiceSelector.retagWithProgress($scope.registry, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retagActionAsync() {
|
async function retagActionAsync() {
|
||||||
|
@ -252,7 +252,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
function createDeleteAsyncGenerator(modifiedDigests, impactedTags) {
|
function createDeleteAsyncGenerator(modifiedDigests, impactedTags) {
|
||||||
$scope.state.tagsDelete.asyncGenerator =
|
$scope.state.tagsDelete.asyncGenerator =
|
||||||
RegistryV2Service.deleteTagsWithProgress($scope.registryId, $scope.repository.Name, modifiedDigests, impactedTags);
|
RegistryServiceSelector.deleteTagsWithProgress($scope.registry, $scope.repository.Name, modifiedDigests, impactedTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeTagsAsync(selectedTags) {
|
async function removeTagsAsync(selectedTags) {
|
||||||
|
@ -289,7 +289,7 @@ angular.module('portainer.app')
|
||||||
Notifications.success('Success', 'Tags successfully deleted');
|
Notifications.success('Success', 'Tags successfully deleted');
|
||||||
|
|
||||||
if ($scope.short.Tags.length === 0) {
|
if ($scope.short.Tags.length === 0) {
|
||||||
$state.go('portainer.registries.registry.repositories', {id: $scope.registryId}, {reload: true});
|
$state.go('portainer.registries.registry.repositories', {id: $scope.registry.Id}, {reload: true});
|
||||||
}
|
}
|
||||||
await loadRepositoryDetails();
|
await loadRepositoryDetails();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -322,10 +322,10 @@ angular.module('portainer.app')
|
||||||
try {
|
try {
|
||||||
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
|
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
|
||||||
const promises = [];
|
const promises = [];
|
||||||
_.map(digests, (item) => promises.push(RegistryV2Service.deleteManifest($scope.registryId, $scope.repository.Name, item.ImageDigest)));
|
_.map(digests, (item) => promises.push(RegistryServiceSelector.deleteManifest($scope.registry, $scope.repository.Name, item.ImageDigest)));
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
Notifications.success('Success', 'Repository sucessfully removed');
|
Notifications.success('Success', 'Repository sucessfully removed');
|
||||||
$state.go('portainer.registries.registry.repositories', {id: $scope.registryId}, {reload: true});
|
$state.go('portainer.registries.registry.repositories', {id: $scope.registry.Id}, {reload: true});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notifications.error('Failure', err, 'Unable to delete repository');
|
Notifications.error('Failure', err, 'Unable to delete repository');
|
||||||
}
|
}
|
||||||
|
@ -351,9 +351,9 @@ angular.module('portainer.app')
|
||||||
*/
|
*/
|
||||||
async function loadRepositoryDetails() {
|
async function loadRepositoryDetails() {
|
||||||
try {
|
try {
|
||||||
const registryId = $scope.registryId;
|
const registry = $scope.registry;
|
||||||
const repository = $scope.repository.Name;
|
const repository = $scope.repository.Name;
|
||||||
const tags = await RegistryV2Service.tags(registryId, repository);
|
const tags = await RegistryServiceSelector.tags(registry, repository);
|
||||||
$scope.tags = [];
|
$scope.tags = [];
|
||||||
$scope.repository.Tags = [];
|
$scope.repository.Tags = [];
|
||||||
$scope.repository.Tags = _.sortBy(_.concat($scope.repository.Tags, _.without(tags.tags, null)));
|
$scope.repository.Tags = _.sortBy(_.concat($scope.repository.Tags, _.without(tags.tags, null)));
|
||||||
|
@ -365,7 +365,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
async function initView() {
|
async function initView() {
|
||||||
try {
|
try {
|
||||||
const registryId = $scope.registryId = $transition$.params().id;
|
const registryId = $transition$.params().id;
|
||||||
$scope.repository.Name = $transition$.params().repository;
|
$scope.repository.Name = $transition$.params().repository;
|
||||||
$scope.state.loading = true;
|
$scope.state.loading = true;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
|
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
|
||||||
|
|
||||||
angular.module('portainer.extensions.registrymanagement')
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
|
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryServiceSelector', 'Notifications', 'Authentication',
|
||||||
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
|
function ($transition$, $scope, RegistryService, RegistryServiceSelector, Notifications, Authentication) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
displayInvalidConfigurationMessage: false,
|
displayInvalidConfigurationMessage: false,
|
||||||
|
@ -10,8 +12,11 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.paginationAction = function (repositories) {
|
$scope.paginationAction = function (repositories) {
|
||||||
|
if ($scope.registry.Type === RegistryTypes.GITLAB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$scope.state.loading = true;
|
$scope.state.loading = true;
|
||||||
RegistryV2Service.getRepositoriesDetails($scope.state.registryId, repositories)
|
RegistryServiceSelector.getRepositoriesDetails($scope.registry, repositories)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
var idx = _.findIndex($scope.repositories, {'Name': data[i].Name});
|
var idx = _.findIndex($scope.repositories, {'Name': data[i].Name});
|
||||||
|
@ -30,20 +35,19 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.state.registryId = $transition$.params().id;
|
const registryId = $transition$.params().id;
|
||||||
|
|
||||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
if (authenticationEnabled) {
|
if (authenticationEnabled) {
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
RegistryService.registry($scope.state.registryId)
|
RegistryService.registry(registryId)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.registry = data;
|
$scope.registry = data;
|
||||||
|
RegistryServiceSelector.ping($scope.registry, false)
|
||||||
RegistryV2Service.ping($scope.state.registryId, false)
|
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
return RegistryV2Service.catalog($scope.state.registryId);
|
return RegistryServiceSelector.repositories($scope.registry);
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.repositories = data;
|
$scope.repositories = data;
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { RegistryImageDetailsViewModel } from 'Extensions/registry-management/mo
|
||||||
class RegistryRepositoryTagController {
|
class RegistryRepositoryTagController {
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($transition$, $async, Notifications, RegistryService, RegistryV2Service, imagelayercommandFilter) {
|
constructor($transition$, $async, Notifications, RegistryService, RegistryServiceSelector, imagelayercommandFilter) {
|
||||||
this.$transition$ = $transition$;
|
this.$transition$ = $transition$;
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.RegistryService = RegistryService;
|
this.RegistryService = RegistryService;
|
||||||
this.RegistryV2Service = RegistryV2Service;
|
this.RegistryServiceSelector = RegistryServiceSelector;
|
||||||
this.imagelayercommandFilter = imagelayercommandFilter;
|
this.imagelayercommandFilter = imagelayercommandFilter;
|
||||||
|
|
||||||
this.context = {};
|
this.context = {};
|
||||||
|
@ -39,7 +39,7 @@ class RegistryRepositoryTagController {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.registry = await this.RegistryService.registry(this.context.registryId);
|
this.registry = await this.RegistryService.registry(this.context.registryId);
|
||||||
this.tag = await this.RegistryV2Service.tag(this.context.registryId, this.context.repository, this.context.tag);
|
this.tag = await this.RegistryServiceSelector.tag(this.registry, this.context.repository, this.context.tag);
|
||||||
const length = this.tag.History.length;
|
const length = this.tag.History.length;
|
||||||
this.history = _.map(this.tag.History, (layer, idx) => new RegistryImageLayerViewModel(length - idx, layer));
|
this.history = _.map(this.tag.History, (layer, idx) => new RegistryImageLayerViewModel(length - idx, layer));
|
||||||
_.forEach(this.history, (item) => item.CreatedBy = this.imagelayercommandFilter(item.CreatedBy))
|
_.forEach(this.history, (item) => item.CreatedBy = this.imagelayercommandFilter(item.CreatedBy))
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
<!-- url-input -->
|
<!-- url-input -->
|
||||||
<div class="form-group" ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
|
<div class="form-group">
|
||||||
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
Registry URL
|
Registry URL
|
||||||
<portainer-tooltip position="bottom" message="URL of an Azure Container Registry. Any protocol will be stripped."></portainer-tooltip>
|
<portainer-tooltip position="bottom" message="URL of an Azure Container Registry. Any protocol will be stripped."></portainer-tooltip>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
<!-- url-input -->
|
<!-- url-input -->
|
||||||
<div class="form-group" ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
|
<div class="form-group">
|
||||||
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
Registry URL
|
Registry URL
|
||||||
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>
|
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="toolBar">
|
||||||
|
<div class="toolBarTitle">
|
||||||
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="searchBar">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover nowrap-cells">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span class="md-checkbox">
|
||||||
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
|
<label for="select_all"></label>
|
||||||
|
</span>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Namespace')">
|
||||||
|
Namespace
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Name
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('PathWithNamespace')">
|
||||||
|
Path with namespace
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PathWithNamespace' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PathWithNamespace' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
|
<td>
|
||||||
|
<span class="md-checkbox">
|
||||||
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableSelection(item)"/>
|
||||||
|
<label for="select_{{ $index }}"></label>
|
||||||
|
</span>
|
||||||
|
{{ item.Namespace }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.Name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.PathWithNamespace }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.Description }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||||
|
<td colspan="3" class="text-center text-muted">No projects available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||||
|
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||||
|
</div>
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
|
@ -0,0 +1,13 @@
|
||||||
|
angular.module('portainer.app').component('gitlabProjectsDatatable', {
|
||||||
|
templateUrl: './gitlabProjectsDatatable.html',
|
||||||
|
controller: 'GitlabProjectsDatatableController',
|
||||||
|
bindings: {
|
||||||
|
titleText: '@',
|
||||||
|
titleIcon: '@',
|
||||||
|
dataset: '<',
|
||||||
|
tableKey: '@',
|
||||||
|
orderBy: '@',
|
||||||
|
reverseOrder: '<',
|
||||||
|
state: '='
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('GitlabProjectsDatatableController', ['$scope', '$controller', 'DatatableService',
|
||||||
|
function ($scope, $controller, DatatableService) {
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
|
this.disableSelection = function(item) {
|
||||||
|
return !this.allowSelection(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on RegistryGitlabProject model
|
||||||
|
this.allowSelection = function(item) {
|
||||||
|
return item.RegistryEnabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onInit = function() {
|
||||||
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
if (storedOrder !== null) {
|
||||||
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
|
@ -0,0 +1,139 @@
|
||||||
|
<form class="form-horizontal" name="registryFormGitlab" ng-submit="$ctrl.retrieveRegistries()">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Important notice
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
<p>
|
||||||
|
For information on how to generate a Gitlab Personal Access Token, follow the <a href="https://gitlab.com/help/user/profile/personal_access_tokens.md" target="_blank">gitlab guide</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> You must provide a token with <code>api</code> scope. Failure to do so will mean you can only push/pull from your registry but not manage it using the <a ui-sref="portainer.extensions.extension({id: 1})">registry management (extension)</a>.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Gitlab registry connection details
|
||||||
|
</div>
|
||||||
|
<!-- credentials-user -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<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="registryFormGitlab.registry_username.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormGitlab.registry_username.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !credentials-user -->
|
||||||
|
<!-- credentials-pat -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registry_perso_acc_token" class="col-sm-3 col-lg-2 control-label text-left">Personal Access Token
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="password" class="form-control" id="registry_perso_acc_token" name="registry_perso_acc_token" ng-model="$ctrl.model.Token" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormGitlab.registry_perso_acc_token.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormGitlab.registry_perso_acc_token.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !credentials-pat -->
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
|
||||||
|
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Override default configuration
|
||||||
|
</a>
|
||||||
|
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.resetDefaults()">
|
||||||
|
<i class="fa fa-cogs space-right" aria-hidden="true"></i> Use default configuration
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- url-input -->
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.overrideConfiguration">
|
||||||
|
<label for="instance_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Instance URL
|
||||||
|
<portainer-tooltip position="bottom" message="URL of Gitlab instance."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="instance_url" name="instance_url" ng-model="$ctrl.model.Gitlab.InstanceURL" placeholder="https://gitlab.com" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormGitlab.instance_url.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormGitlab.instance_url.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !url-input -->
|
||||||
|
<!-- url-input -->
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.overrideConfiguration">
|
||||||
|
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Registry URL
|
||||||
|
<portainer-tooltip position="bottom" message="URL of Gitlab registry instance."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="https://registry.gitlab.com" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="registryFormGitlab.registry_url.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="registryFormGitlab.registry_url.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !url-input -->
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormGitlab.$valid" button-spinner="$ctrl.actionInProgress">
|
||||||
|
<span ng-hide="$ctrl.actionInProgress">Retrieve projects</span>
|
||||||
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="form-horizontal" ng-if="$ctrl.projects">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Gitlab projects
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Select the project's registries you want to manage. Portainer will create one registry for each selected project.
|
||||||
|
</span>
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
If you can't select a project, make sure that registry feature is activated on it.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<gitlab-projects-datatable
|
||||||
|
title-text="Gitlab projects" title-icon="fa-project-diagram"
|
||||||
|
dataset="$ctrl.projects" table-key="gitlab_projects"
|
||||||
|
state="$ctrl.state.gitlab"
|
||||||
|
order-by="Name"
|
||||||
|
></gitlab-projects-datatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button ng-click="$ctrl.createRegistries()" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount" button-spinner="$ctrl.actionInProgress">
|
||||||
|
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
|
||||||
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
angular.module('portainer.app').component('registryFormGitlab', {
|
||||||
|
templateUrl: './registry-form-gitlab.html',
|
||||||
|
bindings: {
|
||||||
|
model: '=',
|
||||||
|
retrieveRegistries: '<',
|
||||||
|
createRegistries: '<',
|
||||||
|
actionInProgress: '<',
|
||||||
|
projects: '=',
|
||||||
|
state: '=',
|
||||||
|
resetDefaults: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,3 +1,6 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
|
||||||
|
|
||||||
export function RegistryViewModel(data) {
|
export function RegistryViewModel(data) {
|
||||||
this.Id = data.Id;
|
this.Id = data.Id;
|
||||||
this.Type = data.Type;
|
this.Type = data.Type;
|
||||||
|
@ -11,6 +14,7 @@ export function RegistryViewModel(data) {
|
||||||
this.UserAccessPolicies = data.UserAccessPolicies;
|
this.UserAccessPolicies = data.UserAccessPolicies;
|
||||||
this.TeamAccessPolicies = data.TeamAccessPolicies;
|
this.TeamAccessPolicies = data.TeamAccessPolicies;
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
|
this.Gitlab = data.Gitlab;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegistryManagementConfigurationDefaultModel(registry) {
|
export function RegistryManagementConfigurationDefaultModel(registry) {
|
||||||
|
@ -22,20 +26,20 @@ export function RegistryManagementConfigurationDefaultModel(registry) {
|
||||||
this.TLSCertFile = null;
|
this.TLSCertFile = null;
|
||||||
this.TLSKeyFile = null;
|
this.TLSKeyFile = null;
|
||||||
|
|
||||||
if (registry.Type === 1 || registry.Type === 2 ) {
|
if (registry.Type === RegistryTypes.QUAY || registry.Type === RegistryTypes.AZURE ) {
|
||||||
this.Authentication = true;
|
this.Authentication = true;
|
||||||
this.Username = registry.Username;
|
this.Username = registry.Username;
|
||||||
this.TLS = true;
|
this.TLS = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registry.Type === 3 && registry.Authentication) {
|
if (registry.Type === RegistryTypes.CUSTOM && registry.Authentication) {
|
||||||
this.Authentication = true;
|
this.Authentication = true;
|
||||||
this.Username = registry.Username;
|
this.Username = registry.Username;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegistryDefaultModel() {
|
export function RegistryDefaultModel() {
|
||||||
this.Type = 3;
|
this.Type = RegistryTypes.CUSTOM;
|
||||||
this.URL = '';
|
this.URL = '';
|
||||||
this.Name = '';
|
this.Name = '';
|
||||||
this.Authentication = false;
|
this.Authentication = false;
|
||||||
|
@ -46,10 +50,16 @@ export function RegistryDefaultModel() {
|
||||||
export function RegistryCreateRequest(model) {
|
export function RegistryCreateRequest(model) {
|
||||||
this.Name = model.Name;
|
this.Name = model.Name;
|
||||||
this.Type = model.Type;
|
this.Type = model.Type;
|
||||||
this.URL = model.URL;
|
this.URL = _.replace(model.URL, /^https?\:\/\//i, '');
|
||||||
this.Authentication = model.Authentication;
|
this.Authentication = model.Authentication;
|
||||||
if (model.Authentication) {
|
if (model.Authentication) {
|
||||||
this.Username = model.Username;
|
this.Username = model.Username;
|
||||||
this.Password = model.Password;
|
this.Password = model.Password;
|
||||||
}
|
}
|
||||||
}
|
if (model.Type === RegistryTypes.GITLAB) {
|
||||||
|
this.Gitlab = {
|
||||||
|
ProjectId: model.Gitlab.ProjectId,
|
||||||
|
InstanceURL: model.Gitlab.InstanceURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry';
|
import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
|
@ -65,6 +66,19 @@ angular.module('portainer.app')
|
||||||
return Registries.create(payload).$promise;
|
return Registries.create(payload).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.createGitlabRegistries = function(model, projects) {
|
||||||
|
const promises = [];
|
||||||
|
_.forEach(projects, (p) => {
|
||||||
|
const m = model;
|
||||||
|
m.Name = p.PathWithNamespace;
|
||||||
|
m.Gitlab.ProjectId = p.Id;
|
||||||
|
m.Password = m.Token;
|
||||||
|
const payload = new RegistryCreateRequest(m);
|
||||||
|
promises.push(Registries.create(payload).$promise);
|
||||||
|
});
|
||||||
|
return $q.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
service.retrieveRegistryFromRepository = function(repository) {
|
service.retrieveRegistryFromRepository = function(repository) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
import { RegistryDefaultModel } from '../../../models/registry';
|
import { RegistryDefaultModel } from '../../../models/registry';
|
||||||
|
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications',
|
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications', 'RegistryGitlabService', 'ExtensionService',
|
||||||
function ($scope, $state, RegistryService, Notifications) {
|
function ($scope, $state, RegistryService, Notifications, RegistryGitlabService, ExtensionService) {
|
||||||
|
|
||||||
$scope.selectQuayRegistry = selectQuayRegistry;
|
$scope.selectQuayRegistry = selectQuayRegistry;
|
||||||
$scope.selectAzureRegistry = selectAzureRegistry;
|
$scope.selectAzureRegistry = selectAzureRegistry;
|
||||||
$scope.selectCustomRegistry = selectCustomRegistry;
|
$scope.selectCustomRegistry = selectCustomRegistry;
|
||||||
|
$scope.selectGitlabRegistry = selectGitlabRegistry;
|
||||||
$scope.create = createRegistry;
|
$scope.create = createRegistry;
|
||||||
|
$scope.useDefaultGitlabConfiguration = useDefaultGitlabConfiguration;
|
||||||
|
$scope.retrieveGitlabRegistries = retrieveGitlabRegistries;
|
||||||
|
$scope.createGitlabRegistries = createGitlabRegistries;
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false,
|
||||||
|
overrideConfiguration: false,
|
||||||
|
gitlab: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectQuayRegistry() {
|
function selectQuayRegistry() {
|
||||||
|
@ -19,6 +26,18 @@ function ($scope, $state, RegistryService, Notifications) {
|
||||||
$scope.model.Authentication = true;
|
$scope.model.Authentication = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useDefaultGitlabConfiguration() {
|
||||||
|
$scope.model.URL = 'https://registry.gitlab.com';
|
||||||
|
$scope.model.Gitlab.InstanceURL = 'https://gitlab.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectGitlabRegistry() {
|
||||||
|
$scope.model.Name = '';
|
||||||
|
$scope.model.Authentication = true;
|
||||||
|
$scope.model.Gitlab = {};
|
||||||
|
useDefaultGitlabConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
function selectAzureRegistry() {
|
function selectAzureRegistry() {
|
||||||
$scope.model.Name = '';
|
$scope.model.Name = '';
|
||||||
$scope.model.URL = '';
|
$scope.model.URL = '';
|
||||||
|
@ -31,9 +50,32 @@ function ($scope, $state, RegistryService, Notifications) {
|
||||||
$scope.model.Authentication = false;
|
$scope.model.Authentication = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRegistry() {
|
function retrieveGitlabRegistries() {
|
||||||
$scope.model.URL = $scope.model.URL.replace(/^https?\:\/\//i, '');
|
$scope.state.actionInProgress = true;
|
||||||
|
RegistryGitlabService.projects($scope.model.Gitlab.InstanceURL, $scope.model.Token)
|
||||||
|
.then((data) => {
|
||||||
|
$scope.gitlabProjects = data;
|
||||||
|
}).catch((err) => {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve projects');
|
||||||
|
}).finally(() => {
|
||||||
|
$scope.state.actionInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGitlabRegistries() {
|
||||||
|
$scope.state.actionInProgress = true;
|
||||||
|
RegistryService.createGitlabRegistries($scope.model, $scope.state.gitlab.selectedItems)
|
||||||
|
.then(() => {
|
||||||
|
Notifications.success('Registries successfully created');
|
||||||
|
$state.go('portainer.registries');
|
||||||
|
}).catch((err) => {
|
||||||
|
Notifications.error('Failure', err, 'Unable to create registries');
|
||||||
|
}).finally(() => {
|
||||||
|
$scope.state.actionInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRegistry() {
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
RegistryService.createRegistry($scope.model)
|
RegistryService.createRegistry($scope.model)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
|
@ -49,7 +91,10 @@ function ($scope, $state, RegistryService, Notifications) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
|
$scope.RegistryTypes = RegistryTypes;
|
||||||
$scope.model = new RegistryDefaultModel();
|
$scope.model = new RegistryDefaultModel();
|
||||||
|
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.REGISTRY_MANAGEMENT)
|
||||||
|
.then((data) => $scope.registryExtensionEnabled = data);
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
|
|
||||||
<div class="form-group" style="margin-bottom: 0">
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
<div class="boxselector_wrapper">
|
<div class="boxselector_wrapper">
|
||||||
<div ng-click="selectQuayRegistry()">
|
<div>
|
||||||
<input type="radio" id="registry_quay" ng-model="model.Type" ng-value="1">
|
<input type="radio" id="registry_quay" ng-model="model.Type" ng-value="RegistryTypes.QUAY">
|
||||||
<label for="registry_quay">
|
<label for="registry_quay" ng-click="selectQuayRegistry()">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Quay.io
|
Quay.io
|
||||||
|
@ -28,9 +28,9 @@
|
||||||
<p>Quay container registry</p>
|
<p>Quay container registry</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ng-click="selectAzureRegistry()">
|
<div>
|
||||||
<input type="radio" id="registry_azure" ng-model="model.Type" ng-value="2">
|
<input type="radio" id="registry_azure" ng-model="model.Type" ng-value="RegistryTypes.AZURE">
|
||||||
<label for="registry_azure">
|
<label for="registry_azure" ng-click="selectAzureRegistry()">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Azure
|
Azure
|
||||||
|
@ -38,9 +38,19 @@
|
||||||
<p>Azure container registry</p>
|
<p>Azure container registry</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ng-click="selectCustomRegistry()">
|
<div>
|
||||||
<input type="radio" id="registry_custom" ng-model="model.Type" ng-value="3">
|
<input type="radio" id="registry_gitlab" ng-model="model.Type" ng-value="RegistryTypes.GITLAB">
|
||||||
<label for="registry_custom">
|
<label for="registry_gitlab" ng-click="selectGitlabRegistry()">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fab fa-gitlab" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Gitlab
|
||||||
|
</div>
|
||||||
|
<p>Gitlab container registry</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="registry_custom" ng-model="model.Type" ng-value="RegistryTypes.CUSTOM">
|
||||||
|
<label for="registry_custom" ng-click="selectCustomRegistry()">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Custom registry
|
Custom registry
|
||||||
|
@ -51,27 +61,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<registry-form-quay ng-if="model.Type === 1"
|
<registry-form-quay ng-if="model.Type === RegistryTypes.QUAY"
|
||||||
model="model"
|
model="model"
|
||||||
form-action="create"
|
form-action="create"
|
||||||
form-action-label="Add registry"
|
form-action-label="Add registry"
|
||||||
action-in-progress="state.actionInProgress"
|
action-in-progress="state.actionInProgress"
|
||||||
></registry-form-quay>
|
></registry-form-quay>
|
||||||
|
|
||||||
<registry-form-azure ng-if="model.Type === 2"
|
<registry-form-azure ng-if="model.Type === RegistryTypes.AZURE"
|
||||||
model="model"
|
model="model"
|
||||||
form-action="create"
|
form-action="create"
|
||||||
form-action-label="Add registry"
|
form-action-label="Add registry"
|
||||||
action-in-progress="state.actionInProgress"
|
action-in-progress="state.actionInProgress"
|
||||||
></registry-form-azure>
|
></registry-form-azure>
|
||||||
|
|
||||||
<registry-form-custom ng-if="model.Type === 3"
|
<registry-form-custom ng-if="model.Type === RegistryTypes.CUSTOM"
|
||||||
model="model"
|
model="model"
|
||||||
form-action="create"
|
form-action="create"
|
||||||
form-action-label="Add registry"
|
form-action-label="Add registry"
|
||||||
action-in-progress="state.actionInProgress"
|
action-in-progress="state.actionInProgress"
|
||||||
></registry-form-custom>
|
></registry-form-custom>
|
||||||
|
|
||||||
|
<registry-form-gitlab ng-if="model.Type === RegistryTypes.GITLAB"
|
||||||
|
model="model"
|
||||||
|
retrieve-registries="retrieveGitlabRegistries"
|
||||||
|
create-registries="createGitlabRegistries"
|
||||||
|
projects="gitlabProjects"
|
||||||
|
state="state"
|
||||||
|
action-in-progress="state.actionInProgress"
|
||||||
|
reset-defaults="useDefaultGitlabConfiguration"
|
||||||
|
></registry-form-gitlab>
|
||||||
</form>
|
</form>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
|
Loading…
Reference in New Issue