mit einem Klick
add-resource
// Add support for a Kubernetes resource type by choosing the required catalog, refresh, detail, object-map, permission, frontend, docs, and test surfaces
// Add support for a Kubernetes resource type by choosing the required catalog, refresh, detail, object-map, permission, frontend, docs, and test surfaces
Work on Luxury Yacht object-panel details, YAML, actions, logs, shell/debug tabs, docked panels, related objects, and tests
Work on Luxury Yacht RBAC permission checks, capability descriptors, permission-denied diagnostics, object action availability, YAML/edit/delete/scale/restart gating, and capability tests
Work on Luxury Yacht canonical Kubernetes resource identity, status presentation, facts, ResourceLink relationships, DTO projection, table/detail/object-map parity, and shared resource model tests
Guide for safely modifying the refresh/streaming subsystem โ covers the full domain lifecycle, registration points, and known fragility areas
Work on Luxury Yacht kubeconfig selection, multi-cluster client lifecycle, auth failure/recovery, selected/background clusters, cluster tabs, refresh subsystem rebuilds, and object catalog lifecycle
Work on logs, shell exec, debug containers, port-forward, node drain/maintenance, long-running operations, permissions, lifecycle, and cleanup tests
| name | add-resource |
| description | Add support for a Kubernetes resource type by choosing the required catalog, refresh, detail, object-map, permission, frontend, docs, and test surfaces |
Add support for a Kubernetes resource type. Resource support can span several surfaces; decide the required surfaces up front instead of assuming this is only a rich-detail task.
This skill covers rich object detail/action support. If the resource also needs
to appear in a table or refresh-driven view, add a refresh-domain payload under
backend/refresh/snapshot and wire the matching frontend refresh domain. Do not
add new list/table payloads to backend/resources; that package is the
detail/action service layer.
Before editing code, decide which surfaces the resource needs:
| Surface | Backend Entry Points | Frontend Entry Points | Notes |
|---|---|---|---|
| Discovery/catalog/browse | backend/objectcatalog, backend/refresh/snapshot/catalog.go | frontend/src/modules/browse | The object catalog owns existence, GVK/GVR, scope, namespace listings, and cluster listings. |
| Refresh table/list | backend/refresh/snapshot/*.go, backend/refresh/system/registrations.go | frontend/src/core/refresh/*, GridTable consumers | Canonical list/table data belongs in refresh snapshots. |
| Resource stream rows | backend/refresh/resourcestream | frontend/src/core/refresh/streaming | Stream row shape must match snapshot row shape. |
| Rich object details/actions | backend/resources, backend/object_detail_provider.go, backend/resources/types | Object-panel details/overview registry | Use for detail tabs, logs/debug helpers, and imperative operations. |
| Shared identity/status/links/facts | backend/resourcemodel | status/link rendering utilities | Backend owns primary status and relationship semantics. |
| YAML/edit/apply | object YAML/read/apply backend paths | object-panel YAML tab | Must carry clusterId and full GVK identity. |
| Object map | .agents/skills/object-map/SKILL.md, backend/refresh/snapshot/object_map.go | frontend/src/modules/object-map, object-panel support list | Fix backend graph data before frontend renderer/allowlist changes. |
| Permissions/capabilities | refresh permission checks, capability backends | RBAC-gated UI/action surfaces | Keep permission-denied diagnostics visible. |
| Docs/tests | owning architecture/workflow docs | adjacent specs/stories when useful | Update durable docs when contracts or supported kinds change. |
If the requested task only needs one surface, keep the implementation scoped to that surface. If user-visible support would be incomplete without another surface, explain the tradeoff before narrowing.
/add-resource <Kind> โ e.g., /add-resource CronJob, /add-resource Ingress
backend/resources/ to find the right category directory (workloads, network, storage, config, policy, etc.). If none fits, create a new one. For table/list data, use backend/refresh/snapshot instead.backend/resources/workloads/deployments.go for workloads, backend/resources/network/ for networking resources.docs/architecture/shared-resource-model.md before adding status, relationship links, capability checks, or object references. The backend owns status semantics; frontend status classes come from statusPresentation; relationship links use resourcemodel.ResourceLink; object references must carry clusterId, group, version, kind, and concrete object names.
Do not guess resource from kind, and do not treat an empty Kubernetes
apiVersion as core v1.docs/architecture/refresh-system.md, docs/architecture/data-access.md,
and .agents/context/code-map.md.Directory: backend/resourcemodel
If the resource has primary status, lifecycle, durable facts, relationships, or links, add or update the shared model before adding DTO projection code:
Build<Kind>ResourceModel function and typed fact fields when the
resource has durable Kubernetes semantics.clusterId, group, version, kind,
resource, scope, namespace, and name.ResourceLink; use display-only refs only when
the source does not provide enough identity for safe navigation.resourcemodel tests for status, facts, refs, and relationship
behavior.Do not add empty fact slots just to reserve future space. Add shared facts only when a migrated consumer actually reads them.
File: backend/resources/<category>/<resource>.go
Follow this pattern:
package <category>
import (
"github.com/luxury-yacht/app/backend/resources/common"
restypes "github.com/luxury-yacht/app/backend/resources/types"
// k8s API imports for the resource
)
type <Kind>Service struct {
deps common.Dependencies
}
func New<Kind>Service(deps common.Dependencies) *<Kind>Service {
return &<Kind>Service{deps: deps}
}
// <Kind> returns the detailed view for a single resource.
func (s *<Kind>Service) <Kind>(namespace, name string) (*restypes.<Kind>Details, error) {
client := s.deps.KubernetesClient
if client == nil {
return nil, fmt.Errorf("kubernetes client not initialized")
}
item, err := client.<APIGroup>().<Resources>(namespace).Get(s.deps.Context, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get <kind>: %w", err)
}
model := resourcemodel.Build<Kind>ResourceModel(s.deps.ClusterID, item)
// Fetch related resources, project shared model facts/status into the DTO,
// and return display-ready details.
}
Key points:
common.Dependencies โ never construct clients directly(namespace, name string)(name string) onlys.deps.MetricsClient when the resource manages podss.deps.Context and s.deps.ClusterID; do not use
context.Background() or unscoped identity in resource servicesbackend/resourcemodelFile: backend/resources/types/types.go
Add a <Kind>Details struct. Include:
Status, StatusState, StatusPresentation, and optionally StatusReasonLook at neighboring types in the same file for field naming conventions โ they use plain strings for display values, not raw Kubernetes types.
If Go DTOs change, refresh or verify the Wails bindings in
frontend/wailsjs/go/models.ts. wails generate may not work in every local
run, so validate bindings with frontend typecheck.
File: backend/object_detail_provider.go
Add an entry to the objectDetailFetchers map:
"<kind-lowercase>": {
withDeps: func(deps common.Dependencies, namespace, name string) (interface{}, string, error) {
detail, err := <category>.New<Kind>Service(deps).<Kind>(namespace, name)
return detail, "", err
},
},
The key must be lowercase (e.g., "cronjob", "ingress"). The lookupObjectDetailFetcher function normalizes input to lowercase.
Also add the exact GVK to objectDetailFetcherGVKs. That map is typed-fetcher
capability metadata, not resource identity. It prevents a custom resource with a
colliding built-in kind from being served by the wrong typed fetcher.
Do not add per-kind raw-object fallbacks in
backend/refresh/snapshot/object_details.go. That snapshot builder delegates
rich detail resolution to the app-level ObjectDetailProvider and already
falls back to a generic details payload for unsupported or custom kinds.
Files:
frontend/wailsjs/go/models.tsfrontend/src/modules/object-panel/components/ObjectPanel/Details/detailsTabTypes.tsfrontend/src/modules/object-panel/components/ObjectPanel/ObjectPanel.tsxfrontend/src/modules/object-panel/components/ObjectPanel/Details/DetailsTab.tsxfrontend/src/modules/object-panel/components/ObjectPanel/Details/useOverviewData.tsAdd the new details type to DetailsTabProps and thread it through:
<kind>Details: types.<Kind>Details | null;
EMPTY_DETAILS slot and detailPayload switch case in
ObjectPanel.tsx.DetailsTab.tsx.UseOverviewDataParams and map it into the overview shape in
useOverviewData.ts.File: backend/objectcatalog/identity.go
If this is a built-in Kubernetes kind that must resolve before the first catalog
sync, add its canonical group/version/resource/scope to builtinResourceCatalog.
Do not add custom resources here; CRDs must hydrate through discovery/CRD data
and carry their real group/version.
The shared interface in backend/resources/common/resource_identity.go should
remain only a contract. Do not add another resolver table or kind-only fallback
there.
File: frontend/src/shared/constants/builtinGroupVersions.ts
If this is a built-in Kubernetes kind with a first-class frontend view, add its canonical group/version to the built-in lookup. Do not add custom resources here; custom resources must carry group/version from catalog or API data.
Directory: frontend/src/modules/object-panel/components/ObjectPanel/Details/Overview/
If the resource is similar to an existing kind (e.g., another workload), extend the existing component with conditional rendering:
const is<Kind> = normalizedKind.toLowerCase() === '<kind-lowercase>';
{is<Kind> && (
<OverviewItem label="SomeField" value={someValue} />
)}
If the resource is substantially different, create a new <Kind>Overview.tsx component following the same prop pattern as WorkloadOverview.tsx.
File: frontend/src/modules/object-panel/components/ObjectPanel/Details/Overview/registry.ts
Register the overview renderer for the new kind:
overviewRegistry.register({
kinds: ['<kind-lowercase>'],
component: <OverviewComponent>,
mapProps: (props) => ({ <kind>Details: props.<kind>Details || props }),
});
Do not rely on registry capabilities as the source of truth for object-panel
actions or tabs; current feature support is driven by RESOURCE_CAPABILITIES.
File: frontend/src/modules/object-panel/components/ObjectPanel/constants.ts
Add or update RESOURCE_CAPABILITIES for supported object-panel actions and
tabs:
delete for deletable resourcesrestart only for restartable workloadsscale only for scalable workloadsobjPanelLogs, shell, debug, trigger, suspend, or nodeLogs only
when the workflow is implemented for that kindPermission checks are evaluated from the panel object's clusterId,
group/version, kind, namespace, and name in
frontend/src/modules/object-panel/components/ObjectPanel/hooks/useObjectPanelCapabilities.ts.
If a new action kind needs different verbs, subresources, or target resources,
update that hook and the backend permission/action path together.
If the resource appears in a table/list refresh surface, update the refresh contract together:
backend/refresh/snapshot/*.gobackend/refresh/system/registrations.gobackend/refresh/snapshot/permission_checks.gobackend/refresh/snapshot/streaming_helpers.go
when a resource stream emits matching rowsbackend/refresh/resourcestream/stream_registration_*.go,
backend/refresh/resourcestream/domains.go, and resource stream tests when
live row updates are neededRefreshDomain and DomainPayloadMap in
frontend/src/core/refresh/types.tsfrontend/src/core/refresh/streaming/resourceStreamDomains.ts when live row
updates are neededfrontend/src/core/refreshRefresh domains are single-cluster only, including Resource WebSocket domains. Do not add multi-cluster descriptor flags or send multi-cluster scopes to snapshot, manual refresh, or stream paths; background refresh should fan out as separate single-cluster requests.
For larger table/list work, use .agents/skills/browse-tables/SKILL.md and
.agents/skills/refresh-subsystem/SKILL.md alongside this skill.
File: backend/resources/<category>/<resource>_test.go
Follow the established test pattern:
func Test<Kind>Service<Kind>(t *testing.T) {
// 1. Create fixtures
resource := &<apiType>{...}
// 2. Create fake client
client := cgofake.NewClientset(resource)
// 3. Create deps with testsupport helpers
deps := testsupport.NewResourceDependencies(
testsupport.WithDepsContext(context.Background()),
testsupport.WithDepsKubeClient(client),
)
// 4. Instantiate service and call method
service := <category>.New<Kind>Service(deps)
details, err := service.<Kind>("namespace", "name")
// 5. Assert
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Assert specific fields...
}
Check backend/test/testsupport/ for available fixture helpers and option functions.
File: backend/objectcatalog/service.go
If the resource should appear quickly in the catalog, add it to streamingResourcePriority. Lower numbers = higher priority. Most resources don't need this โ only add it if the resource is commonly viewed.
Before marking done:
types.go with display-ready fieldsstatusPresentationresourcemodel.ResourceLink constructors and are validatedobjectDetailFetcherGVKs metadataObjectPanel.tsx, DetailsTabProps, DetailsTab.tsx, and useOverviewData.tsRESOURCE_CAPABILITIES reflects supported object-panel actions/tabsmage qc:prerelease passesUse focused checks while iterating:
go test ./backend ./backend/resources/... ./backend/resourcemodel ./backend/refresh/snapshot
npm run typecheck --prefix frontend
npm run test --prefix frontend -- <relevant spec or module>
Then run the final gate for non-documentation work:
mage qc:prerelease
git diff --check
git status --short
Because mage qc:prerelease runs frontend lint-fix, inspect the worktree after
it completes.