@@ -0,0 +1,139 @@
|
|||||||
|
# SPEC: hub — экран «Сервисы» + экран «Прокси (Caddy)»
|
||||||
|
|
||||||
|
> Для агента-разработчика. Проект уже активен: Go API + Vite SPA, живёт под `hub`.
|
||||||
|
> Репозиторий — в Gitea на боксе, CI через act-runner (host-режим, от пользователя `homelab`).
|
||||||
|
> Факты окружения ниже проверены 2026-08-30 — менять не нужно, если не сказано иначе.
|
||||||
|
|
||||||
|
## 1. Что за проект
|
||||||
|
|
||||||
|
Личный хаб-дашборд для телефона (PWA). Три части:
|
||||||
|
1. **Клиент** — Vite SPA (сборка → `web/dist/`, раздаёт Caddy с SPA-fallback на `/index.html`).
|
||||||
|
2. **API** — Go, один бинарник `hub-api`, слушает **`127.0.0.1:8484`** (unit задаёт `PORT=8484`).
|
||||||
|
3. **Раздача/прокси** — Caddy, домен `hub.alexey-homelab.duckdns.org`:
|
||||||
|
- `/api/*` и `/homelab.v1.HomelabService/*` → `127.0.0.1:8484`
|
||||||
|
- всё остальное → `~/apps/hub/dist/` (SPA).
|
||||||
|
|
||||||
|
Деплой: push в `main` → Gitea Actions (`deploy.workflow.yml`) → `make web server` →
|
||||||
|
`~/apps/hub/dist/` + `~/apps/hub/hub-api` (атомарный mv!) → `systemctl --user restart hub-api`.
|
||||||
|
Юнит сервиса: `~/srv/hub-api.service` (user, symlink в `~/.config/systemd/user/`).
|
||||||
|
|
||||||
|
Уже есть в конфиге (`~/apps/hub/config.yaml`): `links` (карточки-ссылки), `monitor`
|
||||||
|
(cpu/disk/mem через скрипты), `exec` (greet). Это работает — не ломай.
|
||||||
|
|
||||||
|
## 2. Задача: экран «Сервисы»
|
||||||
|
|
||||||
|
Единый список всех сервисов бокса. Источник — **два разных мира**, их нужно объединить
|
||||||
|
в один список с полем `kind` и иконкой для отличия:
|
||||||
|
|
||||||
|
- `kind: native` — бинарники под user-systemd (rathole, OliveTin, hermes-*, hub-api...)
|
||||||
|
- `kind: docker` — контейнеры Docker, сгруппированные по compose-проектам
|
||||||
|
|
||||||
|
### 2.1. Native-сервисы
|
||||||
|
|
||||||
|
- Список: парсить **`~/srv/*.service`** (каждый файл = сервис). Часть файлов — symlink'и
|
||||||
|
на `~/.config/systemd/user/*.service` — это нормально, читай через symlink.
|
||||||
|
- Статус (машиночитаемо, всегда exit 0, не путай с `status`):
|
||||||
|
```
|
||||||
|
systemctl --user show <unit> -p ActiveState,SubState,LoadState,UnitFileState --value
|
||||||
|
```
|
||||||
|
- Кнопки: `systemctl --user start|stop|restart <unit>` — **без sudo**, от homelab.
|
||||||
|
- **Disabled-состояние**: если юнит не найден в systemd (`LoadState=not-found`) — это НЕ ошибка,
|
||||||
|
пользователь намеренно сделал `systemctl disable` (сейчас так: ntfy, olivetin, pairdrop,
|
||||||
|
uptime-kuma). Показывай бейдж «выключен» (disabled), серым, без алертов.
|
||||||
|
|
||||||
|
### 2.2. Docker-сервисы
|
||||||
|
|
||||||
|
- Список: **`docker ps -a`** (не юниты!). Контейнеров 37, юнитов на них нет/мало — источник
|
||||||
|
истины только Docker.
|
||||||
|
- Для каждого: `docker inspect -f '{{.State.Running}}|{{.State.Status}}' <name>` +
|
||||||
|
label проекта `com.docker.compose.project` (через `docker inspect -f '{{index .Config.Labels "com.docker.compose.project"}}'`).
|
||||||
|
- **Группировка**: контейнеры одного проекта — одна карточка проекта (itsaplan, pullmd,
|
||||||
|
affine, kurir, minepanel, searxng...), одиночные (gitea, homepage, samba...) — свои карточки.
|
||||||
|
- **Агрегированный статус проекта**: все рабочие running → «работает»; часть → «частично
|
||||||
|
(N/M)»; ни одного → «остановлен». Служебные контейнеры (`*-redis`, `*-postgres`,
|
||||||
|
`*-minio-init-1`, `affine_migration` — они `policy=no`, `running=false`) сворачивать,
|
||||||
|
не показывать как отдельные карточки. Иконка-отличие docker: 🐳 (или своя, по вкусу UI).
|
||||||
|
- Кнопки на проект: `docker start|stop <все контейнеры проекта>` (без sudo — homelab в группе docker).
|
||||||
|
- **Семантика stop**: `docker stop` + policy `unless-stopped` = контейнер не поднимется при
|
||||||
|
ребуте (docker считает его намеренно остановленным). Это ожидаемое поведение кнопки
|
||||||
|
«остановить до ребута» — задокументируй в UI тултипом, не «чини».
|
||||||
|
|
||||||
|
### 2.3. Редактирование unit-файла в браузере
|
||||||
|
|
||||||
|
- GET/PUT `~/srv/<name>.service` (write — через symlink в реальный юнит, ок).
|
||||||
|
- **После сохранения — обязательно `systemctl --user daemon-reload`**, иначе правки не применятся.
|
||||||
|
- ⚠️ Валидация имени сервиса: только `^[a-z0-9_.-]+$` — защита от path traversal.
|
||||||
|
- Предупреждение в UI: правка unit = смена ExecStart = выполнение произвольных команд;
|
||||||
|
restart для docker-обёрток (`oneshot` + `RemainAfterExit`) = stop+start, а не restart.
|
||||||
|
|
||||||
|
## 3. Задача: экран «Прокси» (Caddy)
|
||||||
|
|
||||||
|
Минимум: отдельная страница со списком доменов и возможностью **руками редактировать конфиг**.
|
||||||
|
Идеал: на странице сервиса поле «домен» — но это позже, начни с отдельной страницы.
|
||||||
|
|
||||||
|
**Как устроен Caddy на боксе (не менять схему!):**
|
||||||
|
- Рабочая копия конфига: **`~/caddy-sync/Caddyfile`** — её и правим (она в homelab-зоне, writable).
|
||||||
|
- Применение (NOPASSWD sudo уже настроен):
|
||||||
|
```
|
||||||
|
sudo -n /usr/local/sbin/caddy-sync # validate + копия в /etc/caddy/Caddyfile + backup
|
||||||
|
sudo -n /usr/local/sbin/caddy-reload # validate + reload + автоген ~/www/util/domains
|
||||||
|
sudo -n /usr/local/sbin/hosts-sync # /etc/hosts; ОБЯЗАТЕЛЕН при новом duckdns-домене
|
||||||
|
```
|
||||||
|
- Read: `sudo -n /usr/local/sbin/caddy-cat` (весь Caddyfile) или admin API
|
||||||
|
`http://127.0.0.1:2019/config/` (машиночитаемо, список хостов).
|
||||||
|
- **Запрещено трогать**: секцию ab-hl (внешний выход на VPS), rathole-туннели, knot-resolver.
|
||||||
|
Внешний VPS-выход — отдельная будущая задача.
|
||||||
|
|
||||||
|
**UI страницы «Прокси»:**
|
||||||
|
- Таблица сайтов: домен → куда проксируется (или file_server) → статус (из списка выше).
|
||||||
|
- Кнопка «Редактировать» → редактор (textarea/monaco-лайт) с **рабочей копией** `~/caddy-sync/Caddyfile`.
|
||||||
|
- Кнопка «Применить» → `caddy-sync` → `caddy-reload` → `hosts-sync`, показать вывод каждой
|
||||||
|
команды (это твой «результат валидации» — caddy-sync сам вернёт REJECT при ошибке).
|
||||||
|
- После успеха — обновить таблицу.
|
||||||
|
|
||||||
|
## 4. Контракт API (добавить в Go-бэкенд)
|
||||||
|
|
||||||
|
Предлагаемые эндпоинты (паттерн REST, JSON; auth — как уже сделано в проекте):
|
||||||
|
|
||||||
|
| Метод | Путь | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/services` | список: `[{name, kind, group, status, substate, containers?}]` |
|
||||||
|
| POST | `/api/services/{name}/start` | native: systemctl start; docker: docker start (проект) |
|
||||||
|
| POST | `/api/services/{name}/stop` | аналогично |
|
||||||
|
| POST | `/api/services/{name}/restart` | native: restart; docker: stop+start |
|
||||||
|
| GET | `/api/services/{name}/file` | содержимое `~/srv/<name>.service` |
|
||||||
|
| PUT | `/api/services/{name}/file` | сохранить + `daemon-reload` |
|
||||||
|
| GET | `/api/caddy` | список сайтов + рабочий конфиг |
|
||||||
|
| PUT | `/api/caddy` | сохранить рабочий конфиг |
|
||||||
|
| POST | `/api/caddy/apply` | sync → reload → hosts-sync, вернуть вывод |
|
||||||
|
|
||||||
|
Имя `{name}` всегда валидировать: `^[a-z0-9_.-]+$`. Команды — фиксированные, без
|
||||||
|
интерполяции пользовательского ввода в shell (используй `exec.Command` с аргументами,
|
||||||
|
не shell-строку).
|
||||||
|
|
||||||
|
## 5. Критерии приёмки
|
||||||
|
|
||||||
|
1. Экран «Сервисы» показывает единый список: native из `~/srv` + docker-проекты из `docker ps -a`,
|
||||||
|
с иконками 🐳/⚙️ и агрегированными статусами.
|
||||||
|
2. Кнопки start/stop/restart работают для обоих видов без sudo; disabled-юниты показываются
|
||||||
|
серым как «выключен», не как ошибка.
|
||||||
|
3. Редактирование unit-файла сохраняет файл и делает daemon-reload; кривое имя сервиса не
|
||||||
|
проходит валидацию.
|
||||||
|
4. Экран «Прокси»: таблица доменов, редактор рабочей копии `~/caddy-sync/Caddyfile`,
|
||||||
|
«Применить» выполняет sync/reload/hosts-sync и показывает вывод; секция ab-hl не меняется.
|
||||||
|
5. Всё работает через существующий auth, без новых открытых портов; PWA-обновление
|
||||||
|
подхватывается на iOS (skipWaiting + clientsClaim, как в дизайн-доке).
|
||||||
|
6. Сборка/деплой проходят через существующий CI (`deploy.workflow.yml`), юнит `hub-api` переживает рестарт.
|
||||||
|
|
||||||
|
## 6. Справочные факты бокса (не перепроверять)
|
||||||
|
|
||||||
|
- Все сервисы живут от пользователя `homelab`; user-systemd: `systemctl --user` (XDG_RUNTIME_DIR
|
||||||
|
нужен только из root-сессий).
|
||||||
|
- `~/srv/README.md` — документация списка сервисов; обновить её, если меняется формат.
|
||||||
|
- Порт API: **8484** (не менять — Caddy смотрит туда).
|
||||||
|
- Сборка Go: `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build` (тулчейн на боксе есть,
|
||||||
|
`~/apps/go/bin` в PATH CI).
|
||||||
|
- Фронт: Vite (node 22 / pnpm 11 в `~/.local/bin`).
|
||||||
|
- Сейчас в `~/srv` 26 юнитов: 18 docker-обёрток, 9 native (act-runner, hermes-dashboard,
|
||||||
|
hermes-gateway, hub-api, itsaplan-runner, olivetin, rathole, rathole-status, ttyd-rathole),
|
||||||
|
4 disabled (ntfy, olivetin, pairdrop, uptime-kuma — LoadState=not-found, это норма).
|
||||||
+395
-46
@@ -21,6 +21,102 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// One card in the Services view: a systemd user unit or a docker compose project.
|
||||||
|
type ServiceKind int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceKind_SYSTEMD ServiceKind = 0
|
||||||
|
ServiceKind_DOCKER ServiceKind = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ServiceKind.
|
||||||
|
var (
|
||||||
|
ServiceKind_name = map[int32]string{
|
||||||
|
0: "SYSTEMD",
|
||||||
|
1: "DOCKER",
|
||||||
|
}
|
||||||
|
ServiceKind_value = map[string]int32{
|
||||||
|
"SYSTEMD": 0,
|
||||||
|
"DOCKER": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ServiceKind) Enum() *ServiceKind {
|
||||||
|
p := new(ServiceKind)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ServiceKind) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ServiceKind) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_homelab_v1_homelab_proto_enumTypes[0].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ServiceKind) Type() protoreflect.EnumType {
|
||||||
|
return &file_homelab_v1_homelab_proto_enumTypes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ServiceKind) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServiceKind.Descriptor instead.
|
||||||
|
func (ServiceKind) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceOp int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceOp_START ServiceOp = 0
|
||||||
|
ServiceOp_STOP ServiceOp = 1
|
||||||
|
ServiceOp_RESTART ServiceOp = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ServiceOp.
|
||||||
|
var (
|
||||||
|
ServiceOp_name = map[int32]string{
|
||||||
|
0: "START",
|
||||||
|
1: "STOP",
|
||||||
|
2: "RESTART",
|
||||||
|
}
|
||||||
|
ServiceOp_value = map[string]int32{
|
||||||
|
"START": 0,
|
||||||
|
"STOP": 1,
|
||||||
|
"RESTART": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ServiceOp) Enum() *ServiceOp {
|
||||||
|
p := new(ServiceOp)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ServiceOp) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ServiceOp) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_homelab_v1_homelab_proto_enumTypes[1].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ServiceOp) Type() protoreflect.EnumType {
|
||||||
|
return &file_homelab_v1_homelab_proto_enumTypes[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ServiceOp) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServiceOp.Descriptor instead.
|
||||||
|
func (ServiceOp) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
@@ -190,6 +286,7 @@ type ListContentResponse struct {
|
|||||||
Links []*Link `protobuf:"bytes,1,rep,name=links,proto3" json:"links,omitempty"`
|
Links []*Link `protobuf:"bytes,1,rep,name=links,proto3" json:"links,omitempty"`
|
||||||
Monitors []*Item `protobuf:"bytes,2,rep,name=monitors,proto3" json:"monitors,omitempty"`
|
Monitors []*Item `protobuf:"bytes,2,rep,name=monitors,proto3" json:"monitors,omitempty"`
|
||||||
Exec []*Item `protobuf:"bytes,3,rep,name=exec,proto3" json:"exec,omitempty"`
|
Exec []*Item `protobuf:"bytes,3,rep,name=exec,proto3" json:"exec,omitempty"`
|
||||||
|
Services []*Service `protobuf:"bytes,4,rep,name=services,proto3" json:"services,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -245,6 +342,13 @@ func (x *ListContentResponse) GetExec() []*Item {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ListContentResponse) GetServices() []*Service {
|
||||||
|
if x != nil {
|
||||||
|
return x.Services
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type DocMeta struct {
|
type DocMeta struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
@@ -561,6 +665,210 @@ func (x *SaveDocResponse) GetHash() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Kind ServiceKind `protobuf:"varint,1,opt,name=kind,proto3,enum=homelab.v1.ServiceKind" json:"kind,omitempty"`
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // unit name without .service / compose project name (or container name if unlabeled)
|
||||||
|
State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` // systemd: ActiveState. docker: active | partial | inactive
|
||||||
|
Substate string `protobuf:"bytes,4,opt,name=substate,proto3" json:"substate,omitempty"` // systemd: SubState. docker: "up/total"
|
||||||
|
LoadState string `protobuf:"bytes,5,opt,name=load_state,json=loadState,proto3" json:"load_state,omitempty"` // systemd only: loaded | not-found (= disabled by the user, not an error)
|
||||||
|
UnitFileState string `protobuf:"bytes,6,opt,name=unit_file_state,json=unitFileState,proto3" json:"unit_file_state,omitempty"` // systemd only: enabled | disabled | static
|
||||||
|
Containers []string `protobuf:"bytes,7,rep,name=containers,proto3" json:"containers,omitempty"` // docker only: containers the card controls
|
||||||
|
Error string `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"` // probe failure, empty = ok
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) Reset() {
|
||||||
|
*x = Service{}
|
||||||
|
mi := &file_homelab_v1_homelab_proto_msgTypes[11]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Service) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_homelab_v1_homelab_proto_msgTypes[11]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Service) Descriptor() ([]byte, []int) {
|
||||||
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{11}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetKind() ServiceKind {
|
||||||
|
if x != nil {
|
||||||
|
return x.Kind
|
||||||
|
}
|
||||||
|
return ServiceKind_SYSTEMD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetState() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.State
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetSubstate() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Substate
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetLoadState() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.LoadState
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetUnitFileState() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UnitFileState
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetContainers() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Containers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetError() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Error
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceActionRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Kind ServiceKind `protobuf:"varint,2,opt,name=kind,proto3,enum=homelab.v1.ServiceKind" json:"kind,omitempty"`
|
||||||
|
Op ServiceOp `protobuf:"varint,3,opt,name=op,proto3,enum=homelab.v1.ServiceOp" json:"op,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionRequest) Reset() {
|
||||||
|
*x = ServiceActionRequest{}
|
||||||
|
mi := &file_homelab_v1_homelab_proto_msgTypes[12]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServiceActionRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ServiceActionRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_homelab_v1_homelab_proto_msgTypes[12]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServiceActionRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ServiceActionRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{12}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionRequest) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionRequest) GetKind() ServiceKind {
|
||||||
|
if x != nil {
|
||||||
|
return x.Kind
|
||||||
|
}
|
||||||
|
return ServiceKind_SYSTEMD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionRequest) GetOp() ServiceOp {
|
||||||
|
if x != nil {
|
||||||
|
return x.Op
|
||||||
|
}
|
||||||
|
return ServiceOp_START
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceActionResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionResponse) Reset() {
|
||||||
|
*x = ServiceActionResponse{}
|
||||||
|
mi := &file_homelab_v1_homelab_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServiceActionResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ServiceActionResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_homelab_v1_homelab_proto_msgTypes[13]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServiceActionResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ServiceActionResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{13}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServiceActionResponse) GetOutput() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Output
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type RunScriptRequest struct {
|
type RunScriptRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
@@ -571,7 +879,7 @@ type RunScriptRequest struct {
|
|||||||
|
|
||||||
func (x *RunScriptRequest) Reset() {
|
func (x *RunScriptRequest) Reset() {
|
||||||
*x = RunScriptRequest{}
|
*x = RunScriptRequest{}
|
||||||
mi := &file_homelab_v1_homelab_proto_msgTypes[11]
|
mi := &file_homelab_v1_homelab_proto_msgTypes[14]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -583,7 +891,7 @@ func (x *RunScriptRequest) String() string {
|
|||||||
func (*RunScriptRequest) ProtoMessage() {}
|
func (*RunScriptRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RunScriptRequest) ProtoReflect() protoreflect.Message {
|
func (x *RunScriptRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_homelab_v1_homelab_proto_msgTypes[11]
|
mi := &file_homelab_v1_homelab_proto_msgTypes[14]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -596,7 +904,7 @@ func (x *RunScriptRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RunScriptRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RunScriptRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*RunScriptRequest) Descriptor() ([]byte, []int) {
|
func (*RunScriptRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{11}
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RunScriptRequest) GetName() string {
|
func (x *RunScriptRequest) GetName() string {
|
||||||
@@ -622,7 +930,7 @@ type RunScriptResponse struct {
|
|||||||
|
|
||||||
func (x *RunScriptResponse) Reset() {
|
func (x *RunScriptResponse) Reset() {
|
||||||
*x = RunScriptResponse{}
|
*x = RunScriptResponse{}
|
||||||
mi := &file_homelab_v1_homelab_proto_msgTypes[12]
|
mi := &file_homelab_v1_homelab_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -634,7 +942,7 @@ func (x *RunScriptResponse) String() string {
|
|||||||
func (*RunScriptResponse) ProtoMessage() {}
|
func (*RunScriptResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RunScriptResponse) ProtoReflect() protoreflect.Message {
|
func (x *RunScriptResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_homelab_v1_homelab_proto_msgTypes[12]
|
mi := &file_homelab_v1_homelab_proto_msgTypes[15]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -647,7 +955,7 @@ func (x *RunScriptResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RunScriptResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RunScriptResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*RunScriptResponse) Descriptor() ([]byte, []int) {
|
func (*RunScriptResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{12}
|
return file_homelab_v1_homelab_proto_rawDescGZIP(), []int{15}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RunScriptResponse) GetOutput() string {
|
func (x *RunScriptResponse) GetOutput() string {
|
||||||
@@ -672,11 +980,12 @@ const file_homelab_v1_homelab_proto_rawDesc = "" +
|
|||||||
"\x04Link\x12\x12\n" +
|
"\x04Link\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||||
"\x03url\x18\x02 \x01(\tR\x03url\"\x14\n" +
|
"\x03url\x18\x02 \x01(\tR\x03url\"\x14\n" +
|
||||||
"\x12ListContentRequest\"\x91\x01\n" +
|
"\x12ListContentRequest\"\xc2\x01\n" +
|
||||||
"\x13ListContentResponse\x12&\n" +
|
"\x13ListContentResponse\x12&\n" +
|
||||||
"\x05links\x18\x01 \x03(\v2\x10.homelab.v1.LinkR\x05links\x12,\n" +
|
"\x05links\x18\x01 \x03(\v2\x10.homelab.v1.LinkR\x05links\x12,\n" +
|
||||||
"\bmonitors\x18\x02 \x03(\v2\x10.homelab.v1.ItemR\bmonitors\x12$\n" +
|
"\bmonitors\x18\x02 \x03(\v2\x10.homelab.v1.ItemR\bmonitors\x12$\n" +
|
||||||
"\x04exec\x18\x03 \x03(\v2\x10.homelab.v1.ItemR\x04exec\"1\n" +
|
"\x04exec\x18\x03 \x03(\v2\x10.homelab.v1.ItemR\x04exec\x12/\n" +
|
||||||
|
"\bservices\x18\x04 \x03(\v2\x13.homelab.v1.ServiceR\bservices\"1\n" +
|
||||||
"\aDocMeta\x12\x12\n" +
|
"\aDocMeta\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
||||||
"\x04hash\x18\x02 \x01(\tR\x04hash\"\x11\n" +
|
"\x04hash\x18\x02 \x01(\tR\x04hash\"\x11\n" +
|
||||||
@@ -691,17 +1000,44 @@ const file_homelab_v1_homelab_proto_rawDesc = "" +
|
|||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
|
||||||
"\acontent\x18\x02 \x01(\tR\acontent\"%\n" +
|
"\acontent\x18\x02 \x01(\tR\acontent\"%\n" +
|
||||||
"\x0fSaveDocResponse\x12\x12\n" +
|
"\x0fSaveDocResponse\x12\x12\n" +
|
||||||
"\x04hash\x18\x01 \x01(\tR\x04hash\":\n" +
|
"\x04hash\x18\x01 \x01(\tR\x04hash\"\xf9\x01\n" +
|
||||||
|
"\aService\x12+\n" +
|
||||||
|
"\x04kind\x18\x01 \x01(\x0e2\x17.homelab.v1.ServiceKindR\x04kind\x12\x12\n" +
|
||||||
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
|
||||||
|
"\x05state\x18\x03 \x01(\tR\x05state\x12\x1a\n" +
|
||||||
|
"\bsubstate\x18\x04 \x01(\tR\bsubstate\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"load_state\x18\x05 \x01(\tR\tloadState\x12&\n" +
|
||||||
|
"\x0funit_file_state\x18\x06 \x01(\tR\runitFileState\x12\x1e\n" +
|
||||||
|
"\n" +
|
||||||
|
"containers\x18\a \x03(\tR\n" +
|
||||||
|
"containers\x12\x14\n" +
|
||||||
|
"\x05error\x18\b \x01(\tR\x05error\"~\n" +
|
||||||
|
"\x14ServiceActionRequest\x12\x12\n" +
|
||||||
|
"\x04name\x18\x01 \x01(\tR\x04name\x12+\n" +
|
||||||
|
"\x04kind\x18\x02 \x01(\x0e2\x17.homelab.v1.ServiceKindR\x04kind\x12%\n" +
|
||||||
|
"\x02op\x18\x03 \x01(\x0e2\x15.homelab.v1.ServiceOpR\x02op\"/\n" +
|
||||||
|
"\x15ServiceActionResponse\x12\x16\n" +
|
||||||
|
"\x06output\x18\x01 \x01(\tR\x06output\":\n" +
|
||||||
"\x10RunScriptRequest\x12\x12\n" +
|
"\x10RunScriptRequest\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
||||||
"\x04args\x18\x02 \x03(\tR\x04args\"+\n" +
|
"\x04args\x18\x02 \x03(\tR\x04args\"+\n" +
|
||||||
"\x11RunScriptResponse\x12\x16\n" +
|
"\x11RunScriptResponse\x12\x16\n" +
|
||||||
"\x06output\x18\x01 \x01(\tR\x06output2\xf6\x02\n" +
|
"\x06output\x18\x01 \x01(\tR\x06output*&\n" +
|
||||||
|
"\vServiceKind\x12\v\n" +
|
||||||
|
"\aSYSTEMD\x10\x00\x12\n" +
|
||||||
|
"\n" +
|
||||||
|
"\x06DOCKER\x10\x01*-\n" +
|
||||||
|
"\tServiceOp\x12\t\n" +
|
||||||
|
"\x05START\x10\x00\x12\b\n" +
|
||||||
|
"\x04STOP\x10\x01\x12\v\n" +
|
||||||
|
"\aRESTART\x10\x022\xcc\x03\n" +
|
||||||
"\x0eHomelabService\x12N\n" +
|
"\x0eHomelabService\x12N\n" +
|
||||||
"\vListContent\x12\x1e.homelab.v1.ListContentRequest\x1a\x1f.homelab.v1.ListContentResponse\x12E\n" +
|
"\vListContent\x12\x1e.homelab.v1.ListContentRequest\x1a\x1f.homelab.v1.ListContentResponse\x12E\n" +
|
||||||
"\bListDocs\x12\x1b.homelab.v1.ListDocsRequest\x1a\x1c.homelab.v1.ListDocsResponse\x12?\n" +
|
"\bListDocs\x12\x1b.homelab.v1.ListDocsRequest\x1a\x1c.homelab.v1.ListDocsResponse\x12?\n" +
|
||||||
"\x06GetDoc\x12\x19.homelab.v1.GetDocRequest\x1a\x1a.homelab.v1.GetDocResponse\x12B\n" +
|
"\x06GetDoc\x12\x19.homelab.v1.GetDocRequest\x1a\x1a.homelab.v1.GetDocResponse\x12B\n" +
|
||||||
"\aSaveDoc\x12\x1a.homelab.v1.SaveDocRequest\x1a\x1b.homelab.v1.SaveDocResponse\x12H\n" +
|
"\aSaveDoc\x12\x1a.homelab.v1.SaveDocRequest\x1a\x1b.homelab.v1.SaveDocResponse\x12T\n" +
|
||||||
|
"\rServiceAction\x12 .homelab.v1.ServiceActionRequest\x1a!.homelab.v1.ServiceActionResponse\x12H\n" +
|
||||||
"\tRunScript\x12\x1c.homelab.v1.RunScriptRequest\x1a\x1d.homelab.v1.RunScriptResponseB*Z(pwa-homelab-mon/gen/homelab/v1;homelabv1b\x06proto3"
|
"\tRunScript\x12\x1c.homelab.v1.RunScriptRequest\x1a\x1d.homelab.v1.RunScriptResponseB*Z(pwa-homelab-mon/gen/homelab/v1;homelabv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -716,42 +1052,54 @@ func file_homelab_v1_homelab_proto_rawDescGZIP() []byte {
|
|||||||
return file_homelab_v1_homelab_proto_rawDescData
|
return file_homelab_v1_homelab_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_homelab_v1_homelab_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
var file_homelab_v1_homelab_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
|
var file_homelab_v1_homelab_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
||||||
var file_homelab_v1_homelab_proto_goTypes = []any{
|
var file_homelab_v1_homelab_proto_goTypes = []any{
|
||||||
(*Item)(nil), // 0: homelab.v1.Item
|
(ServiceKind)(0), // 0: homelab.v1.ServiceKind
|
||||||
(*Link)(nil), // 1: homelab.v1.Link
|
(ServiceOp)(0), // 1: homelab.v1.ServiceOp
|
||||||
(*ListContentRequest)(nil), // 2: homelab.v1.ListContentRequest
|
(*Item)(nil), // 2: homelab.v1.Item
|
||||||
(*ListContentResponse)(nil), // 3: homelab.v1.ListContentResponse
|
(*Link)(nil), // 3: homelab.v1.Link
|
||||||
(*DocMeta)(nil), // 4: homelab.v1.DocMeta
|
(*ListContentRequest)(nil), // 4: homelab.v1.ListContentRequest
|
||||||
(*ListDocsRequest)(nil), // 5: homelab.v1.ListDocsRequest
|
(*ListContentResponse)(nil), // 5: homelab.v1.ListContentResponse
|
||||||
(*ListDocsResponse)(nil), // 6: homelab.v1.ListDocsResponse
|
(*DocMeta)(nil), // 6: homelab.v1.DocMeta
|
||||||
(*GetDocRequest)(nil), // 7: homelab.v1.GetDocRequest
|
(*ListDocsRequest)(nil), // 7: homelab.v1.ListDocsRequest
|
||||||
(*GetDocResponse)(nil), // 8: homelab.v1.GetDocResponse
|
(*ListDocsResponse)(nil), // 8: homelab.v1.ListDocsResponse
|
||||||
(*SaveDocRequest)(nil), // 9: homelab.v1.SaveDocRequest
|
(*GetDocRequest)(nil), // 9: homelab.v1.GetDocRequest
|
||||||
(*SaveDocResponse)(nil), // 10: homelab.v1.SaveDocResponse
|
(*GetDocResponse)(nil), // 10: homelab.v1.GetDocResponse
|
||||||
(*RunScriptRequest)(nil), // 11: homelab.v1.RunScriptRequest
|
(*SaveDocRequest)(nil), // 11: homelab.v1.SaveDocRequest
|
||||||
(*RunScriptResponse)(nil), // 12: homelab.v1.RunScriptResponse
|
(*SaveDocResponse)(nil), // 12: homelab.v1.SaveDocResponse
|
||||||
|
(*Service)(nil), // 13: homelab.v1.Service
|
||||||
|
(*ServiceActionRequest)(nil), // 14: homelab.v1.ServiceActionRequest
|
||||||
|
(*ServiceActionResponse)(nil), // 15: homelab.v1.ServiceActionResponse
|
||||||
|
(*RunScriptRequest)(nil), // 16: homelab.v1.RunScriptRequest
|
||||||
|
(*RunScriptResponse)(nil), // 17: homelab.v1.RunScriptResponse
|
||||||
}
|
}
|
||||||
var file_homelab_v1_homelab_proto_depIdxs = []int32{
|
var file_homelab_v1_homelab_proto_depIdxs = []int32{
|
||||||
1, // 0: homelab.v1.ListContentResponse.links:type_name -> homelab.v1.Link
|
3, // 0: homelab.v1.ListContentResponse.links:type_name -> homelab.v1.Link
|
||||||
0, // 1: homelab.v1.ListContentResponse.monitors:type_name -> homelab.v1.Item
|
2, // 1: homelab.v1.ListContentResponse.monitors:type_name -> homelab.v1.Item
|
||||||
0, // 2: homelab.v1.ListContentResponse.exec:type_name -> homelab.v1.Item
|
2, // 2: homelab.v1.ListContentResponse.exec:type_name -> homelab.v1.Item
|
||||||
4, // 3: homelab.v1.ListDocsResponse.docs:type_name -> homelab.v1.DocMeta
|
13, // 3: homelab.v1.ListContentResponse.services:type_name -> homelab.v1.Service
|
||||||
2, // 4: homelab.v1.HomelabService.ListContent:input_type -> homelab.v1.ListContentRequest
|
6, // 4: homelab.v1.ListDocsResponse.docs:type_name -> homelab.v1.DocMeta
|
||||||
5, // 5: homelab.v1.HomelabService.ListDocs:input_type -> homelab.v1.ListDocsRequest
|
0, // 5: homelab.v1.Service.kind:type_name -> homelab.v1.ServiceKind
|
||||||
7, // 6: homelab.v1.HomelabService.GetDoc:input_type -> homelab.v1.GetDocRequest
|
0, // 6: homelab.v1.ServiceActionRequest.kind:type_name -> homelab.v1.ServiceKind
|
||||||
9, // 7: homelab.v1.HomelabService.SaveDoc:input_type -> homelab.v1.SaveDocRequest
|
1, // 7: homelab.v1.ServiceActionRequest.op:type_name -> homelab.v1.ServiceOp
|
||||||
11, // 8: homelab.v1.HomelabService.RunScript:input_type -> homelab.v1.RunScriptRequest
|
4, // 8: homelab.v1.HomelabService.ListContent:input_type -> homelab.v1.ListContentRequest
|
||||||
3, // 9: homelab.v1.HomelabService.ListContent:output_type -> homelab.v1.ListContentResponse
|
7, // 9: homelab.v1.HomelabService.ListDocs:input_type -> homelab.v1.ListDocsRequest
|
||||||
6, // 10: homelab.v1.HomelabService.ListDocs:output_type -> homelab.v1.ListDocsResponse
|
9, // 10: homelab.v1.HomelabService.GetDoc:input_type -> homelab.v1.GetDocRequest
|
||||||
8, // 11: homelab.v1.HomelabService.GetDoc:output_type -> homelab.v1.GetDocResponse
|
11, // 11: homelab.v1.HomelabService.SaveDoc:input_type -> homelab.v1.SaveDocRequest
|
||||||
10, // 12: homelab.v1.HomelabService.SaveDoc:output_type -> homelab.v1.SaveDocResponse
|
14, // 12: homelab.v1.HomelabService.ServiceAction:input_type -> homelab.v1.ServiceActionRequest
|
||||||
12, // 13: homelab.v1.HomelabService.RunScript:output_type -> homelab.v1.RunScriptResponse
|
16, // 13: homelab.v1.HomelabService.RunScript:input_type -> homelab.v1.RunScriptRequest
|
||||||
9, // [9:14] is the sub-list for method output_type
|
5, // 14: homelab.v1.HomelabService.ListContent:output_type -> homelab.v1.ListContentResponse
|
||||||
4, // [4:9] is the sub-list for method input_type
|
8, // 15: homelab.v1.HomelabService.ListDocs:output_type -> homelab.v1.ListDocsResponse
|
||||||
4, // [4:4] is the sub-list for extension type_name
|
10, // 16: homelab.v1.HomelabService.GetDoc:output_type -> homelab.v1.GetDocResponse
|
||||||
4, // [4:4] is the sub-list for extension extendee
|
12, // 17: homelab.v1.HomelabService.SaveDoc:output_type -> homelab.v1.SaveDocResponse
|
||||||
0, // [0:4] is the sub-list for field type_name
|
15, // 18: homelab.v1.HomelabService.ServiceAction:output_type -> homelab.v1.ServiceActionResponse
|
||||||
|
17, // 19: homelab.v1.HomelabService.RunScript:output_type -> homelab.v1.RunScriptResponse
|
||||||
|
14, // [14:20] is the sub-list for method output_type
|
||||||
|
8, // [8:14] is the sub-list for method input_type
|
||||||
|
8, // [8:8] is the sub-list for extension type_name
|
||||||
|
8, // [8:8] is the sub-list for extension extendee
|
||||||
|
0, // [0:8] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_homelab_v1_homelab_proto_init() }
|
func init() { file_homelab_v1_homelab_proto_init() }
|
||||||
@@ -764,13 +1112,14 @@ func file_homelab_v1_homelab_proto_init() {
|
|||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_homelab_v1_homelab_proto_rawDesc), len(file_homelab_v1_homelab_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_homelab_v1_homelab_proto_rawDesc), len(file_homelab_v1_homelab_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 2,
|
||||||
NumMessages: 13,
|
NumMessages: 16,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
GoTypes: file_homelab_v1_homelab_proto_goTypes,
|
GoTypes: file_homelab_v1_homelab_proto_goTypes,
|
||||||
DependencyIndexes: file_homelab_v1_homelab_proto_depIdxs,
|
DependencyIndexes: file_homelab_v1_homelab_proto_depIdxs,
|
||||||
|
EnumInfos: file_homelab_v1_homelab_proto_enumTypes,
|
||||||
MessageInfos: file_homelab_v1_homelab_proto_msgTypes,
|
MessageInfos: file_homelab_v1_homelab_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_homelab_v1_homelab_proto = out.File
|
File_homelab_v1_homelab_proto = out.File
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ const (
|
|||||||
HomelabServiceGetDocProcedure = "/homelab.v1.HomelabService/GetDoc"
|
HomelabServiceGetDocProcedure = "/homelab.v1.HomelabService/GetDoc"
|
||||||
// HomelabServiceSaveDocProcedure is the fully-qualified name of the HomelabService's SaveDoc RPC.
|
// HomelabServiceSaveDocProcedure is the fully-qualified name of the HomelabService's SaveDoc RPC.
|
||||||
HomelabServiceSaveDocProcedure = "/homelab.v1.HomelabService/SaveDoc"
|
HomelabServiceSaveDocProcedure = "/homelab.v1.HomelabService/SaveDoc"
|
||||||
|
// HomelabServiceServiceActionProcedure is the fully-qualified name of the HomelabService's
|
||||||
|
// ServiceAction RPC.
|
||||||
|
HomelabServiceServiceActionProcedure = "/homelab.v1.HomelabService/ServiceAction"
|
||||||
// HomelabServiceRunScriptProcedure is the fully-qualified name of the HomelabService's RunScript
|
// HomelabServiceRunScriptProcedure is the fully-qualified name of the HomelabService's RunScript
|
||||||
// RPC.
|
// RPC.
|
||||||
HomelabServiceRunScriptProcedure = "/homelab.v1.HomelabService/RunScript"
|
HomelabServiceRunScriptProcedure = "/homelab.v1.HomelabService/RunScript"
|
||||||
@@ -53,6 +56,7 @@ type HomelabServiceClient interface {
|
|||||||
ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error)
|
ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error)
|
||||||
GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error)
|
GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error)
|
||||||
SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error)
|
SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error)
|
||||||
|
ServiceAction(context.Context, *connect.Request[v1.ServiceActionRequest]) (*connect.Response[v1.ServiceActionResponse], error)
|
||||||
RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error)
|
RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +95,12 @@ func NewHomelabServiceClient(httpClient connect.HTTPClient, baseURL string, opts
|
|||||||
connect.WithSchema(homelabServiceMethods.ByName("SaveDoc")),
|
connect.WithSchema(homelabServiceMethods.ByName("SaveDoc")),
|
||||||
connect.WithClientOptions(opts...),
|
connect.WithClientOptions(opts...),
|
||||||
),
|
),
|
||||||
|
serviceAction: connect.NewClient[v1.ServiceActionRequest, v1.ServiceActionResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+HomelabServiceServiceActionProcedure,
|
||||||
|
connect.WithSchema(homelabServiceMethods.ByName("ServiceAction")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
runScript: connect.NewClient[v1.RunScriptRequest, v1.RunScriptResponse](
|
runScript: connect.NewClient[v1.RunScriptRequest, v1.RunScriptResponse](
|
||||||
httpClient,
|
httpClient,
|
||||||
baseURL+HomelabServiceRunScriptProcedure,
|
baseURL+HomelabServiceRunScriptProcedure,
|
||||||
@@ -102,11 +112,12 @@ func NewHomelabServiceClient(httpClient connect.HTTPClient, baseURL string, opts
|
|||||||
|
|
||||||
// homelabServiceClient implements HomelabServiceClient.
|
// homelabServiceClient implements HomelabServiceClient.
|
||||||
type homelabServiceClient struct {
|
type homelabServiceClient struct {
|
||||||
listContent *connect.Client[v1.ListContentRequest, v1.ListContentResponse]
|
listContent *connect.Client[v1.ListContentRequest, v1.ListContentResponse]
|
||||||
listDocs *connect.Client[v1.ListDocsRequest, v1.ListDocsResponse]
|
listDocs *connect.Client[v1.ListDocsRequest, v1.ListDocsResponse]
|
||||||
getDoc *connect.Client[v1.GetDocRequest, v1.GetDocResponse]
|
getDoc *connect.Client[v1.GetDocRequest, v1.GetDocResponse]
|
||||||
saveDoc *connect.Client[v1.SaveDocRequest, v1.SaveDocResponse]
|
saveDoc *connect.Client[v1.SaveDocRequest, v1.SaveDocResponse]
|
||||||
runScript *connect.Client[v1.RunScriptRequest, v1.RunScriptResponse]
|
serviceAction *connect.Client[v1.ServiceActionRequest, v1.ServiceActionResponse]
|
||||||
|
runScript *connect.Client[v1.RunScriptRequest, v1.RunScriptResponse]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListContent calls homelab.v1.HomelabService.ListContent.
|
// ListContent calls homelab.v1.HomelabService.ListContent.
|
||||||
@@ -129,6 +140,11 @@ func (c *homelabServiceClient) SaveDoc(ctx context.Context, req *connect.Request
|
|||||||
return c.saveDoc.CallUnary(ctx, req)
|
return c.saveDoc.CallUnary(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceAction calls homelab.v1.HomelabService.ServiceAction.
|
||||||
|
func (c *homelabServiceClient) ServiceAction(ctx context.Context, req *connect.Request[v1.ServiceActionRequest]) (*connect.Response[v1.ServiceActionResponse], error) {
|
||||||
|
return c.serviceAction.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
// RunScript calls homelab.v1.HomelabService.RunScript.
|
// RunScript calls homelab.v1.HomelabService.RunScript.
|
||||||
func (c *homelabServiceClient) RunScript(ctx context.Context, req *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) {
|
func (c *homelabServiceClient) RunScript(ctx context.Context, req *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) {
|
||||||
return c.runScript.CallUnary(ctx, req)
|
return c.runScript.CallUnary(ctx, req)
|
||||||
@@ -140,6 +156,7 @@ type HomelabServiceHandler interface {
|
|||||||
ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error)
|
ListDocs(context.Context, *connect.Request[v1.ListDocsRequest]) (*connect.Response[v1.ListDocsResponse], error)
|
||||||
GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error)
|
GetDoc(context.Context, *connect.Request[v1.GetDocRequest]) (*connect.Response[v1.GetDocResponse], error)
|
||||||
SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error)
|
SaveDoc(context.Context, *connect.Request[v1.SaveDocRequest]) (*connect.Response[v1.SaveDocResponse], error)
|
||||||
|
ServiceAction(context.Context, *connect.Request[v1.ServiceActionRequest]) (*connect.Response[v1.ServiceActionResponse], error)
|
||||||
RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error)
|
RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +191,12 @@ func NewHomelabServiceHandler(svc HomelabServiceHandler, opts ...connect.Handler
|
|||||||
connect.WithSchema(homelabServiceMethods.ByName("SaveDoc")),
|
connect.WithSchema(homelabServiceMethods.ByName("SaveDoc")),
|
||||||
connect.WithHandlerOptions(opts...),
|
connect.WithHandlerOptions(opts...),
|
||||||
)
|
)
|
||||||
|
homelabServiceServiceActionHandler := connect.NewUnaryHandler(
|
||||||
|
HomelabServiceServiceActionProcedure,
|
||||||
|
svc.ServiceAction,
|
||||||
|
connect.WithSchema(homelabServiceMethods.ByName("ServiceAction")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
homelabServiceRunScriptHandler := connect.NewUnaryHandler(
|
homelabServiceRunScriptHandler := connect.NewUnaryHandler(
|
||||||
HomelabServiceRunScriptProcedure,
|
HomelabServiceRunScriptProcedure,
|
||||||
svc.RunScript,
|
svc.RunScript,
|
||||||
@@ -190,6 +213,8 @@ func NewHomelabServiceHandler(svc HomelabServiceHandler, opts ...connect.Handler
|
|||||||
homelabServiceGetDocHandler.ServeHTTP(w, r)
|
homelabServiceGetDocHandler.ServeHTTP(w, r)
|
||||||
case HomelabServiceSaveDocProcedure:
|
case HomelabServiceSaveDocProcedure:
|
||||||
homelabServiceSaveDocHandler.ServeHTTP(w, r)
|
homelabServiceSaveDocHandler.ServeHTTP(w, r)
|
||||||
|
case HomelabServiceServiceActionProcedure:
|
||||||
|
homelabServiceServiceActionHandler.ServeHTTP(w, r)
|
||||||
case HomelabServiceRunScriptProcedure:
|
case HomelabServiceRunScriptProcedure:
|
||||||
homelabServiceRunScriptHandler.ServeHTTP(w, r)
|
homelabServiceRunScriptHandler.ServeHTTP(w, r)
|
||||||
default:
|
default:
|
||||||
@@ -217,6 +242,10 @@ func (UnimplementedHomelabServiceHandler) SaveDoc(context.Context, *connect.Requ
|
|||||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.SaveDoc is not implemented"))
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.SaveDoc is not implemented"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (UnimplementedHomelabServiceHandler) ServiceAction(context.Context, *connect.Request[v1.ServiceActionRequest]) (*connect.Response[v1.ServiceActionResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.ServiceAction is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
func (UnimplementedHomelabServiceHandler) RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) {
|
func (UnimplementedHomelabServiceHandler) RunScript(context.Context, *connect.Request[v1.RunScriptRequest]) (*connect.Response[v1.RunScriptResponse], error) {
|
||||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.RunScript is not implemented"))
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("homelab.v1.HomelabService.RunScript is not implemented"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ message ListContentResponse {
|
|||||||
repeated Link links = 1;
|
repeated Link links = 1;
|
||||||
repeated Item monitors = 2;
|
repeated Item monitors = 2;
|
||||||
repeated Item exec = 3;
|
repeated Item exec = 3;
|
||||||
|
repeated Service services = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DocMeta {
|
message DocMeta {
|
||||||
@@ -49,11 +50,44 @@ message SaveDocResponse {
|
|||||||
string hash = 1;
|
string hash = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One card in the Services view: a systemd user unit or a docker compose project.
|
||||||
|
enum ServiceKind {
|
||||||
|
SYSTEMD = 0;
|
||||||
|
DOCKER = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Service {
|
||||||
|
ServiceKind kind = 1;
|
||||||
|
string name = 2; // unit name without .service / compose project name (or container name if unlabeled)
|
||||||
|
string state = 3; // systemd: ActiveState. docker: active | partial | inactive
|
||||||
|
string substate = 4; // systemd: SubState. docker: "up/total"
|
||||||
|
string load_state = 5; // systemd only: loaded | not-found (= disabled by the user, not an error)
|
||||||
|
string unit_file_state = 6; // systemd only: enabled | disabled | static
|
||||||
|
repeated string containers = 7; // docker only: containers the card controls
|
||||||
|
string error = 8; // probe failure, empty = ok
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServiceOp {
|
||||||
|
START = 0;
|
||||||
|
STOP = 1;
|
||||||
|
RESTART = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServiceActionRequest {
|
||||||
|
string name = 1;
|
||||||
|
ServiceKind kind = 2;
|
||||||
|
ServiceOp op = 3;
|
||||||
|
}
|
||||||
|
message ServiceActionResponse {
|
||||||
|
string output = 1;
|
||||||
|
}
|
||||||
|
|
||||||
service HomelabService {
|
service HomelabService {
|
||||||
rpc ListContent(ListContentRequest) returns (ListContentResponse);
|
rpc ListContent(ListContentRequest) returns (ListContentResponse);
|
||||||
rpc ListDocs(ListDocsRequest) returns (ListDocsResponse);
|
rpc ListDocs(ListDocsRequest) returns (ListDocsResponse);
|
||||||
rpc GetDoc(GetDocRequest) returns (GetDocResponse);
|
rpc GetDoc(GetDocRequest) returns (GetDocResponse);
|
||||||
rpc SaveDoc(SaveDocRequest) returns (SaveDocResponse);
|
rpc SaveDoc(SaveDocRequest) returns (SaveDocResponse);
|
||||||
|
rpc ServiceAction(ServiceActionRequest) returns (ServiceActionResponse);
|
||||||
rpc RunScript(RunScriptRequest) returns (RunScriptResponse);
|
rpc RunScript(RunScriptRequest) returns (RunScriptResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+283
-7
@@ -10,6 +10,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -95,10 +97,12 @@ func loadConfig(path string) (*Config, error) {
|
|||||||
// --- service ---
|
// --- service ---
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
cfg *Config
|
cfg *Config
|
||||||
exec map[string]*Item // by item name, read-only after start
|
exec map[string]*Item // by item name, read-only after start
|
||||||
monVals sync.Map // monitor item name -> string (last output)
|
monVals sync.Map // monitor item name -> string (last output)
|
||||||
docsDir string
|
docsDir string
|
||||||
|
srvDir string // dir with *.service units (~/srv on the box)
|
||||||
|
probeErr sync.Map // probe key -> last logged error; poll errors log once per change, not every 3s tick
|
||||||
}
|
}
|
||||||
|
|
||||||
const execTimeout = 10 * time.Second
|
const execTimeout = 10 * time.Second
|
||||||
@@ -198,9 +202,205 @@ func (s *server) SaveDoc(_ context.Context, req *connect.Request[homelabv1.SaveD
|
|||||||
return connect.NewResponse(&homelabv1.SaveDocResponse{Hash: docHash(content)}), nil
|
return connect.NewResponse(&homelabv1.SaveDocResponse{Hash: docHash(content)}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- services: systemd user units + docker containers, one flat list ---
|
||||||
|
|
||||||
|
// Service names double as file/unit/container selectors — no separators, no traversal.
|
||||||
|
var svcNameRe = regexp.MustCompile(`^[a-z0-9_.-]+$`)
|
||||||
|
|
||||||
|
const svcTimeout = 8 * time.Second
|
||||||
|
|
||||||
|
func (s *server) services(ctx context.Context) []*homelabv1.Service {
|
||||||
|
return append(s.systemdServices(ctx), s.dockerServices(ctx)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// logProbeChange logs poll-path probe failures once per change: the poll repeats every 3s per client,
|
||||||
|
// and the same error every tick is noise. msg "" = probe healthy, logs a single recovery line.
|
||||||
|
func (s *server) logProbeChange(key, msg string) {
|
||||||
|
prev, loaded := s.probeErr.LoadOrStore(key, msg)
|
||||||
|
if !loaded {
|
||||||
|
if msg != "" {
|
||||||
|
log.Printf("probe %s: %s", key, msg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prev.(string) == msg {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.probeErr.Store(key, msg)
|
||||||
|
if msg == "" {
|
||||||
|
log.Printf("probe %s: recovered", key)
|
||||||
|
} else {
|
||||||
|
log.Printf("probe %s: %s", key, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) units() ([]string, error) {
|
||||||
|
entries, err := os.ReadDir(s.srvDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
units := make([]string, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if n := strings.TrimSuffix(e.Name(), ".service"); n != e.Name() && svcNameRe.MatchString(n) {
|
||||||
|
units = append(units, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(units)
|
||||||
|
return units, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) systemdServices(ctx context.Context) []*homelabv1.Service {
|
||||||
|
units, err := s.units()
|
||||||
|
if err != nil {
|
||||||
|
s.logProbeChange("units", err.Error())
|
||||||
|
return []*homelabv1.Service{{Kind: homelabv1.ServiceKind_SYSTEMD, Name: s.srvDir, Error: err.Error()}}
|
||||||
|
}
|
||||||
|
s.logProbeChange("units", "")
|
||||||
|
svcs := make([]*homelabv1.Service, 0, len(units))
|
||||||
|
for _, u := range units {
|
||||||
|
svcs = append(svcs, s.systemdStatus(ctx, u))
|
||||||
|
}
|
||||||
|
return svcs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ponytail: one systemctl per unit (~26 per 3s poll, ~10ms each). Batch into a single
|
||||||
|
// `systemctl show u1 u2 ... -p ...` if the poll ever shows up in a slow trace.
|
||||||
|
func (s *server) systemdStatus(ctx context.Context, unit string) *homelabv1.Service {
|
||||||
|
svc := &homelabv1.Service{Kind: homelabv1.ServiceKind_SYSTEMD, Name: unit}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, svcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
// show is the machine-readable probe (always exit 0, unlike status). LoadState=not-found means the user
|
||||||
|
// disabled the unit on purpose — a state, not an error.
|
||||||
|
out, err := exec.CommandContext(ctx, "systemctl", "--user", "show", unit+".service",
|
||||||
|
"-p", "ActiveState,SubState,LoadState,UnitFileState", "--value").Output()
|
||||||
|
// one line per property, in the order given to -p
|
||||||
|
v := strings.Split(strings.TrimRight(string(out), "\n"), "\n")
|
||||||
|
at := func(i int) string {
|
||||||
|
if i < len(v) {
|
||||||
|
return strings.TrimSpace(v[i])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
svc.State, svc.Substate, svc.LoadState, svc.UnitFileState = at(0), at(1), at(2), at(3)
|
||||||
|
if svc.LoadState == "" {
|
||||||
|
svc.LoadState = "not-found"
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
svc.Error = err.Error()
|
||||||
|
s.logProbeChange("systemctl/"+unit, svc.Error)
|
||||||
|
} else {
|
||||||
|
s.logProbeChange("systemctl/"+unit, "")
|
||||||
|
}
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
// dockerC is one container as seen by `docker inspect`.
|
||||||
|
type dockerC struct {
|
||||||
|
name, project, status, policy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerOut(ctx context.Context, args ...string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, svcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
out, err := exec.CommandContext(ctx, "docker", args...).Output()
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) dockerContainers(ctx context.Context) ([]dockerC, error) {
|
||||||
|
ids, err := dockerOut(ctx, "ps", "-aq")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(ids) == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
out, err := dockerOut(ctx, append([]string{"inspect", "-f",
|
||||||
|
`{{index .Config.Labels "com.docker.compose.project"}}|{{.Name}}|{{.State.Status}}|{{.HostConfig.RestartPolicy.Name}}`},
|
||||||
|
strings.Fields(ids)...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cs []dockerC
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||||
|
f := strings.Split(line, "|")
|
||||||
|
if len(f) != 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cs = append(cs, dockerC{project: f[0], name: strings.TrimPrefix(f[1], "/"), status: f[2], policy: f[3]})
|
||||||
|
}
|
||||||
|
sort.Slice(cs, func(i, j int) bool { return cs[i].name < cs[j].name })
|
||||||
|
return cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// working drops one-shot helpers (restart policy no + already exited: *-init, migrations). They belong to
|
||||||
|
// their project's card but must not drag its aggregate down, and must not be re-run by the start button.
|
||||||
|
func working(cs []dockerC) []dockerC {
|
||||||
|
out := make([]dockerC, 0, len(cs))
|
||||||
|
for _, c := range cs {
|
||||||
|
if c.policy == "no" && c.status != "running" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, c)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) dockerServices(ctx context.Context) []*homelabv1.Service {
|
||||||
|
cs, err := s.dockerContainers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.logProbeChange("docker", err.Error())
|
||||||
|
return []*homelabv1.Service{{Kind: homelabv1.ServiceKind_DOCKER, Name: "docker", Error: err.Error()}}
|
||||||
|
}
|
||||||
|
s.logProbeChange("docker", "")
|
||||||
|
groups := map[string][]dockerC{}
|
||||||
|
var keys []string
|
||||||
|
for _, c := range cs {
|
||||||
|
key := c.project
|
||||||
|
if key == "" {
|
||||||
|
key = c.name // docker run without a compose label -> its own card
|
||||||
|
}
|
||||||
|
if _, ok := groups[key]; !ok {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
groups[key] = append(groups[key], c)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
svcs := make([]*homelabv1.Service, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
if svc := dockerCard(k, working(groups[k])); svc != nil {
|
||||||
|
svcs = append(svcs, svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return svcs
|
||||||
|
}
|
||||||
|
|
||||||
|
// dockerCard aggregates a project: all running -> active, none -> inactive, else partial (up/total).
|
||||||
|
func dockerCard(name string, cs []dockerC) *homelabv1.Service {
|
||||||
|
if len(cs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
up := 0
|
||||||
|
names := make([]string, 0, len(cs))
|
||||||
|
for _, c := range cs {
|
||||||
|
if c.status == "running" {
|
||||||
|
up++
|
||||||
|
}
|
||||||
|
names = append(names, c.name)
|
||||||
|
}
|
||||||
|
state := "partial"
|
||||||
|
switch {
|
||||||
|
case up == len(cs):
|
||||||
|
state = "active"
|
||||||
|
case up == 0:
|
||||||
|
state = "inactive"
|
||||||
|
}
|
||||||
|
return &homelabv1.Service{Kind: homelabv1.ServiceKind_DOCKER, Name: name, State: state,
|
||||||
|
Substate: fmt.Sprintf("%d/%d", up, len(cs)), Containers: names}
|
||||||
|
}
|
||||||
|
|
||||||
// --- content RPC ---
|
// --- content RPC ---
|
||||||
|
|
||||||
func (s *server) ListContent(context.Context, *connect.Request[homelabv1.ListContentRequest]) (*connect.Response[homelabv1.ListContentResponse], error) {
|
func (s *server) ListContent(ctx context.Context, _ *connect.Request[homelabv1.ListContentRequest]) (*connect.Response[homelabv1.ListContentResponse], error) {
|
||||||
resp := &homelabv1.ListContentResponse{}
|
resp := &homelabv1.ListContentResponse{}
|
||||||
for _, l := range s.cfg.Links {
|
for _, l := range s.cfg.Links {
|
||||||
resp.Links = append(resp.Links, &homelabv1.Link{Name: l.Name, Url: l.URL})
|
resp.Links = append(resp.Links, &homelabv1.Link{Name: l.Name, Url: l.URL})
|
||||||
@@ -215,9 +415,72 @@ func (s *server) ListContent(context.Context, *connect.Request[homelabv1.ListCon
|
|||||||
it := &s.cfg.Exec[i]
|
it := &s.cfg.Exec[i]
|
||||||
resp.Exec = append(resp.Exec, &homelabv1.Item{Name: it.Name, Args: int32(it.Args), ArgTypes: it.ArgTypes})
|
resp.Exec = append(resp.Exec, &homelabv1.Item{Name: it.Name, Args: int32(it.Args), ArgTypes: it.ArgTypes})
|
||||||
}
|
}
|
||||||
|
resp.Services = s.services(ctx)
|
||||||
return connect.NewResponse(resp), nil
|
return connect.NewResponse(resp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceAction runs a fixed systemctl/docker command; only `name` is user input and it is regex-checked,
|
||||||
|
// never interpolated into a shell string.
|
||||||
|
func (s *server) ServiceAction(ctx context.Context, req *connect.Request[homelabv1.ServiceActionRequest]) (*connect.Response[homelabv1.ServiceActionResponse], error) {
|
||||||
|
name, kind := req.Msg.GetName(), req.Msg.GetKind()
|
||||||
|
if !svcNameRe.MatchString(name) {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("bad service name %q", name))
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
switch req.Msg.GetOp() {
|
||||||
|
case homelabv1.ServiceOp_START:
|
||||||
|
args = []string{"start"}
|
||||||
|
case homelabv1.ServiceOp_STOP:
|
||||||
|
args = []string{"stop"}
|
||||||
|
case homelabv1.ServiceOp_RESTART:
|
||||||
|
args = []string{"restart"}
|
||||||
|
default:
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("bad op for %q", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, svcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if kind == homelabv1.ServiceKind_DOCKER {
|
||||||
|
targets, err := s.dockerTargets(ctx, name, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInternal, err)
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return connect.NewResponse(&homelabv1.ServiceActionResponse{Output: "no containers to " + args[0]}), nil
|
||||||
|
}
|
||||||
|
cmd = exec.CommandContext(ctx, "docker", append([]string{args[0]}, targets...)...)
|
||||||
|
} else {
|
||||||
|
cmd = exec.CommandContext(ctx, "systemctl", "--user", args[0], name+".service")
|
||||||
|
}
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("service %s/%s %s: ERROR %v: %s", kind, name, args[0], err, out)
|
||||||
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("%v: %s", err, out))
|
||||||
|
}
|
||||||
|
log.Printf("service %s/%s %s -> %s", kind, name, args[0], strings.TrimSpace(string(out)))
|
||||||
|
return connect.NewResponse(&homelabv1.ServiceActionResponse{Output: strings.TrimSpace(string(out))}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dockerTargets resolves a card name (compose project, or bare container name) to the containers an op applies to.
|
||||||
|
func (s *server) dockerTargets(ctx context.Context, name, op string) ([]string, error) {
|
||||||
|
cs, err := s.dockerContainers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var targets []string
|
||||||
|
for _, c := range working(cs) {
|
||||||
|
if c.project != name && c.name != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if running := c.status == "running"; (op == "start" && !running) || (op != "start" && running) {
|
||||||
|
targets = append(targets, c.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) RunScript(_ context.Context, req *connect.Request[homelabv1.RunScriptRequest]) (*connect.Response[homelabv1.RunScriptResponse], error) {
|
func (s *server) RunScript(_ context.Context, req *connect.Request[homelabv1.RunScriptRequest]) (*connect.Response[homelabv1.RunScriptResponse], error) {
|
||||||
it, ok := s.exec[req.Msg.GetName()]
|
it, ok := s.exec[req.Msg.GetName()]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -254,13 +517,13 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := &server{cfg: cfg, exec: map[string]*Item{}, docsDir: docsDir}
|
svc := &server{cfg: cfg, exec: map[string]*Item{}, docsDir: docsDir, srvDir: srvDir()}
|
||||||
for i := range cfg.Exec {
|
for i := range cfg.Exec {
|
||||||
svc.exec[cfg.Exec[i].Name] = &cfg.Exec[i]
|
svc.exec[cfg.Exec[i].Name] = &cfg.Exec[i]
|
||||||
}
|
}
|
||||||
svc.startMonitors()
|
svc.startMonitors()
|
||||||
|
|
||||||
log.Printf("config %s: %d links, %d monitors, %d exec", cfgPath, len(cfg.Links), len(cfg.Monitor), len(cfg.Exec))
|
log.Printf("config %s: %d links, %d monitors, %d exec; units from %s", cfgPath, len(cfg.Links), len(cfg.Monitor), len(cfg.Exec), svc.srvDir)
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.HideBanner = true
|
e.HideBanner = true
|
||||||
@@ -302,3 +565,16 @@ func port() string {
|
|||||||
}
|
}
|
||||||
return ":8080"
|
return ":8080"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// srvDir: units live in ~/srv on the box, which is outside WorkingDirectory, so it is not repo-relative
|
||||||
|
// (unlike dist/ and docs/). SRV_DIR overrides it for dev/tests.
|
||||||
|
func srvDir() string {
|
||||||
|
if d := os.Getenv("SRV_DIR"); d != "" {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "srv"
|
||||||
|
}
|
||||||
|
return filepath.Join(home, "srv")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
// @generated from file homelab/v1/homelab.proto (package homelab.v1, syntax proto3)
|
// @generated from file homelab/v1/homelab.proto (package homelab.v1, syntax proto3)
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||||
import type { Message } from "@bufbuild/protobuf";
|
import type { Message } from "@bufbuild/protobuf";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the file homelab/v1/homelab.proto.
|
* Describes the file homelab/v1/homelab.proto.
|
||||||
*/
|
*/
|
||||||
export const file_homelab_v1_homelab: GenFile = /*@__PURE__*/
|
export const file_homelab_v1_homelab: GenFile = /*@__PURE__*/
|
||||||
fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiUgoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGRhdGEYAiABKAkSDQoFdmFsdWUYAyABKAkSDAoEYXJncxgEIAEoBRIRCglhcmdfdHlwZXMYBSADKAkiIQoETGluaxIMCgRuYW1lGAEgASgJEgsKA3VybBgCIAEoCSIUChJMaXN0Q29udGVudFJlcXVlc3QiegoTTGlzdENvbnRlbnRSZXNwb25zZRIfCgVsaW5rcxgBIAMoCzIQLmhvbWVsYWIudjEuTGluaxIiCghtb25pdG9ycxgCIAMoCzIQLmhvbWVsYWIudjEuSXRlbRIeCgRleGVjGAMgAygLMhAuaG9tZWxhYi52MS5JdGVtIiUKB0RvY01ldGESDAoEbmFtZRgBIAEoCRIMCgRoYXNoGAIgASgJIhEKD0xpc3REb2NzUmVxdWVzdCI1ChBMaXN0RG9jc1Jlc3BvbnNlEiEKBGRvY3MYASADKAsyEy5ob21lbGFiLnYxLkRvY01ldGEiHQoNR2V0RG9jUmVxdWVzdBIMCgRuYW1lGAEgASgJIiEKDkdldERvY1Jlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkiLwoOU2F2ZURvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIh8KD1NhdmVEb2NSZXNwb25zZRIMCgRoYXNoGAEgASgJIi4KEFJ1blNjcmlwdFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIMCgRhcmdzGAIgAygJIiMKEVJ1blNjcmlwdFJlc3BvbnNlEg4KBm91dHB1dBgBIAEoCTL2AgoOSG9tZWxhYlNlcnZpY2USTgoLTGlzdENvbnRlbnQSHi5ob21lbGFiLnYxLkxpc3RDb250ZW50UmVxdWVzdBofLmhvbWVsYWIudjEuTGlzdENvbnRlbnRSZXNwb25zZRJFCghMaXN0RG9jcxIbLmhvbWVsYWIudjEuTGlzdERvY3NSZXF1ZXN0GhwuaG9tZWxhYi52MS5MaXN0RG9jc1Jlc3BvbnNlEj8KBkdldERvYxIZLmhvbWVsYWIudjEuR2V0RG9jUmVxdWVzdBoaLmhvbWVsYWIudjEuR2V0RG9jUmVzcG9uc2USQgoHU2F2ZURvYxIaLmhvbWVsYWIudjEuU2F2ZURvY1JlcXVlc3QaGy5ob21lbGFiLnYxLlNhdmVEb2NSZXNwb25zZRJICglSdW5TY3JpcHQSHC5ob21lbGFiLnYxLlJ1blNjcmlwdFJlcXVlc3QaHS5ob21lbGFiLnYxLlJ1blNjcmlwdFJlc3BvbnNlQipaKHB3YS1ob21lbGFiLW1vbi9nZW4vaG9tZWxhYi92MTtob21lbGFidjFiBnByb3RvMw");
|
fileDesc("Chhob21lbGFiL3YxL2hvbWVsYWIucHJvdG8SCmhvbWVsYWIudjEiUgoESXRlbRIMCgRuYW1lGAEgASgJEgwKBGRhdGEYAiABKAkSDQoFdmFsdWUYAyABKAkSDAoEYXJncxgEIAEoBRIRCglhcmdfdHlwZXMYBSADKAkiIQoETGluaxIMCgRuYW1lGAEgASgJEgsKA3VybBgCIAEoCSIUChJMaXN0Q29udGVudFJlcXVlc3QioQEKE0xpc3RDb250ZW50UmVzcG9uc2USHwoFbGlua3MYASADKAsyEC5ob21lbGFiLnYxLkxpbmsSIgoIbW9uaXRvcnMYAiADKAsyEC5ob21lbGFiLnYxLkl0ZW0SHgoEZXhlYxgDIAMoCzIQLmhvbWVsYWIudjEuSXRlbRIlCghzZXJ2aWNlcxgEIAMoCzITLmhvbWVsYWIudjEuU2VydmljZSIlCgdEb2NNZXRhEgwKBG5hbWUYASABKAkSDAoEaGFzaBgCIAEoCSIRCg9MaXN0RG9jc1JlcXVlc3QiNQoQTGlzdERvY3NSZXNwb25zZRIhCgRkb2NzGAEgAygLMhMuaG9tZWxhYi52MS5Eb2NNZXRhIh0KDUdldERvY1JlcXVlc3QSDAoEbmFtZRgBIAEoCSIhCg5HZXREb2NSZXNwb25zZRIPCgdjb250ZW50GAEgASgJIi8KDlNhdmVEb2NSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHY29udGVudBgCIAEoCSIfCg9TYXZlRG9jUmVzcG9uc2USDAoEaGFzaBgBIAEoCSKvAQoHU2VydmljZRIlCgRraW5kGAEgASgOMhcuaG9tZWxhYi52MS5TZXJ2aWNlS2luZBIMCgRuYW1lGAIgASgJEg0KBXN0YXRlGAMgASgJEhAKCHN1YnN0YXRlGAQgASgJEhIKCmxvYWRfc3RhdGUYBSABKAkSFwoPdW5pdF9maWxlX3N0YXRlGAYgASgJEhIKCmNvbnRhaW5lcnMYByADKAkSDQoFZXJyb3IYCCABKAkibgoUU2VydmljZUFjdGlvblJlcXVlc3QSDAoEbmFtZRgBIAEoCRIlCgRraW5kGAIgASgOMhcuaG9tZWxhYi52MS5TZXJ2aWNlS2luZBIhCgJvcBgDIAEoDjIVLmhvbWVsYWIudjEuU2VydmljZU9wIicKFVNlcnZpY2VBY3Rpb25SZXNwb25zZRIOCgZvdXRwdXQYASABKAkiLgoQUnVuU2NyaXB0UmVxdWVzdBIMCgRuYW1lGAEgASgJEgwKBGFyZ3MYAiADKAkiIwoRUnVuU2NyaXB0UmVzcG9uc2USDgoGb3V0cHV0GAEgASgJKiYKC1NlcnZpY2VLaW5kEgsKB1NZU1RFTUQQABIKCgZET0NLRVIQASotCglTZXJ2aWNlT3ASCQoFU1RBUlQQABIICgRTVE9QEAESCwoHUkVTVEFSVBACMswDCg5Ib21lbGFiU2VydmljZRJOCgtMaXN0Q29udGVudBIeLmhvbWVsYWIudjEuTGlzdENvbnRlbnRSZXF1ZXN0Gh8uaG9tZWxhYi52MS5MaXN0Q29udGVudFJlc3BvbnNlEkUKCExpc3REb2NzEhsuaG9tZWxhYi52MS5MaXN0RG9jc1JlcXVlc3QaHC5ob21lbGFiLnYxLkxpc3REb2NzUmVzcG9uc2USPwoGR2V0RG9jEhkuaG9tZWxhYi52MS5HZXREb2NSZXF1ZXN0GhouaG9tZWxhYi52MS5HZXREb2NSZXNwb25zZRJCCgdTYXZlRG9jEhouaG9tZWxhYi52MS5TYXZlRG9jUmVxdWVzdBobLmhvbWVsYWIudjEuU2F2ZURvY1Jlc3BvbnNlElQKDVNlcnZpY2VBY3Rpb24SIC5ob21lbGFiLnYxLlNlcnZpY2VBY3Rpb25SZXF1ZXN0GiEuaG9tZWxhYi52MS5TZXJ2aWNlQWN0aW9uUmVzcG9uc2USSAoJUnVuU2NyaXB0EhwuaG9tZWxhYi52MS5SdW5TY3JpcHRSZXF1ZXN0Gh0uaG9tZWxhYi52MS5SdW5TY3JpcHRSZXNwb25zZUIqWihwd2EtaG9tZWxhYi1tb24vZ2VuL2hvbWVsYWIvdjE7aG9tZWxhYnYxYgZwcm90bzM");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message homelab.v1.Item
|
* @generated from message homelab.v1.Item
|
||||||
@@ -110,6 +110,11 @@ export type ListContentResponse = Message<"homelab.v1.ListContentResponse"> & {
|
|||||||
* @generated from field: repeated homelab.v1.Item exec = 3;
|
* @generated from field: repeated homelab.v1.Item exec = 3;
|
||||||
*/
|
*/
|
||||||
exec: Item[];
|
exec: Item[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated homelab.v1.Service services = 4;
|
||||||
|
*/
|
||||||
|
services: Service[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,6 +251,116 @@ export type SaveDocResponse = Message<"homelab.v1.SaveDocResponse"> & {
|
|||||||
export const SaveDocResponseSchema: GenMessage<SaveDocResponse> = /*@__PURE__*/
|
export const SaveDocResponseSchema: GenMessage<SaveDocResponse> = /*@__PURE__*/
|
||||||
messageDesc(file_homelab_v1_homelab, 10);
|
messageDesc(file_homelab_v1_homelab, 10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from message homelab.v1.Service
|
||||||
|
*/
|
||||||
|
export type Service = Message<"homelab.v1.Service"> & {
|
||||||
|
/**
|
||||||
|
* @generated from field: homelab.v1.ServiceKind kind = 1;
|
||||||
|
*/
|
||||||
|
kind: ServiceKind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unit name without .service / compose project name (or container name if unlabeled)
|
||||||
|
*
|
||||||
|
* @generated from field: string name = 2;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* systemd: ActiveState. docker: active | partial | inactive
|
||||||
|
*
|
||||||
|
* @generated from field: string state = 3;
|
||||||
|
*/
|
||||||
|
state: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* systemd: SubState. docker: "up/total"
|
||||||
|
*
|
||||||
|
* @generated from field: string substate = 4;
|
||||||
|
*/
|
||||||
|
substate: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* systemd only: loaded | not-found (= disabled by the user, not an error)
|
||||||
|
*
|
||||||
|
* @generated from field: string load_state = 5;
|
||||||
|
*/
|
||||||
|
loadState: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* systemd only: enabled | disabled | static
|
||||||
|
*
|
||||||
|
* @generated from field: string unit_file_state = 6;
|
||||||
|
*/
|
||||||
|
unitFileState: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* docker only: containers the card controls
|
||||||
|
*
|
||||||
|
* @generated from field: repeated string containers = 7;
|
||||||
|
*/
|
||||||
|
containers: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* probe failure, empty = ok
|
||||||
|
*
|
||||||
|
* @generated from field: string error = 8;
|
||||||
|
*/
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the message homelab.v1.Service.
|
||||||
|
* Use `create(ServiceSchema)` to create a new message.
|
||||||
|
*/
|
||||||
|
export const ServiceSchema: GenMessage<Service> = /*@__PURE__*/
|
||||||
|
messageDesc(file_homelab_v1_homelab, 11);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from message homelab.v1.ServiceActionRequest
|
||||||
|
*/
|
||||||
|
export type ServiceActionRequest = Message<"homelab.v1.ServiceActionRequest"> & {
|
||||||
|
/**
|
||||||
|
* @generated from field: string name = 1;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: homelab.v1.ServiceKind kind = 2;
|
||||||
|
*/
|
||||||
|
kind: ServiceKind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: homelab.v1.ServiceOp op = 3;
|
||||||
|
*/
|
||||||
|
op: ServiceOp;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the message homelab.v1.ServiceActionRequest.
|
||||||
|
* Use `create(ServiceActionRequestSchema)` to create a new message.
|
||||||
|
*/
|
||||||
|
export const ServiceActionRequestSchema: GenMessage<ServiceActionRequest> = /*@__PURE__*/
|
||||||
|
messageDesc(file_homelab_v1_homelab, 12);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from message homelab.v1.ServiceActionResponse
|
||||||
|
*/
|
||||||
|
export type ServiceActionResponse = Message<"homelab.v1.ServiceActionResponse"> & {
|
||||||
|
/**
|
||||||
|
* @generated from field: string output = 1;
|
||||||
|
*/
|
||||||
|
output: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the message homelab.v1.ServiceActionResponse.
|
||||||
|
* Use `create(ServiceActionResponseSchema)` to create a new message.
|
||||||
|
*/
|
||||||
|
export const ServiceActionResponseSchema: GenMessage<ServiceActionResponse> = /*@__PURE__*/
|
||||||
|
messageDesc(file_homelab_v1_homelab, 13);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message homelab.v1.RunScriptRequest
|
* @generated from message homelab.v1.RunScriptRequest
|
||||||
*/
|
*/
|
||||||
@@ -266,7 +381,7 @@ export type RunScriptRequest = Message<"homelab.v1.RunScriptRequest"> & {
|
|||||||
* Use `create(RunScriptRequestSchema)` to create a new message.
|
* Use `create(RunScriptRequestSchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export const RunScriptRequestSchema: GenMessage<RunScriptRequest> = /*@__PURE__*/
|
export const RunScriptRequestSchema: GenMessage<RunScriptRequest> = /*@__PURE__*/
|
||||||
messageDesc(file_homelab_v1_homelab, 11);
|
messageDesc(file_homelab_v1_homelab, 14);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message homelab.v1.RunScriptResponse
|
* @generated from message homelab.v1.RunScriptResponse
|
||||||
@@ -283,7 +398,56 @@ export type RunScriptResponse = Message<"homelab.v1.RunScriptResponse"> & {
|
|||||||
* Use `create(RunScriptResponseSchema)` to create a new message.
|
* Use `create(RunScriptResponseSchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export const RunScriptResponseSchema: GenMessage<RunScriptResponse> = /*@__PURE__*/
|
export const RunScriptResponseSchema: GenMessage<RunScriptResponse> = /*@__PURE__*/
|
||||||
messageDesc(file_homelab_v1_homelab, 12);
|
messageDesc(file_homelab_v1_homelab, 15);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One card in the Services view: a systemd user unit or a docker compose project.
|
||||||
|
*
|
||||||
|
* @generated from enum homelab.v1.ServiceKind
|
||||||
|
*/
|
||||||
|
export enum ServiceKind {
|
||||||
|
/**
|
||||||
|
* @generated from enum value: SYSTEMD = 0;
|
||||||
|
*/
|
||||||
|
SYSTEMD = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from enum value: DOCKER = 1;
|
||||||
|
*/
|
||||||
|
DOCKER = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the enum homelab.v1.ServiceKind.
|
||||||
|
*/
|
||||||
|
export const ServiceKindSchema: GenEnum<ServiceKind> = /*@__PURE__*/
|
||||||
|
enumDesc(file_homelab_v1_homelab, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from enum homelab.v1.ServiceOp
|
||||||
|
*/
|
||||||
|
export enum ServiceOp {
|
||||||
|
/**
|
||||||
|
* @generated from enum value: START = 0;
|
||||||
|
*/
|
||||||
|
START = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from enum value: STOP = 1;
|
||||||
|
*/
|
||||||
|
STOP = 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from enum value: RESTART = 2;
|
||||||
|
*/
|
||||||
|
RESTART = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the enum homelab.v1.ServiceOp.
|
||||||
|
*/
|
||||||
|
export const ServiceOpSchema: GenEnum<ServiceOp> = /*@__PURE__*/
|
||||||
|
enumDesc(file_homelab_v1_homelab, 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from service homelab.v1.HomelabService
|
* @generated from service homelab.v1.HomelabService
|
||||||
@@ -321,6 +485,14 @@ export const HomelabService: GenService<{
|
|||||||
input: typeof SaveDocRequestSchema;
|
input: typeof SaveDocRequestSchema;
|
||||||
output: typeof SaveDocResponseSchema;
|
output: typeof SaveDocResponseSchema;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @generated from rpc homelab.v1.HomelabService.ServiceAction
|
||||||
|
*/
|
||||||
|
serviceAction: {
|
||||||
|
methodKind: "unary";
|
||||||
|
input: typeof ServiceActionRequestSchema;
|
||||||
|
output: typeof ServiceActionResponseSchema;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @generated from rpc homelab.v1.HomelabService.RunScript
|
* @generated from rpc homelab.v1.HomelabService.RunScript
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user