Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/components/GameServerSetLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Contributors to Agones a Series of LF Projects, LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Router } from '@kinvolk/headlamp-plugin/lib';
import React from 'react';

interface GameServerSetLinkProps {
namespace: string;
name: string;
onClick?: (e: React.MouseEvent) => void;
}

export function GameServerSetLink({ namespace, name, onClick }: GameServerSetLinkProps) {
const url = Router.createRouteURL('agones-gameserverset', { namespace, name });
return (
<a href={url} onClick={onClick}>
{name}
</a>
);
}
34 changes: 34 additions & 0 deletions src/components/GameServerSetPhaseChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Contributors to Agones a Series of LF Projects, LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Chip from '@mui/material/Chip';
import React from 'react';
import { GameServerSetPhase } from '../utils/gameServerSetHelpers';

const PHASE_STYLE: Record<
GameServerSetPhase,
{ label: string; color: 'default' | 'success' | 'warning' }
> = {
active: { label: 'Active', color: 'success' },
retiring: { label: 'Retiring', color: 'warning' },
unknown: { label: '—', color: 'default' },
};

export function GameServerSetPhaseChip({ phase }: { phase: GameServerSetPhase }) {
const { label, color } = PHASE_STYLE[phase];
if (phase === 'unknown') return <>{label}</>;
return <Chip label={label} color={color} size="small" />;
}
2 changes: 2 additions & 0 deletions src/components/StateChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ type ChipColor = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'succe

