ソースを参照

POR-1797 lower latency by moving env off of revision (#3631)

ianedwards 2 年 前
コミット
5824225be6

+ 1 - 23
api/server/handlers/porter_app/current_app_revision.go

@@ -113,13 +113,6 @@ func (c *LatestAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	agent, err := c.GetAgent(r, cluster, "")
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error getting agent")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-
 	currentAppRevisionReq := connect.NewRequest(&porterv1.CurrentAppRevisionRequest{
 		ProjectId:          int64(project.ID),
 		AppId:              int64(porterApps[0].ID),
@@ -147,23 +140,8 @@ func (c *LatestAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
-		ProjectID:                  project.ID,
-		ClusterID:                  int(cluster.ID),
-		DeploymentTargetID:         request.DeploymentTargetID,
-		Revision:                   encodedRevision,
-		K8SAgent:                   agent,
-		PorterAppRepository:        c.Repo().PorterApp(),
-		DeploymentTargetRepository: c.Repo().DeploymentTarget(),
-	})
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error attaching env to revision")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-
 	response := LatestAppRevisionResponse{
-		AppRevision: revisionWithEnv,
+		AppRevision: encodedRevision,
 	}
 
 	c.WriteResult(w, r, response)

+ 17 - 0
api/server/handlers/porter_app/get_app_env.go

@@ -47,6 +47,7 @@ type GetAppEnvRequest struct {
 // GetAppEnvResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/env endpoint
 type GetAppEnvResponse struct {
 	EnvGroups []environment_groups.EnvironmentGroup `json:"env_groups"`
+	AppEnv    environment_groups.EnvironmentGroup   `json:"app_env"`
 }
 
 // ServeHTTP translates the request into a GetAppEnvRequest request, uses the revision proto to query the cluster for the requested env groups, and returns the response
@@ -138,8 +139,24 @@ func (c *GetAppEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
+		ProjectID:                  project.ID,
+		ClusterID:                  int(cluster.ID),
+		DeploymentTargetID:         revision.DeploymentTargetID,
+		Revision:                   revision,
+		K8SAgent:                   agent,
+		PorterAppRepository:        c.Repo().PorterApp(),
+		DeploymentTargetRepository: c.Repo().DeploymentTarget(),
+	})
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error attaching env to revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	res := &GetAppEnvResponse{
 		EnvGroups: envGroups,
+		AppEnv:    revisionWithEnv.Env,
 	}
 
 	c.WriteResult(w, r, res)

+ 109 - 0
api/server/handlers/porter_app/get_app_revision.go

@@ -0,0 +1,109 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/porter_app"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// GetAppRevisionHandler handles requests to the /apps/{porter_app_name}/revisions/{app_revision_id} endpoint
+type GetAppRevisionHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewGetAppRevisionHandler returns a new GetAppRevisionHandler
+func NewGetAppRevisionHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetAppRevisionHandler {
+	return &GetAppRevisionHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// GetAppRevisionResponse represents the response from the /apps/{porter_app_name}/revisions/{app_revision_id} endpoint
+type GetAppRevisionResponse struct {
+	AppRevision porter_app.Revision `json:"app_revision"`
+}
+
+// GetAppRevisionHandler returns a single app revision
+func (c *GetAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-app-revision")
+	defer span.End()
+
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	appRevisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	agent, err := c.GetAgent(r, cluster, "")
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	getRevisionReq := connect.NewRequest(&porterv1.GetAppRevisionRequest{
+		ProjectId:     int64(project.ID),
+		AppRevisionId: appRevisionID,
+	})
+	ccpResp, err := c.Config().ClusterControlPlaneClient.GetAppRevision(ctx, getRevisionReq)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting app revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp == nil || ccpResp.Msg == nil {
+		err = telemetry.Error(ctx, span, nil, "get app revision response is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	encodedRevision, err := porter_app.EncodedRevisionFromProto(ctx, ccpResp.Msg.AppRevision)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting encoded revision from proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
+		ProjectID:                  project.ID,
+		ClusterID:                  int(cluster.ID),
+		Revision:                   encodedRevision,
+		DeploymentTargetID:         ccpResp.Msg.AppRevision.DeploymentTargetId,
+		K8SAgent:                   agent,
+		PorterAppRepository:        c.Repo().PorterApp(),
+		DeploymentTargetRepository: c.Repo().DeploymentTarget(),
+	})
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error attaching env to revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	res := &GetAppRevisionResponse{
+		AppRevision: revisionWithEnv,
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 1 - 23
api/server/handlers/porter_app/list_app_revisions.go

@@ -118,13 +118,6 @@ func (c *ListAppRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		appRevisions = []*porterv1.AppRevision{}
 	}
 
-	agent, err := c.GetAgent(r, cluster, "")
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error getting agent")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-
 	res := &ListAppRevisionsResponse{
 		AppRevisions: make([]porter_app.Revision, 0),
 	}
@@ -137,22 +130,7 @@ func (c *ListAppRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 			return
 		}
 
-		revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
-			Revision:                   encodedRevision,
-			ProjectID:                  project.ID,
-			ClusterID:                  int(cluster.ID),
-			DeploymentTargetID:         request.DeploymentTargetID,
-			K8SAgent:                   agent,
-			PorterAppRepository:        c.Repo().PorterApp(),
-			DeploymentTargetRepository: c.Repo().DeploymentTarget(),
-		})
-		if err != nil {
-			err := telemetry.Error(ctx, span, err, "error attaching env to revision")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-			return
-		}
-
-		res.AppRevisions = append(res.AppRevisions, revisionWithEnv)
+		res.AppRevisions = append(res.AppRevisions, encodedRevision)
 	}
 
 	c.WriteResult(w, r, res)

+ 29 - 0
api/server/router/porter_app.go

@@ -1038,6 +1038,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id} -> porter_app.NewGetAppRevisionHandler
+	getAppRevisionEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/apps/{%s}/revisions/{%s}", types.URLParamPorterAppName, types.URLParamAppRevisionID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getAppRevisionHandler := porter_app.NewGetAppRevisionHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getAppRevisionEndpoint,
+		Handler:  getAppRevisionHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id} -> porter_app.NewUpdateAppRevisionStatusHandler
 	updateAppRevisionStatusEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 4 - 1
dashboard/src/lib/hooks/usePorterYaml.ts

@@ -82,7 +82,10 @@ export const usePorterYaml = ({
         Boolean(source.git_repo_name) &&
         Boolean(source.git_branch),
       retry: (_failureCount, error) => {
-        if (error.response.data?.error?.includes("404")) {
+        if (
+          error.response.data?.error?.includes("404") ||
+          error.response.data?.error?.includes("not found")
+        ) {
           setPorterYamlFound(false);
           return false;
         }

+ 1 - 0
dashboard/src/lib/porter-apps/index.ts

@@ -121,6 +121,7 @@ export function serviceOverrides({
 
   if (!overrides.predeploy) {
     return {
+      build: validatedBuild,
       services,
     };
   }

+ 0 - 7
dashboard/src/lib/revisions/types.ts

@@ -16,13 +16,6 @@ export const appRevisionValidator = z.object({
   id: z.string(),
   created_at: z.string(),
   updated_at: z.string(),
-  env: z.object({
-    name: z.string(),
-    latest_version: z.number(),
-    variables: z.record(z.string(), z.string()).optional(),
-    secret_variables: z.record(z.string(), z.string()).optional(),
-    created_at: z.string(),
-  }),
 });
 
 export type AppRevision = z.infer<typeof appRevisionValidator>;

+ 8 - 17
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -110,8 +110,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
       app: clientAppFromProto({
         proto: latestProto,
         overrides: servicesFromYaml,
-        variables: latestRevision.env.variables,
-        secrets: latestRevision.env.secret_variables,
       }),
       source: latestSource,
       deletions: {
@@ -250,7 +248,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
 
       // redirect to the default tab after save
       history.push(`/apps/${porterApp.name}/${DEFAULT_TAB}`);
-    } catch (err) { }
+    } catch (err) {}
   });
 
   useEffect(() => {
@@ -258,8 +256,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
       app: clientAppFromProto({
         proto: latestProto,
         overrides: servicesFromYaml,
-        variables: latestRevision.env.variables,
-        secrets: latestRevision.env.secret_variables,
       }),
       source: latestSource,
       deletions: {
@@ -267,12 +263,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         serviceNames: [],
       },
     });
-  }, [
-    servicesFromYaml,
-    currentTab,
-    latestProto,
-    latestRevision.revision_number,
-  ]);
+  }, [servicesFromYaml, latestProto, latestRevision.revision_number]);
 
   return (
     <FormProvider {...porterAppFormMethods}>
@@ -320,11 +311,11 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
             { label: "Environment", value: "environment" },
             ...(latestProto.build
               ? [
-                {
-                  label: "Build Settings",
-                  value: "build-settings",
-                },
-              ]
+                  {
+                    label: "Build Settings",
+                    value: "build-settings",
+                  },
+                ]
               : []),
             { label: "Settings", value: "settings" },
           ]}
