Skip to content

Commit

Permalink
Porter App V2 Revision Diff (#3772)
Browse files Browse the repository at this point in the history
  • Loading branch information
Feroze Mohideen authored Oct 10, 2023
1 parent d428405 commit c562784
Show file tree
Hide file tree
Showing 12 changed files with 635 additions and 85 deletions.
112 changes: 112 additions & 0 deletions api/server/handlers/porter_app/yaml_from_revision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package porter_app

import (
"encoding/base64"
"net/http"

"connectrpc.com/connect"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"gopkg.in/yaml.v2"

v2 "github.com/porter-dev/porter/internal/porter_app/v2"
"github.com/porter-dev/porter/internal/telemetry"

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

// PorterYAMLFromRevisionHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
type PorterYAMLFromRevisionHandler struct {
handlers.PorterHandlerReadWriter
authz.KubernetesAgentGetter
}

// NewPorterYAMLFromRevisionHandler returns a new PorterYAMLFromRevisionHandler
func NewPorterYAMLFromRevisionHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *PorterYAMLFromRevisionHandler {
return &PorterYAMLFromRevisionHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
}
}

// PorterYAMLFromRevisionResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
type PorterYAMLFromRevisionResponse struct {
B64PorterYAML string `json:"b64_porter_yaml"`
}

// ServeHTTP takes a porter app revision and returns the porter yaml for it
func (c *PorterYAMLFromRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-porter-yaml-from-revision")
defer span.End()

project, _ := r.Context().Value(types.ProjectScope).(*models.Project)

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
}

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
}

if ccpResp.Msg.AppRevision == nil {
err = telemetry.Error(ctx, span, nil, "app revision is nil")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

appProto := ccpResp.Msg.AppRevision.App
if appProto == nil {
err = telemetry.Error(ctx, span, nil, "app proto is nil")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

app, err := v2.AppFromProto(appProto)
if err != nil {
err = telemetry.Error(ctx, span, err, "error converting app proto to porter yaml")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

porterYAMLString, err := yaml.Marshal(app)
if err != nil {
err = telemetry.Error(ctx, span, err, "error marshaling porter yaml")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

b64String := base64.StdEncoding.EncodeToString(porterYAMLString)

response := &PorterYAMLFromRevisionResponse{
B64PorterYAML: b64String,
}

c.WriteResult(w, r, response)
}
31 changes: 30 additions & 1 deletion api/server/router/porter_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ func getPorterAppRoutes(
Router: r,
})

// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/parse -> porter_app.NewParsePorterYAMLToProtoHandler
// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/parse -> porter_app.NewParsePorterYAMLToProtoHandler
parsePorterYAMLToProtoEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Expand Down Expand Up @@ -601,6 +601,35 @@ func getPorterAppRoutes(
Router: r,
})

// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/yaml -> porter_app.NewPorterYAMLFromRevisionHandler
porterYAMLFromRevision := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Method: types.HTTPVerbGet,
Path: &types.Path{
Parent: basePath,
RelativePath: fmt.Sprintf("%s/{%s}/revisions/{%s}/yaml", relPathV2, types.URLParamPorterAppName, types.URLParamAppRevisionID),
},
Scopes: []types.PermissionScope{
types.UserScope,
types.ProjectScope,
types.ClusterScope,
},
},
)

porterYAMLFromRevisionHandler := porter_app.NewPorterYAMLFromRevisionHandler(
config,
factory.GetDecoderValidator(),
factory.GetResultWriter(),
)

routes = append(routes, &router.Route{
Endpoint: porterYAMLFromRevision,
Handler: porterYAMLFromRevisionHandler,
Router: r,
})

// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/validate -> porter_app.NewValidatePorterAppHandler
validatePorterAppEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Expand Down
6 changes: 4 additions & 2 deletions dashboard/src/lib/hooks/useRevisionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ export function useRevisionList({
deploymentTargetId: string,
projectId: number,
clusterId: number
}): { revisionList: AppRevision[], revisionIdToNumber: Record<string, number> } {
}): { revisionList: AppRevision[], revisionIdToNumber: Record<string, number>, numberToRevisionId: Record<number, string> } {
const [
revisionList,
setRevisionList,
] = useState<AppRevision[]>([]);
const [revisionIdToNumber, setRevisionIdToNumber] = useState<Record<string, number>>({});
const [numberToRevisionId, setNumberToRevisionId] = useState<Record<number, string>>({});
const { latestRevision } = useLatestRevision();

const { data } = useQuery(
Expand Down Expand Up @@ -56,8 +57,9 @@ export function useRevisionList({
const revisionList = data.app_revisions
setRevisionList(revisionList);
setRevisionIdToNumber(Object.fromEntries(revisionList.map(r => ([r.id, r.revision_number]))))
setNumberToRevisionId(Object.fromEntries(revisionList.map(r => ([r.revision_number, r.id]))))
}
}, [data]);

return { revisionList, revisionIdToNumber };
return { revisionList, revisionIdToNumber, numberToRevisionId };
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AnimateHeight from "react-animate-height";
import ServiceStatusDetail from "./ServiceStatusDetail";
import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
import { useRevisionList } from "lib/hooks/useRevisionList";
import RevisionDiffModal from "../modals/RevisionDiffModal";

type Props = {
event: PorterAppDeployEvent;
Expand All @@ -29,7 +30,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId,
const [revertModalVisible, setRevertModalVisible] = useState(false);
const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail);

const { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });
const { revisionIdToNumber, numberToRevisionId } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });

const renderStatusText = () => {
switch (event.status) {
Expand Down Expand Up @@ -120,6 +121,36 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId,
}
};

const renderRevisionDiffModal = (event: PorterAppDeployEvent) => {
const changedRevisionId = event.metadata.app_revision_id;
const changedRevisionNumber = revisionIdToNumber[event.metadata.app_revision_id];
if (changedRevisionNumber == null || changedRevisionNumber == 1) {
return null;
}
const baseRevisionNumber = revisionIdToNumber[event.metadata.app_revision_id] - 1;
if (numberToRevisionId[baseRevisionNumber] == null) {
return null;
}
const baseRevisionId = numberToRevisionId[baseRevisionNumber];
return (
<>
<Link hasunderline onClick={() => setDiffModalVisible(true)}>
View changes
</Link>
{diffModalVisible && (
<RevisionDiffModal
base={{ revisionId: baseRevisionId, revisionNumber: baseRevisionNumber }}
changed={{ revisionId: changedRevisionId, revisionNumber: changedRevisionNumber }}
close={() => setDiffModalVisible(false)}
projectId={projectId}
clusterId={clusterId}
appName={appName}
/>
)}
</>
)
}

const renderServiceDropdownCta = (numServices: number, color?: string) => {
return (
<ServiceStatusDropdownCtaContainer >
Expand All @@ -146,7 +177,8 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId,
<Icon height="12px" src={getStatusIcon(event.status)} />
<Spacer inline width="10px" />
{renderStatusText()}
{revisionIdToNumber[event.metadata.app_revision_id] != null && latestRevision.revision_number !== revisionIdToNumber[event.metadata.app_revision_id] && (
{/** uncomment the below once we've implemented revert from here */}
{/* {revisionIdToNumber[event.metadata.app_revision_id] != null && latestRevision.revision_number !== revisionIdToNumber[event.metadata.app_revision_id] && (
<>
<Spacer inline x={1} />
<TempWrapper>
Expand All @@ -156,32 +188,9 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId,
</TempWrapper>
</>
)}
<Spacer inline x={1} />
{/* <TempWrapper>
{event.metadata.revision != 1 && (<Link hasunderline onClick={() => setDiffModalVisible(true)}>
View changes
</Link>)}
{diffModalVisible && (
<ChangeLogModal
revision={event.metadata.revision}
currentChart={appData.chart}
modalVisible={diffModalVisible}
setModalVisible={setDiffModalVisible}
appData={appData}
/>
)}
{revertModalVisible && (
<ChangeLogModal
revision={event.metadata.revision}
currentChart={appData.chart}
modalVisible={revertModalVisible}
setModalVisible={setRevertModalVisible}
revertModal={true}
appData={appData}
/>
)}
</TempWrapper> */}
)} */}
<Spacer inline x={0.5} />
{renderRevisionDiffModal(event)}
</Container>
</Container>
{event.metadata.service_deployment_metadata != null &&
Expand Down
Loading

0 comments on commit c562784

Please sign in to comment.