1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

fix(edge): filtering of edge devices [EE-3210] (#7077)

* fix(edge): filtering of edge devices [EE-3210]

fixes [EE-3210]

changes:
- replaces `edgeDeviceFilter` with two filters:
	- `edgeDevice`
	- `edgeDeviceUntrusted`

these filters will only apply to the edge endpoints in the query (so it's possible to get both regular endpoints and edge devices).

if `edgeDevice` is true, will filter out edge agents which are not an edge device.
			false, will filter out edge devices

`edgeDeviceUntrusted` applies only when `edgeDevice` is true. then false (default) will hide the untrusted edge devices, true will show only untrusted edge devices.

fix(edge/job-create): retrieve only trusted endpoints + fix endpoint selector pagination limits onChange

fix(endpoint-groups): remove listing of untrusted edge envs (aka in waiting room)

refactor(endpoints): move filter to another function

feat(endpoints): separate edge filters

refactor(environments): change getEnv api

refactor(endpoints): use single getEnv

feat(groups): show error when failed loading envs

style(endpoints): remove unused endpointsByGroup

* chore(deps): update go to 1.18

* fix(endpoint): filter out untrusted by default

* fix(edge): show correct endpoints

* style(endpoints): fix typo

* fix(endpoints): fix swagger

* fix(admin): use new getEnv function

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
This commit is contained in:
Chaim Lev-Ari 2022-07-19 18:00:45 +02:00 committed by GitHub
parent 1a8fe82821
commit 05357ecce5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 868 additions and 601 deletions

View file

@ -11,7 +11,7 @@
loaded="$ctrl.loaded"
page-type="$ctrl.pageType"
table-type="available"
retrieve-page="$ctrl.getPaginatedEndpoints"
retrieve-page="$ctrl.getAvailableEndpoints"
dataset="$ctrl.endpoints.available"
entry-click="$ctrl.associateEndpoint"
pagination-state="$ctrl.state.available"
@ -34,7 +34,7 @@
loaded="$ctrl.loaded"
page-type="$ctrl.pageType"
table-type="associated"
retrieve-page="$ctrl.getPaginatedEndpoints"
retrieve-page="$ctrl.getAssociatedEndpoints"
dataset="$ctrl.endpoints.associated"
entry-click="$ctrl.dissociateEndpoint"
pagination-state="$ctrl.state.associated"

View file

@ -1,11 +1,13 @@
import angular from 'angular';
import _ from 'lodash-es';
import { EdgeTypes } from '@/portainer/environments/types';
import { getEnvironments } from '@/portainer/environments/environment.service';
class AssoicatedEndpointsSelectorController {
/* @ngInject */
constructor($async, EndpointService) {
constructor($async) {
this.$async = $async;
this.EndpointService = EndpointService;
this.state = {
available: {
@ -27,12 +29,11 @@ class AssoicatedEndpointsSelectorController {
available: null,
};
this.getEndpoints = this.getEndpoints.bind(this);
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
this.getAvailableEndpoints = this.getAvailableEndpoints.bind(this);
this.getAssociatedEndpoints = this.getAssociatedEndpoints.bind(this);
this.getAssociatedEndpointsAsync = this.getAssociatedEndpointsAsync.bind(this);
this.associateEndpoint = this.associateEndpoint.bind(this);
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
this.loadData = this.loadData.bind(this);
}
$onInit() {
@ -46,41 +47,41 @@ class AssoicatedEndpointsSelectorController {
}
loadData() {
this.getAvailableEndpoints();
this.getAssociatedEndpoints();
this.getEndpoints();
}
getEndpoints() {
return this.$async(this.getEndpointsAsync);
}
/* #region internal queries to retrieve endpoints per "side" of the selector */
getAvailableEndpoints() {
return this.$async(async () => {
const { start, search, limit } = this.getPaginationData('available');
const query = { search, types: EdgeTypes };
async getEndpointsAsync() {
const { start, search, limit } = this.getPaginationData('available');
const query = { search, types: [4, 7] };
const response = await getEnvironments({ start, limit, query });
const response = await this.EndpointService.endpoints(start, limit, query);
const endpoints = _.filter(response.value, (endpoint) => !_.includes(this.endpointIds, endpoint.Id));
this.setTableData('available', endpoints, response.totalCount);
this.noEndpoints = this.state.available.totalCount === 0;
const endpoints = _.filter(response.value, (endpoint) => !_.includes(this.endpointIds, endpoint.Id));
this.setTableData('available', endpoints, response.totalCount);
this.noEndpoints = this.state.available.totalCount === 0;
});
}
getAssociatedEndpoints() {
return this.$async(this.getAssociatedEndpointsAsync);
}
async getAssociatedEndpointsAsync() {
let response = { value: [], totalCount: 0 };
if (this.endpointIds.length > 0) {
const { start, search, limit } = this.getPaginationData('associated');
const query = { search, types: [4, 7], endpointIds: this.endpointIds };
response = await this.EndpointService.endpoints(start, limit, query);
}
this.setTableData('associated', response.value, response.totalCount);
return this.$async(async () => {
let response = { value: [], totalCount: 0 };
if (this.endpointIds.length > 0) {
// fetch only if already has associated endpoints
const { start, search, limit } = this.getPaginationData('associated');
const query = { search, types: EdgeTypes, endpointIds: this.endpointIds };
response = await getEnvironments({ start, limit, query });
}
this.setTableData('associated', response.value, response.totalCount);
});
}
/* #endregion */
/* #region On endpoint click (either available or associated) */
associateEndpoint(endpoint) {
this.onAssociate(endpoint);
}
@ -88,7 +89,9 @@ class AssoicatedEndpointsSelectorController {
dissociateEndpoint(endpoint) {
this.onDissociate(endpoint);
}
/* #endregion */
/* #region Utils funcs */
getPaginationData(tableType) {
const { pageNumber, limit, search } = this.state[tableType];
const start = (pageNumber - 1) * limit + 1;
@ -100,6 +103,7 @@ class AssoicatedEndpointsSelectorController {
this.endpoints[tableType] = endpoints;
this.state[tableType].totalCount = parseInt(totalCount, 10);
}
/* #endregion */
}
angular.module('portainer.app').controller('AssoicatedEndpointsSelectorController', AssoicatedEndpointsSelectorController);

View file

@ -1,12 +1,13 @@
import _ from 'lodash-es';
import angular from 'angular';
import { endpointsByGroup } from '@/portainer/environments/environment.service';
import { notifyError } from '@/portainer/services/notifications';
class GroupFormController {
/* @ngInject */
constructor($q, $scope, EndpointService, GroupService, Notifications, Authentication) {
this.$q = $q;
constructor($async, $scope, GroupService, Notifications, Authentication) {
this.$async = $async;
this.$scope = $scope;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.Notifications = Notifications;
this.Authentication = Authentication;
@ -75,23 +76,27 @@ class GroupFormController {
}
getPaginatedEndpointsByGroup(pageType, tableType) {
if (tableType === 'available') {
const context = this.state.available;
const start = (context.pageNumber - 1) * context.limit + 1;
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1).then((data) => {
this.availableEndpoints = data.value;
this.state.available.totalCount = data.totalCount;
});
} else if (tableType === 'associated' && pageType === 'edit') {
const groupId = this.model.Id ? this.model.Id : 1;
const context = this.state.associated;
const start = (context.pageNumber - 1) * context.limit + 1;
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId).then((data) => {
this.associatedEndpoints = data.value;
this.state.associated.totalCount = data.totalCount;
});
}
// ignore (associated + create) group as there is no backend pagination for this table
this.$async(async () => {
try {
if (tableType === 'available') {
const context = this.state.available;
const start = (context.pageNumber - 1) * context.limit + 1;
const data = await endpointsByGroup(1, start, context.limit, { search: context.filter });
this.availableEndpoints = data.value;
this.state.available.totalCount = data.totalCount;
} else if (tableType === 'associated' && pageType === 'edit') {
const groupId = this.model.Id ? this.model.Id : 1;
const context = this.state.associated;
const start = (context.pageNumber - 1) * context.limit + 1;
const data = await endpointsByGroup(groupId, start, context.limit, { search: context.filter });
this.associatedEndpoints = data.value;
this.state.associated.totalCount = data.totalCount;
}
// ignore (associated + create) group as there is no backend pagination for this table
} catch (err) {
notifyError('Failure', err, 'Failed getting endpoints for group');
}
});
}
}

View file

@ -12,61 +12,50 @@ import type {
EnvironmentStatus,
} from '../types';
import { arrayToJson, buildUrl } from './utils';
import { buildUrl } from './utils';
export interface EnvironmentsQueryParams {
search?: string;
types?: EnvironmentType[];
types?: EnvironmentType[] | readonly EnvironmentType[];
tagIds?: TagId[];
endpointIds?: EnvironmentId[];
tagsPartialMatch?: boolean;
groupIds?: EnvironmentGroupId[];
status?: EnvironmentStatus[];
sort?: string;
order?: 'asc' | 'desc';
edgeDeviceFilter?: 'all' | 'trusted' | 'untrusted' | 'none';
edgeDevice?: boolean;
edgeDeviceUntrusted?: boolean;
provisioned?: boolean;
name?: string;
}
export async function getEndpoints(
start: number,
limit: number,
export interface GetEnvironmentsOptions {
start?: number;
limit?: number;
sort?: { by?: string; order?: 'asc' | 'desc' };
query?: EnvironmentsQueryParams;
}
export async function getEnvironments(
{
types,
tagIds,
endpointIds,
status,
groupIds,
...query
}: EnvironmentsQueryParams = {}
start,
limit,
sort = { by: '', order: 'asc' },
query = {},
}: GetEnvironmentsOptions = { query: {} }
) {
if (tagIds && tagIds.length === 0) {
if (query.tagIds && query.tagIds.length === 0) {
return { totalCount: 0, value: <Environment[]>[] };
}
const url = buildUrl();
const params: Record<string, unknown> = { start, limit, ...query };
if (types) {
params.types = arrayToJson(types);
}
if (tagIds) {
params.tagIds = arrayToJson(tagIds);
}
if (endpointIds) {
params.endpointIds = arrayToJson(endpointIds);
}
if (status) {
params.status = arrayToJson(status);
}
if (groupIds) {
params.groupIds = arrayToJson(groupIds);
}
const params: Record<string, unknown> = {
start,
limit,
sort: sort.by,
order: sort.order,
...query,
};
try {
const response = await axios.get<Environment[]>(url, { params });
@ -109,12 +98,16 @@ export async function snapshotEndpoint(id: EnvironmentId) {
}
export async function endpointsByGroup(
groupId: EnvironmentGroupId,
start: number,
limit: number,
search: string,
groupId: EnvironmentGroupId
query: Omit<EnvironmentsQueryParams, 'groupIds'>
) {
return getEndpoints(start, limit, { search, groupIds: [groupId] });
return getEnvironments({
start,
limit,
query: { groupIds: [groupId], ...query },
});
}
export async function disassociateEndpoint(id: EnvironmentId) {

View file

@ -3,16 +3,21 @@ import { useQuery } from 'react-query';
import { withError } from '@/react-tools/react-query';
import { EnvironmentStatus } from '../types';
import { EnvironmentsQueryParams, getEndpoints } from '../environment.service';
import {
EnvironmentsQueryParams,
getEnvironments,
} from '../environment.service';
export const ENVIRONMENTS_POLLING_INTERVAL = 30000; // in ms
interface Query extends EnvironmentsQueryParams {
export interface Query extends EnvironmentsQueryParams {
page?: number;
pageLimit?: number;
sort?: string;
order?: 'asc' | 'desc';
}
type GetEndpointsResponse = Awaited<ReturnType<typeof getEndpoints>>;
type GetEndpointsResponse = Awaited<ReturnType<typeof getEnvironments>>;
export function refetchIfAnyOffline(data?: GetEndpointsResponse) {
if (!data) {
@ -31,7 +36,7 @@ export function refetchIfAnyOffline(data?: GetEndpointsResponse) {
}
export function useEnvironmentList(
{ page = 1, pageLimit = 100, ...query }: Query = {},
{ page = 1, pageLimit = 100, sort, order, ...query }: Query = {},
refetchInterval?:
| number
| false
@ -45,12 +50,19 @@ export function useEnvironmentList(
{
page,
pageLimit,
sort,
order,
...query,
},
],
async () => {
const start = (page - 1) * pageLimit + 1;
return getEndpoints(start, pageLimit, query);
return getEnvironments({
start,
limit: pageLimit,
sort: { by: sort, order },
query,
});
},
{
staleTime,

View file

@ -20,6 +20,11 @@ export enum EnvironmentType {
EdgeAgentOnKubernetes,
}
export const EdgeTypes = [
EnvironmentType.EdgeAgentOnDocker,
EnvironmentType.EdgeAgentOnKubernetes,
] as const;
export enum EnvironmentStatus {
Up = 1,
Down,

View file

@ -133,7 +133,8 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
groupIds: groupFilter,
sort: sortByFilter,
order: sortByDescending ? 'desc' : 'asc',
edgeDeviceFilter: 'none',
provisioned: true,
edgeDevice: false,
tagsPartialMatch: true,
},
refetchIfAnyOffline
@ -312,7 +313,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
groupIds: groupFilter,
sort: sortByFilter,
order: sortByDescending ? 'desc' : 'asc',
edgeDeviceFilter: 'none',
edgeDevice: false,
}}
/>
</div>

View file

@ -2,7 +2,7 @@ import { useState } from 'react';
import { Download } from 'react-feather';
import { Environment } from '@/portainer/environments/types';
import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service/index';
import { Query } from '@/portainer/environments/queries/useEnvironmentList';
import { isKubernetesEnvironment } from '@/portainer/environments/utils';
import { trackEvent } from '@/angulartics.matomo/analytics-services';
@ -14,7 +14,7 @@ import '@reach/dialog/styles.css';
export interface Props {
environments: Environment[];
envQueryParams: EnvironmentsQueryParams;
envQueryParams: Query;
}
export function KubeconfigButton({ environments, envQueryParams }: Props) {
const [isOpen, setIsOpen] = useState(false);

View file

@ -1,15 +1,15 @@
import _ from 'lodash-es';
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
import { getEnvironments } from '@/portainer/environments/environment.service';
import AccessViewerPolicyModel from '../../models/access';
export default class AccessViewerController {
/* @ngInject */
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService, Authentication) {
constructor(Notifications, RoleService, UserService, GroupService, TeamService, TeamMembershipService, Authentication) {
this.Notifications = Notifications;
this.RoleService = RoleService;
this.UserService = UserService;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.TeamService = TeamService;
this.TeamMembershipService = TeamMembershipService;
@ -138,7 +138,7 @@ export default class AccessViewerController {
this.isAdmin = this.Authentication.isAdmin();
this.allUsers = await this.UserService.users();
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
this.endpoints = _.keyBy((await getEnvironments()).value, 'Id');
const groups = await this.GroupService.groups();
this.groupUserAccessPolicies = {};
this.groupTeamAccessPolicies = {};

View file

@ -16,14 +16,6 @@ angular.module('portainer.app').factory('EndpointService', [
return Endpoints.get({ id: endpointID }).$promise;
};
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, types: JSON.stringify(types), tagIds: JSON.stringify(tagIds), endpointIds: JSON.stringify(endpointIds), tagsPartialMatch })
.$promise;
};
service.snapshotEndpoints = function () {
return Endpoints.snapshots({}, {}).$promise;
};
@ -32,10 +24,6 @@ angular.module('portainer.app').factory('EndpointService', [
return Endpoints.snapshot({ id: endpointID }, {}).$promise;
};
service.endpointsByGroup = function (start, limit, search, groupId) {
return Endpoints.query({ start, limit, search, groupId }).$promise;
};
service.updateAccess = function (id, userAccessPolicies, teamAccessPolicies) {
return Endpoints.updateAccess({ id: id }, { UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies }).$promise;
};

View file

@ -1,18 +1,17 @@
import angular from 'angular';
import { getEnvironments } from '../environments/environment.service';
angular.module('portainer.app').factory('NameValidator', NameValidatorFactory);
/* @ngInject */
function NameValidatorFactory(EndpointService, Notifications) {
function NameValidatorFactory(Notifications) {
return {
validateEnvironmentName,
};
async function validateEnvironmentName(environmentName) {
async function validateEnvironmentName(name) {
try {
const endpoints = await EndpointService.endpoints();
const endpointArray = endpoints.value;
const nameDuplicated = endpointArray.filter((item) => item.Name === environmentName);
return nameDuplicated.length > 0;
const endpoints = await getEnvironments({ limit: 1, name });
return endpoints.value.length > 0;
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve environment details');
}

View file

@ -1,5 +1,6 @@
import angular from 'angular';
import uuidv4 from 'uuid/v4';
import { getEnvironments } from '@/portainer/environments/environment.service';
class AuthenticationController {
/* @ngInject */
@ -12,7 +13,6 @@ class AuthenticationController {
$window,
Authentication,
UserService,
EndpointService,
StateManager,
Notifications,
SettingsService,
@ -28,7 +28,6 @@ class AuthenticationController {
this.$window = $window;
this.Authentication = Authentication;
this.UserService = UserService;
this.EndpointService = EndpointService;
this.StateManager = StateManager;
this.Notifications = Notifications;
this.SettingsService = SettingsService;
@ -119,8 +118,8 @@ class AuthenticationController {
async checkForEndpointsAsync() {
try {
const endpoints = await this.EndpointService.endpoints(0, 1);
const isAdmin = this.Authentication.isAdmin();
const endpoints = await getEnvironments({ limit: 1 });
if (this.Authentication.getUserDetails().forceChangePassword) {
return this.$state.go('portainer.account');

View file

@ -1,5 +1,6 @@
import angular from 'angular';
import EndpointHelper from 'Portainer/helpers/endpointHelper';
import EndpointHelper from '@/portainer/helpers/endpointHelper';
import { getEnvironments } from '@/portainer/environments/environment.service';
angular.module('portainer.app').controller('EndpointsController', EndpointsController);
@ -46,10 +47,10 @@ function EndpointsController($q, $scope, $state, $async, EndpointService, GroupS
}
$scope.getPaginatedEndpoints = getPaginatedEndpoints;
function getPaginatedEndpoints(lastId, limit, search) {
function getPaginatedEndpoints(start, limit, search) {
const deferred = $q.defer();
$q.all({
endpoints: EndpointService.endpoints(lastId, limit, { search }),
endpoints: getEnvironments({ start, limit, query: { search } }),
groups: GroupService.groups(),
})
.then(function success(data) {

View file

@ -1,3 +1,5 @@
import { getEnvironments } from '@/portainer/environments/environment.service';
angular.module('portainer.app').controller('InitAdminController', [
'$scope',
'$state',
@ -6,10 +8,9 @@ angular.module('portainer.app').controller('InitAdminController', [
'StateManager',
'SettingsService',
'UserService',
'EndpointService',
'BackupService',
'StatusService',
function ($scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
function ($scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, BackupService, StatusService) {
$scope.uploadBackup = uploadBackup;
$scope.logo = StateManager.getState().application.logo;
@ -50,7 +51,7 @@ angular.module('portainer.app').controller('InitAdminController', [
return StateManager.initialize();
})
.then(function () {
return EndpointService.endpoints(0, 100);
return getEnvironments({ limit: 100 });
})
.then(function success(data) {
if (data.value.length === 0) {

View file

@ -1,6 +1,7 @@
import { ResourceControlType } from '@/portainer/access-control/types';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { FeatureId } from 'Portainer/feature-flags/enums';
import { getEnvironments } from '@/portainer/environments/environment.service';
angular.module('portainer.app').controller('StackController', [
'$async',
@ -20,7 +21,6 @@ angular.module('portainer.app').controller('StackController', [
'Notifications',
'FormHelper',
'EndpointProvider',
'EndpointService',
'GroupService',
'ModalService',
'StackHelper',
@ -46,7 +46,6 @@ angular.module('portainer.app').controller('StackController', [
Notifications,
FormHelper,
EndpointProvider,
EndpointService,
GroupService,
ModalService,
StackHelper,
@ -317,60 +316,62 @@ angular.module('portainer.app').controller('StackController', [
}
function loadStack(id) {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
return $async(() => {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
EndpointService.endpoints()
.then(function success(data) {
$scope.endpoints = data.value;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve environments');
});
$q.all({
stack: StackService.stack(id),
groups: GroupService.groups(),
containers: ContainerService.containers(true),
})
.then(function success(data) {
var stack = data.stack;
$scope.groups = data.groups;
$scope.stack = stack;
$scope.containerNames = ContainerHelper.getContainerNames(data.containers);
$scope.formValues.Env = $scope.stack.Env;
let resourcesPromise = Promise.resolve({});
if (!stack.Status || stack.Status === 1) {
resourcesPromise = stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name);
}
return $q.all({
stackFile: StackService.getStackFile(id),
resources: resourcesPromise,
getEnvironments()
.then(function success(data) {
$scope.endpoints = data.value;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve environments');
});
})
.then(function success(data) {
const isSwarm = $scope.stack.Type === 1;
$scope.stackFileContent = data.stackFile;
// workaround for missing status, if stack has resources, set the status to 1 (active), otherwise to 2 (inactive) (https://github.com/portainer/portainer/issues/4422)
if (!$scope.stack.Status) {
$scope.stack.Status = data.resources && ((isSwarm && data.resources.services.length) || data.resources.containers.length) ? 1 : 2;
}
if ($scope.stack.Status === 1) {
if (isSwarm) {
assignSwarmStackResources(data.resources, agentProxy);
} else {
assignComposeStackResources(data.resources);
$q.all({
stack: StackService.stack(id),
groups: GroupService.groups(),
containers: ContainerService.containers(true),
})
.then(function success(data) {
var stack = data.stack;
$scope.groups = data.groups;
$scope.stack = stack;
$scope.containerNames = ContainerHelper.getContainerNames(data.containers);
$scope.formValues.Env = $scope.stack.Env;
let resourcesPromise = Promise.resolve({});
if (!stack.Status || stack.Status === 1) {
resourcesPromise = stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name);
}
}
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');
});
return $q.all({
stackFile: StackService.getStackFile(id),
resources: resourcesPromise,
});
})
.then(function success(data) {
const isSwarm = $scope.stack.Type === 1;
$scope.stackFileContent = data.stackFile;
// workaround for missing status, if stack has resources, set the status to 1 (active), otherwise to 2 (inactive) (https://github.com/portainer/portainer/issues/4422)
if (!$scope.stack.Status) {
$scope.stack.Status = data.resources && ((isSwarm && data.resources.services.length) || data.resources.containers.length) ? 1 : 2;
}
if ($scope.stack.Status === 1) {
if (isSwarm) {
assignSwarmStackResources(data.resources, agentProxy);
} else {
assignComposeStackResources(data.resources);
}
}
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');
});
});
}
function retrieveSwarmStackResources(stackName, agentProxy) {