-
Notifications
You must be signed in to change notification settings - Fork 239
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Feroze Mohideen
authored
Sep 20, 2023
1 parent
8b662a3
commit a208a76
Showing
20 changed files
with
790 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
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/kubernetes" | ||
"github.com/porter-dev/porter/internal/models" | ||
"github.com/porter-dev/porter/internal/telemetry" | ||
) | ||
|
||
// JobStatusHandler is the handler for GET /apps/jobs | ||
type JobStatusHandler struct { | ||
handlers.PorterHandlerReadWriter | ||
authz.KubernetesAgentGetter | ||
} | ||
|
||
// NewJobStatusHandler returns a new JobStatusHandler | ||
func NewJobStatusHandler( | ||
config *config.Config, | ||
decoderValidator shared.RequestDecoderValidator, | ||
writer shared.ResultWriter, | ||
) *JobStatusHandler { | ||
return &JobStatusHandler{ | ||
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), | ||
KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config), | ||
} | ||
} | ||
|
||
// JobStatusRequest is the expected format for a request body on GET /apps/jobs | ||
type JobStatusRequest struct { | ||
DeploymentTargetID string `schema:"deployment_target_id"` | ||
JobName string `schema:"job_name"` | ||
} | ||
|
||
func (c *JobStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
ctx, span := telemetry.NewSpan(r.Context(), "serve-job-status") | ||
defer span.End() | ||
|
||
request := &JobStatusRequest{} | ||
if ok := c.DecodeAndValidate(w, r, request); !ok { | ||
err := telemetry.Error(ctx, span, nil, "invalid request") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) | ||
return | ||
} | ||
|
||
cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) | ||
project, _ := ctx.Value(types.ProjectScope).(*models.Project) | ||
|
||
name, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName) | ||
if reqErr != nil { | ||
err := telemetry.Error(ctx, span, reqErr, "invalid porter app name") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) | ||
return | ||
} | ||
|
||
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: name}) | ||
|
||
if request.DeploymentTargetID == "" { | ||
err := telemetry.Error(ctx, span, nil, "must provide deployment target id") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) | ||
return | ||
} | ||
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID}) | ||
|
||
deploymentTargetDetailsReq := connect.NewRequest(&porterv1.DeploymentTargetDetailsRequest{ | ||
ProjectId: int64(project.ID), | ||
DeploymentTargetId: request.DeploymentTargetID, | ||
}) | ||
|
||
deploymentTargetDetailsResp, err := c.Config().ClusterControlPlaneClient.DeploymentTargetDetails(ctx, deploymentTargetDetailsReq) | ||
if err != nil { | ||
err := telemetry.Error(ctx, span, err, "error getting deployment target details from cluster control plane client") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) | ||
return | ||
} | ||
|
||
if deploymentTargetDetailsResp == nil || deploymentTargetDetailsResp.Msg == nil { | ||
err := telemetry.Error(ctx, span, err, "deployment target details resp is nil") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) | ||
return | ||
} | ||
|
||
if deploymentTargetDetailsResp.Msg.ClusterId != int64(cluster.ID) { | ||
err := telemetry.Error(ctx, span, err, "deployment target details resp cluster id does not match cluster id") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) | ||
return | ||
} | ||
|
||
namespace := deploymentTargetDetailsResp.Msg.Namespace | ||
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: namespace}) | ||
|
||
agent, err := c.GetAgent(r, cluster, "") | ||
if err != nil { | ||
err = telemetry.Error(ctx, span, err, "unable to get agent") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) | ||
return | ||
} | ||
|
||
labels := []kubernetes.Label{ | ||
{ | ||
Key: "porter.run/deployment-target-id", | ||
Val: request.DeploymentTargetID, | ||
}, | ||
{ | ||
Key: "porter.run/app-name", | ||
Val: name, | ||
}, | ||
} | ||
if request.JobName != "" { | ||
labels = append(labels, kubernetes.Label{ | ||
Key: "porter.run/service-name", | ||
Val: request.JobName, | ||
}) | ||
} | ||
jobs, err := agent.ListJobsByLabel(namespace, labels...) | ||
if err != nil { | ||
err = telemetry.Error(ctx, span, err, "error listing jobs") | ||
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) | ||
return | ||
} | ||
|
||
c.WriteResult(w, r, jobs) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import _ from "lodash"; | ||
import { useEffect, useState } from "react"; | ||
import api from "shared/api"; | ||
import { useRevisionIdToNumber } from "./useRevisionList"; | ||
import { z } from "zod"; | ||
import { useQuery } from "@tanstack/react-query"; | ||
|
||
const jobRunValidator = z.object({ | ||
metadata: z.object({ | ||
labels: z.object({ | ||
"porter.run/app-revision-id": z.string(), | ||
"porter.run/service-name": z.string(), | ||
}), | ||
creationTimestamp: z.string(), | ||
uid: z.string(), | ||
}), | ||
status: z.object({ | ||
startTime: z.string().optional(), | ||
completionTime: z.string().optional(), | ||
conditions: z.array(z.object({ | ||
lastTransitionTime: z.string(), | ||
})).default([]), | ||
succeeded: z.number().optional(), | ||
failed: z.number().optional(), | ||
}), | ||
revisionNumber: z.number().optional(), | ||
jobName: z.string().optional(), | ||
}); | ||
|
||
export type JobRun = z.infer<typeof jobRunValidator>; | ||
|
||
export const useJobs = ( | ||
{ | ||
appName, | ||
projectId, | ||
clusterId, | ||
deploymentTargetId, | ||
selectedJobName, | ||
}: { | ||
appName: string, | ||
projectId: number, | ||
clusterId: number, | ||
deploymentTargetId: string, | ||
selectedJobName: string, | ||
} | ||
) => { | ||
const [jobRuns, setJobRuns] = useState<JobRun[]>([]); | ||
|
||
const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId); | ||
|
||
const { data } = useQuery( | ||
["jobRuns", appName, deploymentTargetId, revisionIdToNumber, selectedJobName], | ||
async () => { | ||
const res = await api.appJobs( | ||
"<token>", | ||
{ | ||
deployment_target_id: deploymentTargetId, | ||
job_name: selectedJobName === "all" ? "" : selectedJobName, | ||
}, | ||
{ | ||
project_id: projectId, | ||
cluster_id: clusterId, | ||
porter_app_name: appName, | ||
}); | ||
const parsed = await z.array(jobRunValidator).parseAsync(res.data); | ||
const parsedWithRevision = parsed.map((jobRun) => { | ||
const revisionId = jobRun.metadata.labels["porter.run/app-revision-id"]; | ||
const revisionNumber = revisionIdToNumber[revisionId]; | ||
return { | ||
...jobRun, | ||
revisionNumber, | ||
jobName: jobRun.metadata.labels["porter.run/service-name"], | ||
}; | ||
}); | ||
return parsedWithRevision; | ||
}, | ||
{ | ||
enabled: revisionIdToNumber != null, | ||
refetchInterval: 5000, | ||
refetchOnWindowFocus: false, | ||
}, | ||
); | ||
|
||
useEffect(() => { | ||
if (data != null) { | ||
setJobRuns(data); | ||
} | ||
}, [data]); | ||
|
||
return { | ||
jobRuns, | ||
}; | ||
}; |
Oops, something went wrong.