@@ -343,7 +334,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
               setRedeployOnSave={setRedeployOnSave}
             />
           ))
-          .with("environment", () => <Environment />)
+          .with("environment", () => <Environment latestSource={latestSource} />)
           .with("settings", () => <Settings />)
           .with("logs", () => <LogsTab />)
           .with("metrics", () => <MetricsTab />)

+ 12 - 5
dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx

@@ -5,21 +5,27 @@ import EnvVariables from "../../validate-apply/app-settings/EnvVariables";
 import Button from "components/porter/Button";
 import Error from "components/porter/Error";
 import { useFormContext } from "react-hook-form";
-import { PorterAppFormData } from "lib/porter-apps";
+import { PorterAppFormData, SourceOptions } from "lib/porter-apps";
 import { useLatestRevision } from "../LatestRevisionContext";
 import { useQuery } from "@tanstack/react-query";
 import api from "shared/api";
 import { z } from "zod";
 import { populatedEnvGroup } from "../../validate-apply/app-settings/types";
 import EnvGroups from "../../validate-apply/app-settings/EnvGroups";
+import EnvSettings from "../../validate-apply/app-settings/EnvSettings";
 
-const Environment: React.FC = () => {
+type Props = {
+  latestSource: SourceOptions;
+};
+
+const Environment: React.FC<Props> = ({ latestSource }) => {
   const {
     latestRevision,
     latestProto,
     clusterId,
     projectId,
     previewRevision,
+    servicesFromYaml,
   } = useLatestRevision();
   const {
     formState: { isSubmitting, errors },
@@ -66,12 +72,13 @@ const Environment: React.FC = () => {
       <Text size={16}>Environment variables</Text>
       <Spacer y={0.5} />
       <Text color="helper">Shared among all services.</Text>
-      <EnvVariables />
-      <EnvGroups
+      <EnvSettings
         appName={latestProto.name}
-        revisionId={previewRevision ? previewRevision.id : latestRevision.id} // get versions of env groups attached to preview revision if set
+        revision={previewRevision ? previewRevision : latestRevision} // get versions of env groups attached to preview revision if set
         baseEnvGroups={baseEnvGroups}
         existingEnvGroupNames={envGroupNames}
+        latestSource={latestSource}
+        servicesFromYaml={servicesFromYaml}
       />
       <Spacer y={0.5} />
       <Button

+ 5 - 2
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -49,6 +49,7 @@ import {
   PopulatedEnvGroup,
   populatedEnvGroup,
 } from "../validate-apply/app-settings/types";
+import EnvSettings from "../validate-apply/app-settings/EnvSettings";
 
 type CreateAppProps = {} & RouteComponentProps;
 
@@ -612,8 +613,10 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                     <Text color="helper">
                       Specify environment variables shared among all services.
                     </Text>
-                    <EnvVariables />
-                    <EnvGroups baseEnvGroups={baseEnvGroups} />
+                    <EnvSettings
+                      baseEnvGroups={baseEnvGroups}
+                      servicesFromYaml={null}
+                    />
                   </>,
                   source.type === "github" && (
                     <>

+ 5 - 43
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroups.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useMemo, useState } from "react";
+import React, { useMemo, useState } from "react";
 import styled from "styled-components";
 import { useFieldArray, useFormContext } from "react-hook-form";
 
@@ -8,30 +8,23 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import { PorterAppFormData } from "lib/porter-apps";
 import ExpandableEnvGroup from "./ExpandableEnvGroup";
-import { PopulatedEnvGroup, populatedEnvGroup } from "./types";
-import { useQuery } from "@tanstack/react-query";
-import { Context } from "shared/Context";
-import api from "shared/api";
-import { z } from "zod";
+import { PopulatedEnvGroup } from "./types";
+
 import { valueExists } from "shared/util";
 import EnvGroupModal from "./EnvGroupModal";
 import { IterableElement } from "type-fest";
 
 type Props = {
-  appName?: string;
-  revisionId?: string;
   baseEnvGroups?: PopulatedEnvGroup[];
   existingEnvGroupNames?: string[];
+  attachedEnvGroups?: PopulatedEnvGroup[];
 };
 
 const EnvGroups: React.FC<Props> = ({
-  appName,
-  revisionId,
   baseEnvGroups = [],
   existingEnvGroupNames = [],
+  attachedEnvGroups = [],
 }) => {
-  const { currentCluster, currentProject } = useContext(Context);
-
   const [showEnvModal, setShowEnvModal] = useState(false);
   const [hovered, setHovered] = useState(false);
 
@@ -51,37 +44,6 @@ const EnvGroups: React.FC<Props> = ({
 
   const maxEnvGroupsReached = envGroups.length >= 3;
 
-  const { data: attachedEnvGroups = [] } = useQuery(
-    ["getAttachedEnvGroups", appName, revisionId],
-    async () => {
-      if (!appName || !revisionId || !currentCluster?.id || !currentProject?.id)
-        return [];
-
-      const res = await api.getAttachedEnvGroups(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-          app_name: appName,
-          revision_id: revisionId,
-        }
-      );
-
-      const { env_groups } = await z
-        .object({
-          env_groups: z.array(populatedEnvGroup),
-        })
-        .parseAsync(res.data);
-
-      return env_groups;
-    },
-    {
-      enabled:
-        !!appName && !!revisionId && !!currentCluster && !!currentProject,
-    }
-  );
-
   const populatedEnvWithFallback = useMemo(() => {
     return envGroups
       .map((envGroup, index) => {

+ 100 - 0
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvSettings.tsx

@@ -0,0 +1,100 @@
+import React, { useContext, useEffect } from "react";
+import { useFormContext } from "react-hook-form";
+import {
+  PorterAppFormData,
+  SourceOptions,
+  clientAppFromProto,
+} from "lib/porter-apps";
+import { z } from "zod";
+
+import { PopulatedEnvGroup, populatedEnvGroup } from "./types";
+import EnvVariables from "./EnvVariables";
+import EnvGroups from "./EnvGroups";
+import { Context } from "shared/Context";
+import { useQuery } from "@tanstack/react-query";
+import api from "shared/api";
+import { AppRevision } from "lib/revisions/types";
+import { PorterApp } from "@porter-dev/api-contracts";
+import { DetectedServices } from "lib/porter-apps/services";
+
+type Props = {
+  appName?: string;
+  revision?: AppRevision;
+  baseEnvGroups?: PopulatedEnvGroup[];
+  existingEnvGroupNames?: string[];
+  servicesFromYaml: DetectedServices | null;
+  latestSource?: SourceOptions;
+};
+
+const EnvSettings: React.FC<Props> = (props) => {
+  const { currentCluster, currentProject } = useContext(Context);
+  const { reset } = useFormContext<PorterAppFormData>();
+
+  const { appName, revision, latestSource, servicesFromYaml } = props;
+
+  const { data: { attachedEnvGroups = [], appEnv } = {} } = useQuery(
+    ["getAttachedEnvGroups", appName, revision?.id],
+    async () => {
+      if (!appName || !revision || !currentCluster?.id || !currentProject?.id) {
+        return {
+          attachedEnvGroups: [],
+          appEnv: undefined,
+        };
+      }
+
+      const res = await api.getAttachedEnvGroups(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          app_name: appName,
+          revision_id: revision.id,
+        }
+      );
+
+      const { env_groups: attachedEnvGroups, app_env: appEnv } = await z
+        .object({
+          env_groups: z.array(populatedEnvGroup),
+          app_env: populatedEnvGroup,
+        })
+        .parseAsync(res.data);
+
+      return {
+        attachedEnvGroups,
+        appEnv,
+      };
+    },
+    {
+      enabled: !!appName && !!revision && !!currentCluster && !!currentProject,
+    }
+  );
+
+  useEffect(() => {
+    if (!appEnv || !revision || !latestSource) {
+      return;
+    }
+    reset({
+      app: clientAppFromProto({
+        proto: PorterApp.fromJsonString(atob(revision.b64_app_proto)),
+        overrides: servicesFromYaml,
+        variables: appEnv.variables,
+        secrets: appEnv.secret_variables,
+      }),
+      source: latestSource,
+      deletions: {
+        serviceNames: [],
+        envGroupNames: [],
+      },
+    });
+  }, [appEnv, revision?.id, latestSource]);
+
+  return (
+    <>
+      <EnvVariables />
+      <EnvGroups {...props} attachedEnvGroups={attachedEnvGroups} />
+    </>
+  );
+};
+
+export default EnvSettings;

+ 4 - 8
dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx

@@ -22,9 +22,8 @@ type RevisionTableContentsProps = {
   setRevertData: Dispatch<
     SetStateAction<{
       app: PorterApp;
-      revision: number;
-      variables: Record<string, string>;
-      secrets: Record<string, string>;
+      revisionId: string;
+      number: number;
     } | null>
   >;
 };
@@ -190,8 +189,6 @@ const RevisionTableContents: React.FC<RevisionTableContentsProps> = ({
                         app: clientAppFromProto({
                           proto: revision.app_proto,
                           overrides: servicesFromYaml,
-                          variables: revision.env.variables,
-                          secrets: revision.env.secret_variables,
                         }),
                         source: latestSource,
                         deletions: {
@@ -233,9 +230,8 @@ const RevisionTableContents: React.FC<RevisionTableContentsProps> = ({
 
                           setRevertData({
                             app: revision.app_proto,
-                            revision: revision.revision_number,
-                            variables: revision.env.variables ?? {},
-                            secrets: revision.env.secret_variables ?? {},
+                            revisionId: revision.id,
+                            number: revision.revision_number,
                           });
                         }}
                       >

+ 31 - 7
dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionsList.tsx

@@ -43,9 +43,8 @@ const RevisionsList: React.FC<Props> = ({
   const [expandRevisions, setExpandRevisions] = useState(false);
   const [revertData, setRevertData] = useState<{
     app: PorterApp;
-    revision: number;
-    variables: Record<string, string>;
-    secrets: Record<string, string>;
+    revisionId: string;
+    number: number;
   } | null>(null);
 
   const res = useQuery(
@@ -78,13 +77,38 @@ const RevisionsList: React.FC<Props> = ({
       return;
     }
 
+    const res = await api.getRevision(
+      "<token>",
+      {},
+      {
+        project_id: projectId,
+        cluster_id: clusterId,
+        porter_app_name: appName,
+        revision_id: revertData.revisionId,
+      }
+    );
+
+    const { app_revision } = await z
+      .object({
+        app_revision: appRevisionValidator.extend({
+          env: z.object({
+            name: z.string(),
+            latest_version: z.number(),
+            variables: z.record(z.string(), z.string()).optional(),
+            secret_variables: z.record(z.string(), z.string()).optional(),
+            created_at: z.string(),
+          }),
+        }),
+      })
+      .parseAsync(res.data);
+
     setValue(
       "app",
       clientAppFromProto({
-        proto: revertData.app,
+        proto: PorterApp.fromJsonString(atob(app_revision.b64_app_proto)),
         overrides: servicesFromYaml,
-        variables: revertData.variables,
-        secrets: revertData.secrets,
+        variables: app_revision.env.variables,
+        secrets: app_revision.env.secret_variables,
       })
     );
     setRevertData(null);
@@ -116,7 +140,7 @@ const RevisionsList: React.FC<Props> = ({
           .otherwise(() => null)}
         {revertData ? (
           <ConfirmOverlay
-            message={`Are you sure you want to revert to revision ${revertData?.revision}?`}
+            message={`Are you sure you want to revert to revision ${revertData?.number}?`}
             onYes={onRevert}
             onNo={() => {
               setRevertData(null);

+ 61 - 35
dashboard/src/shared/api.tsx

@@ -337,8 +337,9 @@ const getFeedEvents = baseApi<
   }
 >("GET", (pathParams) => {
   let { project_id, cluster_id, stack_name, page } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1
-    }`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${
+    page || 1
+  }`;
 });
 
 const createEnvironment = baseApi<
@@ -763,9 +764,11 @@ const detectBuildpack = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 
 const detectGitlabBuildpack = baseApi<
@@ -796,9 +799,11 @@ const getBranchContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 
 const getProcfileContents = baseApi<
@@ -814,9 +819,11 @@ const getProcfileContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 
 const getPorterYamlContents = baseApi<
@@ -832,9 +839,11 @@ const getPorterYamlContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
 });
 
 const parsePorterYaml = baseApi<
@@ -871,9 +880,11 @@ const getBranchHead = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/head`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/head`;
 });
 
 const validatePorterApp = baseApi<
@@ -897,21 +908,21 @@ const validatePorterApp = baseApi<
 
 const createApp = baseApi<
   | {
-    name: string;
-    type: "github";
-    git_repo_id: number;
-    git_branch: string;
-    git_repo_name: string;
-    porter_yaml_path: string;
-  }
+      name: string;
+      type: "github";
+      git_repo_id: number;
+      git_branch: string;
+      git_repo_name: string;
+      porter_yaml_path: string;
+    }
   | {
-    name: string;
-    type: "docker-registry";
-    image: {
-      repository: string;
-      tag: string;
-    };
-  },
+      name: string;
+      type: "docker-registry";
+      image: {
+        repository: string;
+        tag: string;
+      };
+    },
   {
     project_id: number;
     cluster_id: number;
@@ -959,6 +970,18 @@ const getLatestRevision = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/apps/${porter_app_name}/latest`;
 });
 
+const getRevision = baseApi<
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+    porter_app_name: string;
+    revision_id: string;
+  }
+>("GET", ({ project_id, cluster_id, porter_app_name, revision_id }) => {
+  return `/api/projects/${project_id}/clusters/${cluster_id}/apps/${porter_app_name}/revisions/${revision_id}`;
+});
+
 const listAppRevisions = baseApi<
   {
     deployment_target_id: string;
@@ -1886,9 +1909,11 @@ const getEnvGroup = baseApi<
     version?: number;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
-    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
-    }`;
+  return `/api/projects/${pathParams.id}/clusters/${
+    pathParams.cluster_id
+  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
+    pathParams.version ? "&version=" + pathParams.version : ""
+  }`;
 });
 
 const getConfigMap = baseApi<
@@ -2945,7 +2970,7 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
+const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 
 const createSecretAndOpenGitHubPullRequest = baseApi<
   {
@@ -3101,6 +3126,7 @@ export default {
   applyApp,
   getAttachedEnvGroups,
   getLatestRevision,
+  getRevision,
   listAppRevisions,
   getLatestAppRevisions,
   getGitlabProcfileContents,

+ 2 - 2
go.mod

@@ -82,7 +82,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.1.7
+	github.com/porter-dev/api-contracts v0.1.8
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d
@@ -359,7 +359,7 @@ require (
 	k8s.io/component-base v0.25.2 // indirect
 	k8s.io/klog/v2 v2.80.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
-	k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect
+	k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
 	oras.land/oras-go v1.2.0 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.12.1 // indirect

+ 2 - 2
go.sum

@@ -1516,8 +1516,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.1.7 h1:Mxua9qTur0HIhIS4gmK0a9sLcHrgJfFwSQI0CxZBkh4=
-github.com/porter-dev/api-contracts v0.1.7/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.1.8 h1:g8qq2TeN6W6T+FgQfv7RP/sDEFE2CxhK1sm6C4q78e8=
+github.com/porter-dev/api-contracts v0.1.8/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 4 - 1
internal/porter_app/revisions.go

@@ -30,8 +30,10 @@ type Revision struct {
 	CreatedAt time.Time `json:"created_at"`
 	// UpdatedAt is the time the revision was updated
 	UpdatedAt time.Time `json:"updated_at"`
+	// DeploymentTargetID is the id of the deployment target the revision is associated with
+	DeploymentTargetID string `json:"deployment_target_id"`
 	// Env is the environment variables for the revision
-	Env environment_groups.EnvironmentGroup `json:"env"`
+	Env environment_groups.EnvironmentGroup `json:"env,omitempty"`
 }
 
 // GetAppRevisionInput is the input struct for GetAppRevisions
@@ -109,6 +111,7 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi
 		RevisionNumber: appRevision.RevisionNumber,
 		CreatedAt:      appRevision.CreatedAt.AsTime(),
 		UpdatedAt:      appRevision.UpdatedAt.AsTime(),
+		DeploymentTargetID: appRevision.DeploymentTargetId,
 	}
 
 	return revision, nil