Merge remote-tracking branch 'origin/develop' into feat/EE-189/EE-248/support-automated-sync-for-stacks
This commit is contained in:
4
.vscode.example/settings.json
Normal file
4
.vscode.example/settings.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast", "-E", "exportloopref"]
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
@@ -187,6 +189,11 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func configureFilePaths(args []string, filePaths []string) []string {
|
||||
for _, path := range filePaths {
|
||||
args = append(args, "--compose-file", path)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
// @param search query string false "Search query"
|
||||
// @param groupId query int false "List endpoints of this group"
|
||||
// @param limit query int false "Limit results to this value"
|
||||
// @param type query int false "List endpoints of this type"
|
||||
// @param types query []int false "List endpoints of this type"
|
||||
// @param tagIds query []int false "search endpoints with these tags (depends on tagsPartialMatch)"
|
||||
// @param tagsPartialMatch query bool false "If true, will return endpoint which has one of tagIds, if false (or missing) will return only endpoints that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these endpoints"
|
||||
@@ -46,7 +46,9 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
endpointType, _ := request.RetrieveNumericQueryParameter(r, "type", true)
|
||||
|
||||
var endpointTypes []int
|
||||
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
|
||||
|
||||
var tagIDs []portainer.TagID
|
||||
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
|
||||
@@ -98,8 +100,8 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
|
||||
}
|
||||
|
||||
if endpointType != 0 {
|
||||
filteredEndpoints = filterEndpointsByType(filteredEndpoints, portainer.EndpointType(endpointType))
|
||||
if endpointTypes != nil {
|
||||
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, endpointTypes)
|
||||
}
|
||||
|
||||
if tagIDs != nil {
|
||||
@@ -212,11 +214,16 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
|
||||
return false
|
||||
}
|
||||
|
||||
func filterEndpointsByType(endpoints []portainer.Endpoint, endpointType portainer.EndpointType) []portainer.Endpoint {
|
||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointType := range endpointTypes {
|
||||
typeSet[portainer.EndpointType(endpointType)] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == endpointType {
|
||||
if typeSet[endpoint.Type] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,11 +151,17 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
|
||||
if payload.Kubernetes != nil {
|
||||
if payload.Kubernetes.Configuration.RestrictDefaultNamespace !=
|
||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
endpoint.Kubernetes = *payload.Kubernetes
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
||||
updateAuthorizations = true
|
||||
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||
|
||||
@@ -50,6 +50,8 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
||||
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
@@ -156,6 +158,8 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
||||
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||
@@ -296,6 +300,8 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
||||
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/stacks/{id}/git",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackGitUpdate))).Methods(http.MethodPost)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdateGit))).Methods(http.MethodPost)
|
||||
h.Handle("/stacks/{id}/git/redeploy",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackGitRedeploy))).Methods(http.MethodPut)
|
||||
h.Handle("/stacks/{id}/file",
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 " not found"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id} [delete]
|
||||
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 " not found"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/start [post]
|
||||
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 " not found"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/stop [post]
|
||||
func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -61,7 +61,7 @@ func (payload *updateSwarmStackPayload) Validate(r *http.Request) error {
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 " not found"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id} [put]
|
||||
func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -44,23 +44,24 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id Stacks
|
||||
// @summary Update and redeploy an existing stack (with Git config)
|
||||
// @description Update and redeploy an existing stack (with Git config)
|
||||
// @description **Access policy**: authenticated
|
||||
// @id StackUpdateGit
|
||||
// @summary Redeploy a stack
|
||||
// @description Pull and redeploy a stack via Git
|
||||
// @description **Access policy**: restricted
|
||||
// @tags stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Stack identifier"
|
||||
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
|
||||
// @param body body stackGitUpdatePayload true "Stack Git config"
|
||||
// @param body body updateStackGitPayload true "Git configs for pull and redeploy a stack"
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/git
|
||||
func (handler *Handler) stackGitUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// @router /stacks/{id}/git [put]
|
||||
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
|
||||
|
||||
@@ -34,7 +34,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
token, err := transport.getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
token, err := transport.getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string {
|
||||
return manager.adminToken
|
||||
}
|
||||
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) {
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) {
|
||||
manager.mutex.Lock()
|
||||
defer manager.mutex.Unlock()
|
||||
|
||||
@@ -61,7 +61,13 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, err
|
||||
teamIds = append(teamIds, int(membership.TeamID))
|
||||
}
|
||||
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds)
|
||||
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (transport *baseTransport) executeKubernetesRequest(request *http.Request)
|
||||
// #region ROUND TRIP
|
||||
|
||||
func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
token, err := transport.getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response
|
||||
return transport.proxyKubernetesRequest(request)
|
||||
}
|
||||
|
||||
func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
|
||||
func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -112,7 +112,7 @@ func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (strin
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
token = tokenManager.getAdminServiceAccountToken()
|
||||
} else {
|
||||
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed retrieving service account token: %v", err)
|
||||
return "", err
|
||||
|
||||
@@ -9,10 +9,6 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type (
|
||||
namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
|
||||
)
|
||||
|
||||
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
||||
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||
kcl.lock.Lock()
|
||||
@@ -48,18 +44,8 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
|
||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
|
||||
|
||||
var accessPolicies namespaceAccessPolicies
|
||||
err = json.Unmarshal([]byte(accessData), &accessPolicies)
|
||||
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string, restrictDefaultNamespace bool) error {
|
||||
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,20 +56,16 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service
|
||||
}
|
||||
|
||||
for _, namespace := range namespaces.Items {
|
||||
if namespace.Name == defaultNamespace {
|
||||
continue
|
||||
}
|
||||
|
||||
policies, ok := accessPolicies[namespace.Name]
|
||||
if !ok {
|
||||
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
|
||||
if namespace.Name == defaultNamespace && !restrictDefaultNamespace {
|
||||
err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasUserAccessToNamespace(userID, teamIDs, policies) {
|
||||
policies, ok := accessPolicies[namespace.Name]
|
||||
if !ok || !hasUserAccessToNamespace(userID, teamIDs, policies) {
|
||||
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -17,7 +17,7 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error)
|
||||
// SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes
|
||||
// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user.
|
||||
//It will also create required default RoleBinding and ClusterRoleBinding rules.
|
||||
func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error {
|
||||
func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error {
|
||||
serviceAccountName := userServiceAccountName(userID, kcl.instanceID)
|
||||
|
||||
err := kcl.ensureRequiredResourcesExist()
|
||||
@@ -25,20 +25,7 @@ func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error
|
||||
return err
|
||||
}
|
||||
|
||||
err = kcl.ensureServiceAccountForUserExists(serviceAccountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName)
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
|
||||
return kcl.createPortainerUserClusterRole()
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName string) error {
|
||||
err := kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
|
||||
err = kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,7 +40,11 @@ func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName stri
|
||||
return err
|
||||
}
|
||||
|
||||
return kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace)
|
||||
return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName, restrictDefaultNamespace)
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
|
||||
return kcl.createPortainerUserClusterRole()
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {
|
||||
|
||||
@@ -414,10 +414,11 @@ type (
|
||||
|
||||
// KubernetesConfiguration represents the configuration of a Kubernetes endpoint
|
||||
KubernetesConfiguration struct {
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
|
||||
}
|
||||
|
||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||
@@ -1182,7 +1183,7 @@ type (
|
||||
|
||||
// KubeClient represents a service used to query a Kubernetes environment
|
||||
KubeClient interface {
|
||||
SetupUserServiceAccount(userID int, teamIDs []int) error
|
||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||
@@ -1294,6 +1295,7 @@ type (
|
||||
Logout(endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
|
||||
Remove(stack *Stack, endpoint *Endpoint) error
|
||||
NormalizeStackName(name string) string
|
||||
}
|
||||
|
||||
// TagService represents a service for managing tag data
|
||||
|
||||
@@ -1003,6 +1003,8 @@ definitions:
|
||||
type: boolean
|
||||
UseServerMetrics:
|
||||
type: boolean
|
||||
RestrictDefaultNamespace:
|
||||
type: boolean
|
||||
type: object
|
||||
portainer.KubernetesData:
|
||||
properties:
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
<td ng-show="$ctrl.columnVisibility.columns.ports.display">
|
||||
<a
|
||||
ng-if="item.Ports.length > 0"
|
||||
ng-repeat="p in item.Ports"
|
||||
ng-repeat="p in item.Ports | unique: 'public'"
|
||||
class="image-tag"
|
||||
ng-href="http://{{ $ctrl.state.publicURL || p.host }}:{{ p.public }}"
|
||||
target="_blank"
|
||||
|
||||
@@ -322,4 +322,7 @@ angular
|
||||
}
|
||||
return fullName.substring(0, versionIdx);
|
||||
};
|
||||
})
|
||||
.filter('unique', function () {
|
||||
return _.uniqBy;
|
||||
});
|
||||
|
||||
@@ -53,33 +53,43 @@ function ImageHelperFactory() {
|
||||
*/
|
||||
export function buildImageFullURI(imageModel) {
|
||||
if (!imageModel.UseRegistry) {
|
||||
return imageModel.Image;
|
||||
return ensureTag(imageModel.Image);
|
||||
}
|
||||
|
||||
let fullImageName = '';
|
||||
const imageName = buildImageFullURIWithRegistry(imageModel);
|
||||
|
||||
return ensureTag(imageName);
|
||||
|
||||
function ensureTag(image, defaultTag = 'latest') {
|
||||
return image.includes(':') ? image : `${image}:${defaultTag}`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildImageFullURIWithRegistry(imageModel) {
|
||||
switch (imageModel.Registry.Type) {
|
||||
case RegistryTypes.GITLAB:
|
||||
fullImageName = imageModel.Registry.URL + '/' + imageModel.Registry.Gitlab.ProjectPath + (imageModel.Image.startsWith(':') ? '' : '/') + imageModel.Image;
|
||||
break;
|
||||
case RegistryTypes.ANONYMOUS:
|
||||
fullImageName = imageModel.Image;
|
||||
break;
|
||||
return buildImageURIForGitLab(imageModel);
|
||||
case RegistryTypes.QUAY:
|
||||
fullImageName =
|
||||
(imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '') +
|
||||
(imageModel.Registry.Quay.UseOrganisation ? imageModel.Registry.Quay.OrganisationName : imageModel.Registry.Username) +
|
||||
'/' +
|
||||
imageModel.Image;
|
||||
break;
|
||||
return buildImageURIForQuay(imageModel);
|
||||
case RegistryTypes.ANONYMOUS:
|
||||
return imageModel.Image;
|
||||
default:
|
||||
fullImageName = imageModel.Registry.URL + '/' + imageModel.Image;
|
||||
break;
|
||||
return buildImageURIForOtherRegistry(imageModel);
|
||||
}
|
||||
|
||||
if (!imageModel.Image.includes(':')) {
|
||||
fullImageName += ':latest';
|
||||
function buildImageURIForGitLab(imageModel) {
|
||||
const slash = imageModel.Image.startsWith(':') ? '' : '/';
|
||||
return `${imageModel.Registry.URL}/${imageModel.Registry.Gitlab.ProjectPath}${slash}${imageModel.Image}`;
|
||||
}
|
||||
|
||||
return fullImageName;
|
||||
function buildImageURIForQuay(imageModel) {
|
||||
const name = imageModel.Registry.Quay.UseOrganisation ? imageModel.Registry.Quay.OrganisationName : imageModel.Registry.Username;
|
||||
const url = imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '';
|
||||
return `${url}${name}/${imageModel.Image}`;
|
||||
}
|
||||
|
||||
function buildImageURIForOtherRegistry(imageModel) {
|
||||
const url = imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '';
|
||||
return url + imageModel.Image;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ angular.module('portainer.docker').controller('ImagesController', [
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
ImageService.pullImage(registryModel, false)
|
||||
.then(function success() {
|
||||
.then(function success(data) {
|
||||
var err = data[data.length - 1].errorDetail;
|
||||
if (err) {
|
||||
return Notifications.error('Failure', err, 'Unable to pull image');
|
||||
}
|
||||
Notifications.success('Image successfully pulled', registryModel.Image);
|
||||
$state.reload();
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ export class EdgeGroupFormController {
|
||||
async getDynamicEndpointsAsync() {
|
||||
const { pageNumber, limit, search } = this.endpoints.state;
|
||||
const start = (pageNumber - 1) * limit + 1;
|
||||
const query = { search, type: 4, tagIds: this.model.TagIds, tagsPartialMatch: this.model.PartialMatch };
|
||||
const query = { search, types: [4], tagIds: this.model.TagIds, tagsPartialMatch: this.model.PartialMatch };
|
||||
|
||||
const response = await this.EndpointService.endpoints(start, limit, query);
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ export class EditEdgeStackViewController {
|
||||
|
||||
async getPaginatedEndpointsAsync(lastId, limit, search) {
|
||||
try {
|
||||
const query = { search, type: 4, endpointIds: this.stackEndpointIds };
|
||||
const query = { search, types: [4], endpointIds: this.stackEndpointIds };
|
||||
const { value, totalCount } = await this.EndpointService.endpoints(lastId, limit, query);
|
||||
const endpoints = _.map(value, (endpoint) => {
|
||||
const status = this.stack.Status[endpoint.Id];
|
||||
|
||||
@@ -10,5 +10,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
refreshCallback: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,7 +18,11 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
|
||||
};
|
||||
|
||||
this.canManageAccess = function (item) {
|
||||
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
|
||||
if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) {
|
||||
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
|
||||
} else {
|
||||
return !this.isSystemNamespace(item);
|
||||
}
|
||||
};
|
||||
|
||||
this.disableRemove = function (item) {
|
||||
|
||||
@@ -145,11 +145,7 @@
|
||||
<label class="control-label text-left">
|
||||
Restrict access to the default namespace
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-default" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ class KubernetesConfigureController {
|
||||
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
|
||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
|
||||
}
|
||||
|
||||
transformFormValues() {
|
||||
@@ -259,6 +260,7 @@ class KubernetesConfigureController {
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
IngressClasses: [],
|
||||
RestrictDefaultNamespace: false,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -281,6 +283,7 @@ class KubernetesConfigureController {
|
||||
|
||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
|
||||
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
||||
ic.IsNew = false;
|
||||
ic.NeedsDeletion = false;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
order-by="Namespace.Name"
|
||||
remove-action="ctrl.removeAction"
|
||||
refresh-callback="ctrl.getResourcePools"
|
||||
endpoint="ctrl.endpoint"
|
||||
></kubernetes-resource-pools-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView',
|
||||
templateUrl: './resourcePools.html',
|
||||
controller: 'KubernetesResourcePoolsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ class AssoicatedEndpointsSelectorController {
|
||||
|
||||
async getEndpointsAsync() {
|
||||
const { start, search, limit } = this.getPaginationData('available');
|
||||
const query = { search, type: 4 };
|
||||
const query = { search, types: [4] };
|
||||
|
||||
const response = await this.EndpointService.endpoints(start, limit, query);
|
||||
|
||||
@@ -73,7 +73,7 @@ class AssoicatedEndpointsSelectorController {
|
||||
let response = { value: [], totalCount: 0 };
|
||||
if (this.endpointIds.length > 0) {
|
||||
const { start, search, limit } = this.getPaginationData('associated');
|
||||
const query = { search, type: 4, endpointIds: this.endpointIds };
|
||||
const query = { search, types: [4], endpointIds: this.endpointIds };
|
||||
|
||||
response = await this.EndpointService.endpoints(start, limit, query);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
||||
ctrl.duplicateStack = duplicateStack;
|
||||
ctrl.migrateStack = migrateStack;
|
||||
ctrl.isMigrationButtonDisabled = isMigrationButtonDisabled;
|
||||
ctrl.isEndpointSelected = isEndpointSelected;
|
||||
|
||||
function isFormValidForMigration() {
|
||||
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
|
||||
@@ -62,5 +63,9 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
||||
function isTargetEndpointAndCurrentEquals() {
|
||||
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id === ctrl.currentEndpointId;
|
||||
}
|
||||
|
||||
function isEndpointSelected() {
|
||||
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<span ng-hide="$ctrl.state.duplicationInProgress"> <i class="fa fa-clone space-right" aria-hidden="true"></i> Duplicate </span>
|
||||
<span ng-show="$ctrl.state.duplicationInProgress">Duplication in progress...</span>
|
||||
</button>
|
||||
<div ng-if="$ctrl.yamlError"
|
||||
<div ng-if="$ctrl.yamlError && $ctrl.isEndpointSelected()"
|
||||
><span class="text-danger small">{{ $ctrl.yamlError }}</span></div
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -17,11 +17,12 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
return Endpoints.get({ id: endpointID }).$promise;
|
||||
};
|
||||
|
||||
service.endpoints = function (start, limit, { search, type, tagIds, endpointIds, tagsPartialMatch } = {}) {
|
||||
service.endpoints = function (start, limit, { search, types, tagIds, endpointIds, tagsPartialMatch } = {}) {
|
||||
if (tagIds && !tagIds.length) {
|
||||
return Promise.resolve({ value: [], totalCount: 0 });
|
||||
}
|
||||
return Endpoints.query({ start, limit, search, type, tagIds: JSON.stringify(tagIds), endpointIds: JSON.stringify(endpointIds), tagsPartialMatch }).$promise;
|
||||
return Endpoints.query({ start, limit, search, types: JSON.stringify(types), tagIds: JSON.stringify(tagIds), endpointIds: JSON.stringify(endpointIds), tagsPartialMatch })
|
||||
.$promise;
|
||||
};
|
||||
|
||||
service.snapshotEndpoints = function () {
|
||||
|
||||
@@ -32,9 +32,12 @@
|
||||
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
|
||||
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client",
|
||||
"start:toolkit": "grunt start:toolkit",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
"clean:all": "grunt clean:all",
|
||||
"format": "prettier --loglevel warn --write \"**/*.{js,css,html}\""
|
||||
"format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"",
|
||||
"lint": "yarn lint:client; yarn lint:server",
|
||||
"lint:server": "cd api && golangci-lint run -E exportloopref",
|
||||
"lint:client": "eslint --cache --fix ."
|
||||
},
|
||||
"scriptsComments": {
|
||||
"build": "Build the entire app (backend/frontend) in development mode",
|
||||
@@ -172,4 +175,4 @@
|
||||
"*.js": "eslint --cache --fix",
|
||||
"*.{js,css,md,html}": "prettier --write"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user