Merge remote-tracking branch 'origin/develop' into feat/EE-189/EE-248/support-automated-sync-for-stacks

This commit is contained in:
Dmitry Salakhov
2021-07-26 17:25:30 +12:00
36 changed files with 155 additions and 107 deletions

View File

@@ -0,0 +1,4 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"]
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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",

View 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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -1003,6 +1003,8 @@ definitions:
type: boolean
UseServerMetrics:
type: boolean
RestrictDefaultNamespace:
type: boolean
type: object
portainer.KubernetesData:
properties:

View File

@@ -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"

View File

@@ -322,4 +322,7 @@ angular
}
return fullName.substring(0, versionIdx);
};
})
.filter('unique', function () {
return _.uniqBy;
});

View File

@@ -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;
}
}

View File

@@ -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();
})

View File

@@ -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);

View File

@@ -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];

View File

@@ -10,5 +10,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab
reverseOrder: '<',
removeAction: '<',
refreshCallback: '<',
endpoint: '<',
},
});

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView',
templateUrl: './resourcePools.html',
controller: 'KubernetesResourcePoolsController',
controllerAs: 'ctrl',
bindings: {
endpoint: '<',
},
});

View File

@@ -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);
}

View File

@@ -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;
}
},
]);

View File

@@ -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>

View File

@@ -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 () {

View File

@@ -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"
}
}
}