const STATE_COLORS: Record<string, ChipColor> = {
// In-progress lifecycle states
PortAllocation: 'info',
Creating: 'info',
Starting: 'info',
Scheduled: 'info',
RequestReady: 'info',
// Stable states
Expand Down
6 changes: 6 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { FleetDetail } from './views/fleets/Detail';
import { FleetList } from './views/fleets/List';
import { GameServerDetail } from './views/gameservers/Detail';
import { GameServerList } from './views/gameservers/List';
import { GameServerSetDetail } from './views/gameserversets/Detail';
import { GameServerSetList } from './views/gameserversets/List';
import { AgonesOverview } from './views/overview/Overview';

// ─── Map ─────────────────────────────────────────────────────────────────────
Expand All @@ -35,6 +37,7 @@ registerSidebarEntry({ parent: null, name: 'agones', label: 'Agones', url: '/ago
registerSidebarEntry({ parent: 'agones', name: 'agones-overview', label: 'Overview', url: '/agones' });
registerSidebarEntry({ parent: 'agones', name: 'agones-fleets', label: 'Fleets', url: '/agones/fleets' });
registerSidebarEntry({ parent: 'agones', name: 'agones-gameservers', label: 'Game Servers', url: '/agones/gameservers' });
registerSidebarEntry({ parent: 'agones', name: 'agones-gameserversets', label: 'Game Server Sets', url: '/agones/gameserversets' });
registerSidebarEntry({ parent: 'agones', name: 'agones-fleetautoscalers', label: 'Autoscalers', url: '/agones/fleetautoscalers' });

// ─── Routes ──────────────────────────────────────────────────────────────────
Expand All @@ -47,5 +50,8 @@ registerRoute({ path: '/agones/fleets/:namespace/:name', sidebar: 'agones-fleets
registerRoute({ path: '/agones/gameservers', sidebar: 'agones-gameservers', name: 'agones-gameservers', exact: true, component: () => <GameServerList /> });
registerRoute({ path: '/agones/gameservers/:namespace/:name', sidebar: 'agones-gameservers', name: 'agones-gameserver', component: () => <GameServerDetail /> });

registerRoute({ path: '/agones/gameserversets', sidebar: 'agones-gameserversets', name: 'agones-gameserversets', exact: true, component: () => <GameServerSetList /> });
registerRoute({ path: '/agones/gameserversets/:namespace/:name', sidebar: 'agones-gameserversets', name: 'agones-gameserverset', component: () => <GameServerSetDetail /> });

registerRoute({ path: '/agones/fleetautoscalers', sidebar: 'agones-fleetautoscalers', name: 'agones-fleetautoscalers', exact: true, component: () => <FleetAutoscalerList /> });
registerRoute({ path: '/agones/fleetautoscalers/:namespace/:name', sidebar: 'agones-fleetautoscalers', name: 'agones-fleetautoscaler', component: () => <FleetAutoscalerDetail /> });
3 changes: 2 additions & 1 deletion src/mapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

import { GraphSource } from '@kinvolk/headlamp-plugin/lib/components/resourceMap/graph/graphModel';
import { fleetsSource } from './views/map/fleetsSource';
import { gameServerSetsSource } from './views/map/gameServerSetsSource';
import { gameServersSource } from './views/map/gameServersSource';
import { podsSource } from './views/map/podsSource';

export const agonesMapSource: GraphSource = {
id: 'agones',
label: 'Agones',
sources: [fleetsSource, gameServersSource, podsSource],
sources: [fleetsSource, gameServerSetsSource, gameServersSource, podsSource],
};
5 changes: 5 additions & 0 deletions src/resources/gameserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { KubeObject, KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/k8s/cluster';
import { GAME_SERVER_SET_LABEL } from '../utils/agonesLabels';

export interface GameServerSpecPort {
name?: string;
Expand Down Expand Up @@ -101,6 +102,10 @@ export class GameServer extends KubeObject<AgonesGameServer> {
return this.metadata.labels?.['agones.dev/fleet'] ?? '';
}

get gameServerSet(): string {
return this.metadata.labels?.[GAME_SERVER_SET_LABEL] ?? '';
}

get ports(): string {
const ports = this.status.ports ?? [];
if (ports.length === 0) return '';
Expand Down
84 changes: 84 additions & 0 deletions src/resources/gameserverset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright Contributors to Agones a Series of LF Projects, LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { KubeObject, KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/k8s/cluster';
import { FLEET_NAME_LABEL } from '../utils/agonesLabels';

export interface AgonesGameServerSet extends KubeObjectInterface {
spec: {
replicas: number;
scheduling: string;
template: object;
};
status?: {
replicas?: number;
readyReplicas?: number;
reservedReplicas?: number;
allocatedReplicas?: number;
shutdownReplicas?: number;
};
}

export class GameServerSet extends KubeObject<AgonesGameServerSet> {
static apiVersion = 'agones.dev/v1';
static kind = 'GameServerSet';
static apiName = 'gameserversets';
static isNamespaced = true;

static get detailsRoute() {
return 'agones-gameserverset';
}

get spec() {
return this.jsonData.spec;
}

get status() {
return this.jsonData.status || {};
}

get fleet(): string {
return this.metadata.labels?.[FLEET_NAME_LABEL] ?? '';
}

get scheduling(): string {
return this.spec.scheduling || 'Packed';
}

get desiredReplicas(): number {
return this.spec.replicas || 0;
}

get currentReplicas(): number {
return this.status.replicas || 0;
}

get readyReplicas(): number {
return this.status.readyReplicas || 0;
}

get allocatedReplicas(): number {
return this.status.allocatedReplicas || 0;
}

get reservedReplicas(): number {
return this.status.reservedReplicas || 0;
}

get shutdownReplicas(): number {
return this.status.shutdownReplicas || 0;
}
}
20 changes: 20 additions & 0 deletions src/utils/agonesLabels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Contributors to Agones a Series of LF Projects, LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/** Agones label keys (pkg/apis/agones/v1). */
export const FLEET_NAME_LABEL = 'agones.dev/fleet';
export const GAME_SERVER_SET_LABEL = 'agones.dev/gameserverset';
export const GAME_SERVER_POD_LABEL = 'agones.dev/gameserver';
76 changes: 76 additions & 0 deletions src/utils/gameServerSetHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Contributors to Agones a Series of LF Projects, LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { KubeObject } from '@kinvolk/headlamp-plugin/lib/k8s/cluster';
import { Fleet } from '../resources/fleet';
import { GameServerSet } from '../resources/gameserverset';
import { FLEET_NAME_LABEL } from './agonesLabels';

/** Mirrors metav1.IsControlledBy — fleet must be the controlling owner. */
export function isControlledBy(child: KubeObject, owner: KubeObject): boolean {
const refs = child.metadata.ownerReferences ?? [];
return refs.some(
ref => ref.uid === owner.metadata.uid && ref.controller === true
);
}

/** Template equality check (Agones uses Semantic.DeepEqual on spec.template). */
export function templatesMatch(
a: { template?: object },
b: { template?: object }
): boolean {
return JSON.stringify(a.template ?? {}) === JSON.stringify(b.template ?? {});
}

export type GameServerSetPhase = 'active' | 'retiring' | 'unknown';

/**
* Active when spec.template matches the fleet's template (pkg/fleets/controller.go).
* Among duplicates, the oldest creationTimestamp wins as active.
*/
export function getGameServerSetPhase(
gss: GameServerSet,
fleet: Fleet | undefined,
siblings: GameServerSet[]
): GameServerSetPhase {
if (!fleet) return 'unknown';
if (!templatesMatch(gss.spec, fleet.spec)) return 'retiring';

const matching = siblings.filter(s => templatesMatch(s.spec, fleet.spec));
if (matching.length <= 1) return 'active';

const oldest = matching.reduce((a, b) =>
new Date(a.metadata.creationTimestamp ?? 0) <=
new Date(b.metadata.creationTimestamp ?? 0)
? a
: b
);
return oldest.metadata.uid === gss.metadata.uid ? 'active' : 'retiring';
}

/** Fleet-owned GameServerSets: label selector + ownerRef filter (pkg/fleets/fleets.go). */
export function filterGameServerSetsByFleet(
sets: GameServerSet[],
fleet: Fleet
): GameServerSet[] {
const fleetName = fleet.metadata.name;
return sets.filter(
gss =>
gss.metadata.namespace === fleet.metadata.namespace &&
gss.metadata.labels?.[FLEET_NAME_LABEL] === fleetName &&
isControlledBy(gss, fleet)
);
}
2 changes: 2 additions & 0 deletions src/views/fleets/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ReplicaBar } from '../../components/ReplicaBar';
import { UtilBar } from '../../components/UtilBar';
import { Fleet } from '../../resources/fleet';
import { GameServer } from '../../resources/gameserver';
import { GameServerSetsSection } from './GameServerSetsSection';

interface WithGameServers { gameServers: GameServer[] | null }

Expand Down Expand Up @@ -143,6 +144,7 @@ export function FleetDetail() {
}
extraSections={item =>
item && [
<GameServerSetsSection fleet={item} />,
<AggregateCapacitySection gameServers={gameServers ?? null} />,
<GameServersSection gameServers={gameServers ?? null} />,
<AllocateSection fleet={item} />,
Expand Down
Loading