diff --git a/api/server/handlers/environment_groups/list.go b/api/server/handlers/environment_groups/list.go index e2556d3566..bb1dff0319 100644 --- a/api/server/handlers/environment_groups/list.go +++ b/api/server/handlers/environment_groups/list.go @@ -49,6 +49,7 @@ func (c *ListEnvironmentGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http. ctx, span := telemetry.NewSpan(r.Context(), "serve-list-env-groups") defer span.End() + project, _ := ctx.Value(types.ProjectScope).(*models.Project) cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) agent, err := c.GetAgent(r, cluster, "") @@ -84,26 +85,39 @@ func (c *ListEnvironmentGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http. return } - applications, err := environmentgroups.LinkedApplications(ctx, agent, latestVersion.Name) - if err != nil { - err = telemetry.Error(ctx, span, err, "unable to get linked applications") - c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) - return - } + var linkedApplications []string + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { + applications, err := environmentgroups.LinkedApplications(ctx, agent, latestVersion.Name, true) + if err != nil { + err = telemetry.Error(ctx, span, err, "unable to get linked applications") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } - applicationSetForEnvGroup := make(map[string]struct{}) - for _, app := range applications { - if app.Namespace == "" { - continue + applicationSetForEnvGroup := make(map[string]struct{}) + for _, app := range applications { + if app.Namespace == "" { + continue + } + if _, ok := applicationSetForEnvGroup[app.Namespace]; !ok { + applicationSetForEnvGroup[app.Namespace] = struct{}{} + } } - if _, ok := applicationSetForEnvGroup[app.Namespace]; !ok { - applicationSetForEnvGroup[app.Namespace] = struct{}{} + for appNamespace := range applicationSetForEnvGroup { + porterAppName := strings.TrimPrefix(appNamespace, "porter-stack-") + linkedApplications = append(linkedApplications, porterAppName) + } + } else { + applications, err := environmentgroups.LinkedApplications(ctx, agent, latestVersion.Name, false) + if err != nil { + err = telemetry.Error(ctx, span, err, "unable to get linked applications") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + for _, app := range applications { + linkedApplications = append(linkedApplications, app.Name) } - } - var linkedApplications []string - for appNamespace := range applicationSetForEnvGroup { - porterAppName := strings.TrimPrefix(appNamespace, "porter-stack-") - linkedApplications = append(linkedApplications, porterAppName) } secrets := make(map[string]string) diff --git a/dashboard/src/assets/check.png b/dashboard/src/assets/check.png new file mode 100644 index 0000000000..d23bba6d28 Binary files /dev/null and b/dashboard/src/assets/check.png differ diff --git a/dashboard/src/components/CloudFormationForm.tsx b/dashboard/src/components/CloudFormationForm.tsx index 116a0dc37e..1b36a28aa4 100644 --- a/dashboard/src/components/CloudFormationForm.tsx +++ b/dashboard/src/components/CloudFormationForm.tsx @@ -15,10 +15,11 @@ import Button from "./porter/Button"; import Link from "./porter/Link"; import Container from "./porter/Container"; import Step from "./porter/Step"; -import { Box, Step as MuiStep, StepContent, StepLabel, Stepper, ThemeProvider, Typography, createTheme } from "@material-ui/core"; import { useQuery } from "@tanstack/react-query"; import Modal from "./porter/Modal"; import theme from "shared/themes/midnight"; +import VerticalSteps from "./porter/VerticalSteps"; +import PreflightChecks from "./PreflightChecks"; type Props = { goBack: () => void; @@ -26,36 +27,6 @@ type Props = { switchToCredentialFlow: () => void; }; -const stepperTheme = createTheme({ - palette: { - background: { - paper: 'none', - }, - text: { - primary: '#DFDFE1', - secondary: '#aaaabb', - }, - action: { - active: '#001E3C', - }, - }, - typography: { - fontFamily: "Work Sans, sans-serif", - }, - overrides: { - MuiStepIcon: { - root: { - '&$completed': { - color: theme.button, - }, - '&$active': { - color: theme.button, - }, - }, - }, - }, -}); - const CloudFormationForm: React.FC = ({ goBack, proceed, @@ -65,6 +36,7 @@ const CloudFormationForm: React.FC = ({ const [currentStep, setCurrentStep] = useState(0); const [hasClickedCloudformationButton, setHasClickedCloudformationButton] = useState(false); const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); + const [preflightData, setPreflightData] = useState(undefined); const { currentProject, user } = useContext(Context); const markStepStarted = async ( @@ -116,10 +88,17 @@ const CloudFormationForm: React.FC = ({ id: currentProject.id, } ); + setPreflightData({ + "Msg": { + "preflight_checks": { + cloudFormation: {}, + } + } + }) return true; }, { - enabled: currentStep === 2, + enabled: currentStep === 3, retry: (failureCount, err) => { // if they've waited over 35 seconds notify us on slack. Cloudformation stack should only take around 20-25 seconds to create if (failureCount === 7 && hasClickedCloudformationButton) { @@ -203,6 +182,7 @@ const CloudFormationForm: React.FC = ({ } const directToCloudFormation = () => { + setCurrentStep(3) const externalId = getExternalId(); let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement"; const cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole¶m_ExternalIdParameter=${externalId}¶m_TrustArnParameter=${trustArn}` @@ -216,150 +196,168 @@ const CloudFormationForm: React.FC = ({ <> Grant Porter permissions to create infrastructure in your AWS account by following 3 simple steps. - - - - Log in to your AWS Account. - - Return to Porter after successful login. - - - - - - - - - - - - - Enter your AWS Account ID. - - Make sure this is the ID of the account you are currently logged into and would like to provision resources in. - - - 👤 AWS account ID - { - window.open("https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/account", "_blank") - }} - > - help_outline - - - } - value={AWSAccountID} - setValue={handleAWSAccountIDChange} - placeholder="ex: 915037676314" - error={awsAccountIdInputError} - /> - - - - - - - - - - This grants Porter permissions to create infrastructure in your account.}>Create an AWS Cloudformation Stack. - - Clicking the button below will take you to the AWS CloudFormation console. Return to Porter after clicking 'Create stack' in the bottom right corner. - - - - - - - - Once the CloudFormation stack has status{" "} - - CREATE_COMPLETE - , you may proceed. - - - This may take 1 - 2 minutes. - - - - - - - - setShowNeedHelpModal(true)}> + + Log in to your AWS account + + + Return to Porter after successful login. + + + + + + + + + , + <> + Enter your AWS account ID + + + Make sure this is the ID of the account you are currently logged into and would like to provision resources in. + + + + 👤 AWS account ID + { + window.open("https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/account", "_blank") + }} + > + help_outline + + + } + value={AWSAccountID} + setValue={handleAWSAccountIDChange} + placeholder="ex: 915037676314" + error={awsAccountIdInputError} + /> + + + + + + + , + <> + Create an AWS CloudFormation stack + + + This grants Porter permissions to create infrastructure in your account. + + + + Clicking the button below will take you to the AWS CloudFormation console. Return to Porter after clicking 'Create stack' in the bottom right corner. + + + + + + + + + + + + + , + <> + Check permissions + + + Checking if Porter can access AWS account with ID {AWSAccountID}. This can take up to a minute. setShowNeedHelpModal(true)}> Need help? - - - {showNeedHelpModal && - setShowNeedHelpModal(false)} width={"800px"}> - Granting Porter access to AWS - - - Porter needs access to your AWS account in order to create infrastructure. You can grant Porter access to AWS by following these steps: - - - - - Create an AWS account - - - if you don't already have one. - - - - Once you are logged in to your AWS account, - - - copy your account ID - . - - - Fill in your account ID on Porter and select "Grant permissions". - - After being redirected to AWS CloudFormation, select "Create stack" on the bottom right. - - The stack will start to create. Refresh until the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE": - - - - - - Return to Porter and select "Continue". - - If you continue to see issues, email support. - - } - - + + + + + + + + + + , + ]} + /> + {showNeedHelpModal && + setShowNeedHelpModal(false)} width={"800px"}> + Granting Porter access to AWS + + + Porter needs access to your AWS account in order to create infrastructure. You can grant Porter access to AWS by following these steps: + + + + + Create an AWS account + + + if you don't already have one. + + + + Once you are logged in to your AWS account, + + + copy your account ID + . + + + Fill in your account ID on Porter and select "Grant permissions". + + After being redirected to AWS CloudFormation, select "Create stack" on the bottom right. + + The stack will start to create. Refresh until the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE": + + + + + + Return to Porter and select "Continue". + + If you continue to see issues, email support. + + } ); } diff --git a/dashboard/src/components/PreflightChecks.tsx b/dashboard/src/components/PreflightChecks.tsx index b5d5132138..09f049767d 100644 --- a/dashboard/src/components/PreflightChecks.tsx +++ b/dashboard/src/components/PreflightChecks.tsx @@ -91,33 +91,45 @@ const PreflightChecks: React.FC = (props) => { ); }; return ( - - Cluster provision check - - - Porter checks that the account has the right permissions and resources to provision a cluster. - - - { - props.error ? - props.provider === 'AWS' ? - : - <> - - - - Check to see if billing is enabled on your account - - - - : - Object.keys(currentMessageConst).map((checkKey) => ( - - )) - } - - ); + props.provider === 'DEFAULT' ? + + {Object.keys(currentMessageConst).map((checkKey) => ( + + ))} + + : + + ( + + + Cluster provision check + + + Porter checks that the account has the right permissions and resources to provision a cluster. + + + { + props.error ? + props.provider === 'AWS' ? + : + <> + + + + Check to see if billing is enabled on your account + + + + : + Object.keys(currentMessageConst).map((checkKey) => ( + + )) + + } + + ) + ) }; @@ -131,7 +143,7 @@ const AppearingDiv = styled.div<{ color?: string }>` display: flex; flex-direction: column; color: ${(props) => props.color || "#ffffff44"}; - margin-left: 10px; + @keyframes floatIn { from { opacity: 0; diff --git a/dashboard/src/components/ProvisionerSettings.tsx b/dashboard/src/components/ProvisionerSettings.tsx index 61bddbbbf6..83594d295f 100644 --- a/dashboard/src/components/ProvisionerSettings.tsx +++ b/dashboard/src/components/ProvisionerSettings.tsx @@ -1013,6 +1013,7 @@ const ProvisionerSettings: React.FC = (props) => { Preflight checks for the account didn't pass. Please fix the issues and retry. + < Button // disabled={isDisabled()} disabled={isLoading} diff --git a/dashboard/src/components/porter/VerticalSteps.tsx b/dashboard/src/components/porter/VerticalSteps.tsx index b68be7fbdd..4b01613fe5 100644 --- a/dashboard/src/components/porter/VerticalSteps.tsx +++ b/dashboard/src/components/porter/VerticalSteps.tsx @@ -1,39 +1,54 @@ import React, { useEffect, useState } from "react"; import styled from "styled-components"; +import AnimateHeight from "react-animate-height"; +import Button from "./Button"; +import Spacer from "./Spacer"; +import Container from "./Container"; + +import check from "assets/check.png"; type Props = { steps: React.ReactNode[]; currentStep: number; + onlyShowCurrentStep?: boolean; }; const VerticalSteps: React.FC = ({ steps, currentStep, + onlyShowCurrentStep, }) => { const [isExpanded, setIsExpanded] = useState(false); return ( + {steps.map((step, i) => { return ( - - { - (i !== steps.length - 1) && ( - - ) - } - - {i+1} - - - {step} - { - i > currentStep && ( + + {i === steps.length - 1 && ( + + )} + {onlyShowCurrentStep && i < currentStep ? ( + + ) : ( + + {i+1} + + )} + + + {step} + {i > currentStep && ( - ) - } - - + )} + + + ); })} @@ -42,6 +57,33 @@ const VerticalSteps: React.FC = ({ export default VerticalSteps; +const LineCover = styled.div` + width: 10px; + height: 100%; + position: absolute; + left: -4px; + top: 0; + background: #121212; +`; + +const Relative = styled.div` + position: relative; +`; + +const Check = styled.img` + height: 26px; + border-radius: 50%; + position: absolute; + left: -8px; + top: -2px; + opacity: 1; + display: flex; + justify-content: center; + align-items: center; + background: #121212; + padding: 8px; +`; + const Number = styled.div` font-size: 12px; color: #fff; @@ -57,14 +99,13 @@ const ReadOnlyOverlay = styled.div` `; const Line = styled.div<{ - isActive: boolean; + isActive?: boolean; }>` width: 1px; - height: calc(100% + 35px); + height: calc(100% - 10px); background: #414141; position: absolute; left: 4px; - top: 8px; opacity: 1; `; @@ -91,7 +132,7 @@ const OpacityWrapper = styled.div<{ opacity: ${props => props.isActive ? 1 : 0.5}; `; -const StepWrapper = styled.div<{ +const StepWrapper = styled(AnimateHeight)<{ isLast: boolean; }>` padding-left: 30px; @@ -101,4 +142,5 @@ const StepWrapper = styled.div<{ const StyledVerticalSteps = styled.div<{ }>` + position: relative; `; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx b/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx index d2b0bca355..516358a03f 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx @@ -27,8 +27,8 @@ export const LatestRevisionContext = createContext<{ clusterId: number; projectId: number; deploymentTargetId: string; - previewRevision: number | null; - setPreviewRevision: Dispatch>; + previewRevision: AppRevision | null; + setPreviewRevision: Dispatch>; } | null>(null); export const useLatestRevision = () => { @@ -48,7 +48,7 @@ export const LatestRevisionProvider = ({ appName?: string; children: JSX.Element; }) => { - const [previewRevision, setPreviewRevision] = useState(null); + const [previewRevision, setPreviewRevision] = useState(null); const { currentCluster, currentProject } = useContext(Context); const deploymentTarget = useDefaultDeploymentTarget(); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx index 3a3cd2fdc6..beb7629ee1 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx @@ -19,6 +19,7 @@ const Environment: React.FC = () => { latestProto, clusterId, projectId, + previewRevision, } = useLatestRevision(); const { formState: { isSubmitting, errors }, @@ -68,7 +69,7 @@ const Environment: React.FC = () => { diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroups.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroups.tsx index bf892379a7..47d04fe242 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroups.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroups.tsx @@ -153,7 +153,7 @@ const EnvGroups: React.FC = ({ Max 4 Env Groups allowed - {envGroups.length > 0 && ( + {populatedEnvWithFallback.length > 0 && ( <> Synced environment groups diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx index 5deacac4a1..46fedf0c73 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx @@ -102,7 +102,7 @@ const RevisionTableContents: React.FC = ({ const { numDeployed, latestRevision } = args; if (previewRevision) { - return previewRevision; + return previewRevision.revision_number; } if (latestRevision && latestRevision.revision_number !== 0) { @@ -181,7 +181,8 @@ const RevisionTableContents: React.FC = ({ key={revision.revision_number} selected={ previewRevision - ? revision.revision_number === previewRevision + ? revision.revision_number === + previewRevision.revision_number : isLatestDeployedRevision } onClick={() => { @@ -198,10 +199,9 @@ const RevisionTableContents: React.FC = ({ envGroupNames: [], }, }); + setPreviewRevision( - isLatestDeployedRevision - ? null - : revision.revision_number + isLatestDeployedRevision ? null : revision ); }} > diff --git a/dashboard/src/shared/util.ts b/dashboard/src/shared/util.ts index e88a6799b2..c14efd3f36 100644 --- a/dashboard/src/shared/util.ts +++ b/dashboard/src/shared/util.ts @@ -13,12 +13,7 @@ export function valueExists(value: T | null | undefined): value is T { export const PREFLIGHT_MESSAGE_CONST = { - "apiEnabled": "APIs enabled on service account", - "cidrAvailability": "CIDR availability", - "eip": "Elastic IP availability", - "natGateway": "NAT Gateway availability", - "vpc": "VPC availability", - "vcpus": "vCPUs availability", + "cloudFormation": "CloudFormation stack created", } export const PREFLIGHT_MESSAGE_CONST_AWS = { diff --git a/internal/kubernetes/environment_groups/delete.go b/internal/kubernetes/environment_groups/delete.go index 44847fd632..bc64a737b7 100644 --- a/internal/kubernetes/environment_groups/delete.go +++ b/internal/kubernetes/environment_groups/delete.go @@ -27,7 +27,7 @@ func DeleteEnvironmentGroup(ctx context.Context, a *kubernetes.Agent, name strin } for _, environmentGroup := range environmentGroups { - applications, err := LinkedApplications(ctx, a, environmentGroup.Name) + applications, err := LinkedApplications(ctx, a, environmentGroup.Name, true) if err != nil { return telemetry.Error(ctx, span, err, "unable to list linked applications") } diff --git a/internal/kubernetes/environment_groups/list.go b/internal/kubernetes/environment_groups/list.go index cfe3eb3e54..ab3e68f248 100644 --- a/internal/kubernetes/environment_groups/list.go +++ b/internal/kubernetes/environment_groups/list.go @@ -9,6 +9,8 @@ import ( "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/telemetry" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -22,6 +24,9 @@ const ( // Namespace_EnvironmentGroups is the base namespace for storing all environment groups. // The configmaps and secrets here should be considered the source's of truth for a given version Namespace_EnvironmentGroups = "porter-env-group" + + // LabelKey_AppName is the label key for the app name + LabelKey_AppName = "porter.run/app-name" ) // EnvironmentGroup represents a ConfigMap in the porter-env-group namespace @@ -228,26 +233,48 @@ type LinkedPorterApplication struct { Namespace string } -// LinkedApplications lists all applications that are linked to a given environment group. Since there can be multiple linked environment groups we must check by the presence of a label on the deployment and job -func LinkedApplications(ctx context.Context, a *kubernetes.Agent, environmentGroupName string) ([]LinkedPorterApplication, error) { - ctx, span := telemetry.NewSpan(ctx, "list-linked-applications") - defer span.End() +func listLinkedAppsByUniqueAppLabel(environmentGroupName string, deployments []appsv1.Deployment, cronJobs []batchv1.CronJob) []LinkedPorterApplication { + appsByName := make(map[string]LinkedPorterApplication) - if environmentGroupName == "" { - return nil, telemetry.Error(ctx, span, nil, "environment group cannot be empty") + for _, d := range deployments { + applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".") + appName := d.Labels[LabelKey_AppName] + + for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups { + if linkedEnvironmentGroup == environmentGroupName && appName != "" { + appsByName[appName] = LinkedPorterApplication{ + Name: appName, + Namespace: d.Namespace, + } + } + } } - telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "environment-group-name", Value: environmentGroupName}) - deployListResp, err := a.Clientset.AppsV1().Deployments(metav1.NamespaceAll).List(ctx, - metav1.ListOptions{ - LabelSelector: LabelKey_LinkedEnvironmentGroup, - }) - if err != nil { - return nil, telemetry.Error(ctx, span, err, "unable to list linked deployment applications") + for _, d := range cronJobs { + applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".") + appName := d.Labels[LabelKey_AppName] + + for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups { + if linkedEnvironmentGroup == environmentGroupName && appName != "" { + appsByName[appName] = LinkedPorterApplication{ + Name: appName, + Namespace: d.Namespace, + } + } + } } var apps []LinkedPorterApplication - for _, d := range deployListResp.Items { + for _, app := range appsByName { + apps = append(apps, app) + } + + return apps +} + +func listLinkedAppsByUniqueNamespace(environmentGroupName string, deployments []appsv1.Deployment, cronJobs []batchv1.CronJob) []LinkedPorterApplication { + var apps []LinkedPorterApplication + for _, d := range deployments { applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".") for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups { @@ -260,15 +287,7 @@ func LinkedApplications(ctx context.Context, a *kubernetes.Agent, environmentGro } } - cronListResp, err := a.Clientset.BatchV1().CronJobs(metav1.NamespaceAll).List(ctx, - metav1.ListOptions{ - LabelSelector: LabelKey_LinkedEnvironmentGroup, - }) - if err != nil { - return nil, telemetry.Error(ctx, span, err, "unable to list linked cronjob applications") - } - - for _, d := range cronListResp.Items { + for _, d := range cronJobs { applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".") for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups { if linkedEnvironmentGroup == environmentGroupName { @@ -280,5 +299,40 @@ func LinkedApplications(ctx context.Context, a *kubernetes.Agent, environmentGro } } + return apps +} + +// LinkedApplications lists all applications that are linked to a given environment group. Since there can be multiple linked environment groups we must check by the presence of a label on the deployment and job +func LinkedApplications(ctx context.Context, a *kubernetes.Agent, environmentGroupName string, byUniqueNamespace bool) ([]LinkedPorterApplication, error) { + ctx, span := telemetry.NewSpan(ctx, "list-linked-applications") + defer span.End() + + if environmentGroupName == "" { + return nil, telemetry.Error(ctx, span, nil, "environment group cannot be empty") + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "environment-group-name", Value: environmentGroupName}) + + deployListResp, err := a.Clientset.AppsV1().Deployments(metav1.NamespaceAll).List(ctx, + metav1.ListOptions{ + LabelSelector: LabelKey_LinkedEnvironmentGroup, + }) + if err != nil { + return nil, telemetry.Error(ctx, span, err, "unable to list linked deployment applications") + } + cronListResp, err := a.Clientset.BatchV1().CronJobs(metav1.NamespaceAll).List(ctx, + metav1.ListOptions{ + LabelSelector: LabelKey_LinkedEnvironmentGroup, + }) + if err != nil { + return nil, telemetry.Error(ctx, span, err, "unable to list linked cronjob applications") + } + + var apps []LinkedPorterApplication + if byUniqueNamespace { + apps = listLinkedAppsByUniqueNamespace(environmentGroupName, deployListResp.Items, cronListResp.Items) + return apps, nil + } + + apps = listLinkedAppsByUniqueAppLabel(environmentGroupName, deployListResp.Items, cronListResp.Items) return apps, nil }