You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/07/27 01:20:30 UTC
(airavata-custos) 14/18: Show the cluster username in the access status panel
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch pearc26
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit 9da7089614e798c61d55e600871d94bb3ce51be3
Author: lahiruj <la...@gmail.com>
AuthorDate: Sat Jul 25 04:01:39 2026 -0400
Show the cluster username in the access status panel
---
internal/server/access_status.go | 28 +++++++-----
internal/server/access_status_integration_test.go | 38 ++++++++++++++++
pkg/service/access_status.go | 51 ++++++++++++++++------
.../__tests__/AllocationAccessBand.test.tsx | 3 ++
.../components/AllocationAccessBand.tsx | 15 +++++--
web/src/features/core/access-status/schemas.ts | 2 +
6 files changed, 108 insertions(+), 29 deletions(-)
diff --git a/internal/server/access_status.go b/internal/server/access_status.go
index 5ddfc697c..0997eeca5 100644
--- a/internal/server/access_status.go
+++ b/internal/server/access_status.go
@@ -51,15 +51,17 @@ type accessCheckEventResponse struct {
}
type accessStatusResponse struct {
- Checks []accessCheckResponse `json:"checks"`
- Events []accessCheckEventResponse `json:"events"`
+ Checks []accessCheckResponse `json:"checks"`
+ Events []accessCheckEventResponse `json:"events"`
+ LocalUsername string `json:"local_username,omitempty"`
}
type memberAccessStatusResponse struct {
- UserID string `json:"user_id"`
- DisplayName string `json:"display_name"`
- Email string `json:"email"`
- Checks []accessCheckResponse `json:"checks"`
+ UserID string `json:"user_id"`
+ DisplayName string `json:"display_name"`
+ Email string `json:"email"`
+ LocalUsername string `json:"local_username,omitempty"`
+ Checks []accessCheckResponse `json:"checks"`
}
type allocationAccessMembersResponse struct {
@@ -87,10 +89,11 @@ func (s *Server) getAllocationAccessStatus(w http.ResponseWriter, r *http.Reques
now := time.Now().UTC()
for _, m := range rows {
resp.Members = append(resp.Members, memberAccessStatusResponse{
- UserID: m.UserID,
- DisplayName: m.DisplayName,
- Email: m.Email,
- Checks: s.checkResponses(m.Checks, now),
+ UserID: m.UserID,
+ DisplayName: m.DisplayName,
+ Email: m.Email,
+ LocalUsername: m.LocalUsername,
+ Checks: s.checkResponses(m.Checks, now),
})
}
common.WriteJSON(w, http.StatusOK, resp)
@@ -104,8 +107,9 @@ func (s *Server) getAllocationAccessStatus(w http.ResponseWriter, r *http.Reques
}
now := time.Now().UTC()
resp := accessStatusResponse{
- Checks: s.checkResponses(status.Checks, now),
- Events: make([]accessCheckEventResponse, 0, len(status.Events)),
+ Checks: s.checkResponses(status.Checks, now),
+ Events: make([]accessCheckEventResponse, 0, len(status.Events)),
+ LocalUsername: status.LocalUsername,
}
typeByCheckID := make(map[string]models.AccessCheckType, len(status.Checks))
for _, c := range status.Checks {
diff --git a/internal/server/access_status_integration_test.go b/internal/server/access_status_integration_test.go
index c55a2adf8..7db2db36c 100644
--- a/internal/server/access_status_integration_test.go
+++ b/internal/server/access_status_integration_test.go
@@ -119,6 +119,7 @@ func TestAccessStatus_SelfViewWithDerivedAndRealChecks(t *testing.T) {
CheckType string `json:"check_type"`
EventType string `json:"event_type"`
} `json:"events"`
+ LocalUsername string `json:"local_username"`
}
if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
t.Fatalf("decode: %v", err)
@@ -143,6 +144,43 @@ func TestAccessStatus_SelfViewWithDerivedAndRealChecks(t *testing.T) {
}
}
+func TestAccessStatus_CarriesLocalUsername(t *testing.T) {
+ fx := setupAccessStatusFixture(t)
+ var clusterID string
+ if err := fx.db.Get(&clusterID,
+ `SELECT compute_cluster_id FROM compute_allocations WHERE id = ?`, fx.allocID); err != nil {
+ t.Fatalf("cluster id: %v", err)
+ }
+ if _, err := fx.db.Exec(
+ `INSERT INTO compute_cluster_users (id, compute_cluster_id, user_id, local_username, provisioned_at)
+ VALUES (?, ?, ?, 'memberlocal', NOW(6))`, uuid.NewString(), clusterID, fx.member); err != nil {
+ t.Fatalf("seed cluster user: %v", err)
+ }
+
+ rr := fx.get(t, "/compute-allocations/"+fx.allocID+"/access-status", fx.member)
+ if rr.Code != http.StatusOK {
+ t.Fatalf("status: got %d", rr.Code)
+ }
+ var got struct {
+ LocalUsername string `json:"local_username"`
+ Checks []struct {
+ Type string `json:"type"`
+ UIState string `json:"ui_state"`
+ } `json:"checks"`
+ }
+ if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if got.LocalUsername != "memberlocal" {
+ t.Errorf("local_username: got %q, want memberlocal", got.LocalUsername)
+ }
+ for _, c := range got.Checks {
+ if c.Type == "SIGN_IN" && c.UIState != "ok" {
+ t.Errorf("provisioned account should derive sign-in ok, got %q", c.UIState)
+ }
+ }
+}
+
func TestAccessStatus_StuckDerivation(t *testing.T) {
fx := setupAccessStatusFixture(t)
rec := func(ok bool) {
diff --git a/pkg/service/access_status.go b/pkg/service/access_status.go
index dea3189ed..2a0b933db 100644
--- a/pkg/service/access_status.go
+++ b/pkg/service/access_status.go
@@ -31,18 +31,21 @@ import (
const DefaultAccessCheckStuckAfter = time.Hour
// AccessStatus is a user's checks on one allocation plus their milestone
-// history, newest first.
+// history, newest first. LocalUsername is the account the user signs in
+// with on the allocation's cluster, empty until one is assigned.
type AccessStatus struct {
- Checks []models.AccessCheck
- Events []models.AccessCheckEvent
+ Checks []models.AccessCheck
+ Events []models.AccessCheckEvent
+ LocalUsername string
}
// MemberAccessStatus is one allocation member's checks, for the manager view.
type MemberAccessStatus struct {
- UserID string
- DisplayName string
- Email string
- Checks []models.AccessCheck
+ UserID string
+ DisplayName string
+ Email string
+ LocalUsername string
+ Checks []models.AccessCheck
}
// SetAccessCheckStuckAfter overrides the failing-to-stuck threshold.
@@ -182,12 +185,17 @@ func (s *Service) AccessStatusForUser(ctx context.Context, allocationID, userID
if err != nil {
return nil, err
}
- checks, realIDs := s.fillDefaultChecks(ctx, alloc, allocationID, userID, checks)
+ cu := s.clusterUserFor(ctx, alloc, userID)
+ checks, realIDs := fillDefaultChecks(allocationID, userID, cu, checks)
events, err := s.accessChecks.FindEventsByChecks(ctx, realIDs)
if err != nil {
return nil, err
}
- return &AccessStatus{Checks: checks, Events: events}, nil
+ status := &AccessStatus{Checks: checks, Events: events}
+ if cu != nil {
+ status.LocalUsername = cu.LocalUsername
+ }
+ return status, nil
}
// AccessStatusForAllocation returns every ACTIVE member's checks, for the
@@ -217,20 +225,35 @@ func (s *Service) AccessStatusForAllocation(ctx context.Context, allocationID st
if m.MembershipStatus != models.ACTIVE {
continue
}
- mc, _ := s.fillDefaultChecks(ctx, alloc, allocationID, m.UserID, byUser[m.UserID])
- out = append(out, MemberAccessStatus{
+ cu := s.clusterUserFor(ctx, alloc, m.UserID)
+ mc, _ := fillDefaultChecks(allocationID, m.UserID, cu, byUser[m.UserID])
+ row := MemberAccessStatus{
UserID: m.UserID,
DisplayName: m.DisplayName,
Email: m.Email,
Checks: mc,
- })
+ }
+ if cu != nil {
+ row.LocalUsername = cu.LocalUsername
+ }
+ out = append(out, row)
}
return out, nil
}
+// clusterUserFor returns the user's account on the allocation's cluster, or
+// nil when none is assigned yet (or the lookup fails, which reads the same).
+func (s *Service) clusterUserFor(ctx context.Context, alloc *models.ComputeAllocation, userID string) *models.ComputeClusterUser {
+ cu, err := s.clusterUsers.FindByPair(ctx, alloc.ComputeClusterID, userID)
+ if err != nil {
+ return nil
+ }
+ return cu
+}
+
// fillDefaultChecks appends synthetic checks (empty ID, no history) for the
// types no probe has reported yet, and returns the real check ids.
-func (s *Service) fillDefaultChecks(ctx context.Context, alloc *models.ComputeAllocation, allocationID, userID string, checks []models.AccessCheck) ([]models.AccessCheck, []string) {
+func fillDefaultChecks(allocationID, userID string, cu *models.ComputeClusterUser, checks []models.AccessCheck) ([]models.AccessCheck, []string) {
have := make(map[models.AccessCheckType]bool, len(checks))
realIDs := make([]string, 0, len(checks))
for _, c := range checks {
@@ -244,7 +267,7 @@ func (s *Service) fillDefaultChecks(ctx context.Context, alloc *models.ComputeAl
CheckType: models.AccessCheckSignIn, Status: models.AccessCheckPending,
LastCheckedAt: now,
}
- if cu, err := s.clusterUsers.FindByPair(ctx, alloc.ComputeClusterID, userID); err == nil && cu != nil && cu.ProvisionedAt != nil {
+ if cu != nil && cu.ProvisionedAt != nil {
c.Status = models.AccessCheckOK
c.LastCheckedAt = *cu.ProvisionedAt
c.LastOKAt = cu.ProvisionedAt
diff --git a/web/src/features/core/access-status/__tests__/AllocationAccessBand.test.tsx b/web/src/features/core/access-status/__tests__/AllocationAccessBand.test.tsx
index a6afd8934..054d12a34 100644
--- a/web/src/features/core/access-status/__tests__/AllocationAccessBand.test.tsx
+++ b/web/src/features/core/access-status/__tests__/AllocationAccessBand.test.tsx
@@ -57,6 +57,7 @@ describe("<AllocationAccessBand />", () => {
renderBand({
checks: [check("SIGN_IN", "ok"), check("JOB_SUBMISSION", "ok")],
events: [{ check_type: "SIGN_IN", event_type: "ONLINE", occurred_at: now }],
+ local_username: "lahirujay",
});
expect(screen.getByText("Your access is live")).toBeInTheDocument();
expect(screen.queryByText("Cluster sign-in")).not.toBeInTheDocument();
@@ -64,6 +65,8 @@ describe("<AllocationAccessBand />", () => {
fireEvent.click(screen.getByRole("button", { name: "View checks" }));
expect(screen.getByText("Cluster sign-in")).toBeInTheDocument();
expect(screen.getByText("Job submission")).toBeInTheDocument();
+ expect(screen.getByText("lahirujay")).toBeInTheDocument();
+ expect(screen.getByText(/Signing in over SSH as/)).toBeInTheDocument();
expect(screen.getByText("Cluster sign-in came online")).toBeInTheDocument();
});
diff --git a/web/src/features/core/access-status/components/AllocationAccessBand.tsx b/web/src/features/core/access-status/components/AllocationAccessBand.tsx
index 804210f52..6193be674 100644
--- a/web/src/features/core/access-status/components/AllocationAccessBand.tsx
+++ b/web/src/features/core/access-status/components/AllocationAccessBand.tsx
@@ -105,7 +105,7 @@ export function AllocationAccessBand({
<p className="text-sm text-muted-foreground">{verdictBody(overall, checks)}</p>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{checks.map((c) => (
- <CheckRow key={c.type} check={c} />
+ <CheckRow key={c.type} check={c} localUsername={query.data?.local_username} />
))}
</div>
{events.length > 0 ? <Timeline events={events} /> : null}
@@ -115,13 +115,22 @@ export function AllocationAccessBand({
);
}
-function CheckRow({ check }: { check: AccessCheck }) {
+function CheckRow({ check, localUsername }: { check: AccessCheck; localUsername?: string | null }) {
const chip = CHECK_CHIP[check.ui_state];
return (
<div className="flex items-start justify-between gap-3 rounded-lg border border-border p-3">
<div className="space-y-0.5">
<div className="text-sm font-medium">{CHECK_NAME[check.type]}</div>
- <div className="text-xs text-muted-foreground">{CHECK_DESCRIPTION[check.type]}</div>
+ <div className="text-xs text-muted-foreground">
+ {check.type === "SIGN_IN" && localUsername ? (
+ <>
+ Signing in over SSH as{" "}
+ <code className="rounded bg-muted px-1 font-mono text-[11px]">{localUsername}</code>.
+ </>
+ ) : (
+ CHECK_DESCRIPTION[check.type]
+ )}
+ </div>
{check.ui_state === "retrying" ? (
<div className="text-xs text-[color:var(--tone-warn-fg)]">
Retrying automatically. Usually clears within a few minutes.
diff --git a/web/src/features/core/access-status/schemas.ts b/web/src/features/core/access-status/schemas.ts
index b885dc7f2..bde4a000d 100644
--- a/web/src/features/core/access-status/schemas.ts
+++ b/web/src/features/core/access-status/schemas.ts
@@ -43,6 +43,7 @@ export type AccessCheckEvent = z.infer<typeof accessCheckEventSchema>;
export const accessStatusSchema = z.object({
checks: z.array(accessCheckSchema),
events: z.array(accessCheckEventSchema),
+ local_username: z.string().nullish(),
});
export type AccessStatus = z.infer<typeof accessStatusSchema>;
@@ -50,6 +51,7 @@ export const memberAccessStatusSchema = z.object({
user_id: z.string(),
display_name: z.string(),
email: z.string(),
+ local_username: z.string().nullish(),
checks: z.array(accessCheckSchema),
});
export type MemberAccessStatus = z.infer<typeof memberAccessStatusSchema>;