diff --git a/.env.example b/.env.example index 3cd1b42..8e99691 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ EASYPANEL_ENDPOINT=https://panel.example.com EASYPANEL_API_KEY=your-easypanel-api-key # Optional Go duration. Deploy calls can block while images pull (default: 5m). EASYPANEL_HTTP_TIMEOUT=5m +# Optional. Set to 1 to skip the startup panel version/support-range probe. +# EASYPANEL_SKIP_VERSION_CHECK=1 GRPC_PORT=50051 GRPC_AUTH_TOKENS=client-token-1,client-token-2 DEFAULT_DOMAIN=xyz.easypanel.host diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..71320ac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog + +All notable changes to this project are documented here. Versions follow the +`vMAJOR.MINOR.PATCH` git tags that drive the release pipeline. + +## v0.1.0 + +Supported Easypanel versions: **2.32.0 .. 2.33.1**, both bounds backed by pinned +route surfaces in `internal/easypanel/testdata/`. + +### Breaking + +- `GetServiceStatus` no longer returns environment variable values by default. + Keys are kept and values become ``. Set `include_env: true` on the + request to get clear text. Easypanel stores secrets in the same env blob its + inspect routes return, and a `GRPC_AUTH_TOKENS` entry can read every project. + +### Features + +- `GetServiceStatus` now reports runtime state alongside stored configuration: + `status` (`running` / `stopped` / `unknown`), `running_containers`, + `containers[]` (id, name, image, state, status, created) and, for compose + services, `compose_services[]`. Compose services fall back to the running + container's image, which the panel's inspect route never provides. +- `GetLogs` gained the panel's remaining Loki filters: `stream`, `levels`, + `search`, `start` and `end`. `limit` is clamped to the panel's maximum of 1000 + instead of being rejected as a validation error. +- Panel version support window is now explicit (`MinSupportedVersion`, + `MaxTestedVersion`). The gRPC server probes the live panel at startup and logs + whether it is inside that window. Skip with `EASYPANEL_SKIP_VERSION_CHECK=1`. +- New CLI commands: `settings panel-version` (panel version + supported range) + and `settings logs` (log aggregation status). +- `scripts/panel-surface.sh` + `scripts/extract-panel-surface.py` re-pin the panel + route surface from the official `easypanel/easypanel` images. + +### Fixes + +- `GetLogs` no longer surfaces the panel's opaque `[BAD_REQUEST] fetch failed`. + Easypanel serves logs only from its own Loki deployment, which exists only when + log aggregation is enabled (Settings -> Logs, licensed feature). The RPC now + prechecks `logs.getSettings` and returns `FailedPrecondition` naming the cause + and the fix; an unreachable-but-enabled log store is reported separately. +- Invalid `stream` values are rejected locally with `InvalidArgument` rather than + producing a panel validation error. +- A failed container query no longer fails the whole `GetServiceStatus` call; it + degrades to `status: "unknown"` with configuration intact. + +### Internal + +- Every panel route is declared in `internal/easypanel/routes.go` instead of being + spread across string literals. +- Pinned panel route surfaces for 2.32.0, 2.32.2, 2.33.0 and 2.33.1 with tests + asserting every route used still exists, that its input shape is unchanged across + the supported range, and that the declared window matches the pinned files. +- New unit tests cover log filter forwarding, limit clamping, precondition + mapping, runtime status assembly, env redaction, version parsing and the panel + version probe. New live test `TestGRPC_RuntimeStatusAndLogs`. +- Enabling log aggregation now takes effect on the next `GetLogs` call: the + disabled precheck invalidates the cached settings instead of waiting out the TTL. +- `EASYPANEL_SKIP_VERSION_CHECK` is parsed as a boolean, so `=0` no longer skips + the probe. + +## v0.0.2 + +- Apply Easypanel config changes via redeploy. +- Full resource limits (CPU/memory reservation) plus `UpdateDeploy` RPC. +- Docker images and release pipeline with `dev` / release tagging. +- Deployment guide. + +## v0.0.1 + +- Initial release: Easypanel tRPC CLI and gRPC PaaS adapter with bearer token + auth, container and compose deployments, domain management. diff --git a/README.md b/README.md index 51acbf6..0798eb2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ A Go toolkit for managing [Easypanel](https://easypanel.io) deployments — both - **Domain Management** — Default subdomains + custom domains with HTTPS - **Lifecycle Control** — Start, stop, restart, scale, update env/resources - **Resource Limits** — CPU and memory constraints per service +- **Runtime Status** — Live container state, image, and compose sub-services per service +- **Version Pinned** — Panel route surface pinned per Easypanel release, checked by tests and at startup ## Quick Start @@ -180,7 +182,7 @@ Tokens are configured via `GRPC_AUTH_TOKENS` (comma-separated). Tokens grant pan | `DeployContainer` | Deploy Docker image → create service + set image + env + deploy + auto-domain | | `DeployCompose` | Deploy docker-compose stack (strips `ports:`, routes via `composeService`) | | `DestroyService` | Remove service and all associated domains | -| `GetServiceStatus` | Service details: name, type, image, env, domains, deploy URL | +| `GetServiceStatus` | Service config (name, type, image, env, domains, deploy URL) plus runtime state (`status`, running containers, compose sub-services). Env values are redacted unless `include_env` is set | | `ListServices` | All services in a project | | `AddDomain` | Attach domain to service (supports compose sub-services) | | `RemoveDomain` | Detach domain by ID | @@ -189,7 +191,7 @@ Tokens are configured via `GRPC_AUTH_TOKENS` (comma-separated). Tokens grant pan | `RestartService` | Restart service | | `StopService` | Stop service | | `StartService` | Start service | -| `GetLogs` | Fetch app/compose logs with structured entries and flat text | +| `GetLogs` | Fetch app/compose logs with structured entries and flat text. Requires panel log aggregation (see [Logs](#logs)) | | `ScaleService` | Set replica count | | `UpdateResources` | Set CPU/memory limits | | `UpdateDeploy` | Set app replicas, zero-downtime behavior, and command | @@ -281,9 +283,13 @@ grpcurl -plaintext -H "authorization: your-token" -d '{ │ ├── auth/ │ │ └── interceptor.go # gRPC auth interceptor (unary + stream) │ ├── easypanel/ -│ │ └── client.go # Reusable Easypanel HTTP client +│ │ ├── client.go # Reusable Easypanel HTTP client +│ │ ├── routes.go # Every panel route this project calls +│ │ ├── version.go # Panel version probe + supported range + log settings +│ │ └── testdata/ # Pinned panel route surfaces (2.32.2 / 2.33.0 / 2.33.1) │ ├── server/ -│ │ └── paas.go # PaaS gRPC service implementation +│ │ ├── paas.go # PaaS gRPC service implementation +│ │ └── logs.go # Panel log response flattening │ └── version/ │ └── version.go # Build metadata injected via -ldflags ├── proto/ @@ -293,10 +299,14 @@ grpcurl -plaintext -H "authorization: your-token" -d '{ ├── .github/workflows/ │ ├── ci.yml # PR checks: fmt, vet, test, build, image build │ └── release.yml # master -> :dev images, tags -> release + binaries +├── scripts/ +│ ├── panel-surface.sh # Re-pin panel route surfaces from official images +│ └── extract-panel-surface.py # Route/input extractor for the panel bundle ├── Dockerfile # Multi-stage, multi-arch (targets: server, cli) ├── Makefile # build / test / cross / docker targets ├── .dockerignore ├── grpc_e2e_test.go # E2E tests (live panel) +├── CHANGELOG.md # Release notes per version ├── .env.example # Environment template ├── .gitignore ├── go.mod @@ -320,9 +330,122 @@ The gRPC server **automatically strips `ports:` directives** from compose conten --- +## Service status + +`GetServiceStatus` merges the panel's stored configuration with the live Docker +view, so a single call answers both "how is it configured" and "is it up": + +```jsonc +{ + "name": "hermes-a1", + "type": "compose", + "enabled": true, + "image": "nousresearch/hermes-agent:v2026.8.3", + "env": "API_KEY=", // values hidden unless include_env is set + "domains": ["pods-hermes-a1-agent.cv911b.easypanel.host"], + "status": "running", // running | stopped | unknown + "runningContainers": 1, + "containers": [{ + "id": "21bb7f989f0f", + "name": "pods_hermes-a1-agent-1", + "image": "nousresearch/hermes-agent:v2026.8.3", + "state": "running", + "status": "Up 47 hours" + }], + "composeServices": ["agent"] // compose only +} +``` + +The panel reports only running containers, so an empty list means stopped. If the +panel refuses the container query the RPC still returns the configuration with +`status: "unknown"` instead of failing. Compose services have no configured image, +so `image` falls back to the running container's image. + +### Env values are redacted by default + +Easypanel stores secrets in the same env blob its inspect routes return, and a +gRPC token is panel-wide: any caller can read any project. `GetServiceStatus` +therefore returns keys with `` values. Set `include_env: true` to get +clear text: + +```bash +grpcurl -plaintext -H "authorization: Bearer $TOKEN" \ + -d '{"project":"demo","service":"api","serviceType":"app","includeEnv":true}' \ + localhost:50051 paas.PaaS/GetServiceStatus +``` + +--- + +## Supported Easypanel versions + +| | Version | +|---|---| +| Minimum supported | `2.32.0` | +| Newest tested | `2.33.1` | + +The panel's route surface is pinned in `internal/easypanel/testdata/panel-surface-*.json`, +extracted from the official `easypanel/easypanel` images. Every route this project +calls is listed in `internal/easypanel/routes.go`, and +`TestRequiredRoutesExistInEverySupportedPanel` asserts each one exists in every +pinned release. Both bounds of the window are backed by a pinned surface: `2.32.0`, +`2.32.2`, `2.33.0` and `2.33.1` expose the same routes with identical input fields +(374 procedures in `2.32.0`, 375 from `2.32.2` on, the addition being unrelated to +this project), which is why one code path covers the range. + +The gRPC server probes the live panel version at startup (`GET /api/openapi.json`, +the one version route present in all supported releases) and logs whether it falls +inside that window. Set `EASYPANEL_SKIP_VERSION_CHECK=1` to skip the probe. + +```bash +# Which panel am I talking to? +deploy-everything settings panel-version + +# Re-pin the route surface after a panel upgrade (needs docker + python3) +scripts/panel-surface.sh 2.32.0 2.32.2 2.33.0 2.33.1 +go test ./internal/easypanel/ -run TestRequiredRoutes -v +``` + +After adding a newer release, bump `MaxTestedVersion` in +`internal/easypanel/version.go` to match the newest pinned file. `MinSupportedVersion` +must equal the oldest pinned file; the tests enforce both. + +--- + +## Logs + +Easypanel serves service logs from its own Loki deployment and has no other log +source: no `docker logs` route exists in the panel API. Loki and Promtail are only +deployed when log aggregation is enabled under **Settings -> Logs**, which the panel +gates behind a license with advanced monitoring. + +When aggregation is off, the panel answers log queries with an opaque +`[BAD_REQUEST] fetch failed`. `GetLogs` prechecks `logs.getSettings` and returns +`FailedPrecondition` with the reason instead: + +```text +panel log aggregation is disabled, so Loki is not deployed and the panel has no +log source. Enable it in the Easypanel UI under Settings -> Logs (requires a +license with advanced monitoring), then retry GetLogs. +``` + +Check the current state with: + +```bash +deploy-everything settings logs +``` + +`GetLogs` filters map straight onto the panel's Loki query: `stream` +(`stdout`/`stderr`), `levels`, `search`, `start`, `end`, and `limit` (clamped to the +panel's maximum of 1000). Supplying `start` switches the query to oldest-first. + +--- + ## Testing ```bash +# Unit tests (no panel needed): route surface, version parsing, log/status mapping +go test ./internal/... ./cmd/... -count=1 + # Run E2E tests (requires live Easypanel instance) go test -v -run "TestGRPC_" -timeout 180s @@ -331,6 +454,7 @@ go test -v -run TestGRPC_AuthReject -timeout 30s go test -v -run TestGRPC_DeployContainerE2E -timeout 120s go test -v -run TestGRPC_DeployComposeE2E -timeout 120s go test -v -run TestGRPC_UpdateResources -timeout 60s +go test -v -run TestGRPC_RuntimeStatusAndLogs -timeout 120s ``` Tests create real services on the panel, verify HTTP reachability, and clean up after themselves. @@ -382,11 +506,11 @@ One `Dockerfile`, two final stages, both distroless + static (`CGO_ENABLED=0`), ```bash # Run the gRPC server docker run --rm -p 50051:50051 --env-file .env \ - ghcr.io/igun997/deploy-everything:0.0.2 + ghcr.io/igun997/deploy-everything:0.1.0 # Run a CLI command docker run --rm --env-file .env \ - ghcr.io/igun997/deploy-everything-cli:0.0.2 projects list + ghcr.io/igun997/deploy-everything-cli:0.1.0 projects list # Bleeding edge (current master) docker run --rm --env-file .env \ @@ -407,13 +531,13 @@ docker run --rm --env-file .env \ | Git event | Image tags | |-----------|-----------| | push/merge to `master` | `dev`, `dev-` | -| tag `v0.0.2` | `0.0.2`, `0.0`, `0`, `latest` | +| tag `v0.1.0` | `0.1.0`, `0.1`, `0`, `latest` | | tag `v1.2.3` | `1.2.3`, `1.2`, `1`, `latest` | | pre-release tag `v1.2.3-rc1` | `1.2.3-rc1` only (no `latest`, no `1.2`/`1`) | So `:dev` always points at the current `master`, `:latest` always points at the newest stable release. -The leading `v` is stripped from image tags: git tag `v0.0.2` produces image tag `0.0.2`. The binaries still report the full `v0.0.2` from `version`. +The leading `v` is stripped from image tags: git tag `v0.1.0` produces image tag `0.1.0`. The binaries still report the full `v0.1.0` from `version`. ### Cutting a release diff --git a/cmd/grpc-server/main.go b/cmd/grpc-server/main.go index 1175c8b..fb87752 100644 --- a/cmd/grpc-server/main.go +++ b/cmd/grpc-server/main.go @@ -1,11 +1,14 @@ package main import ( + "context" "fmt" "log" "net" "os" + "strconv" "strings" + "time" "github.com/igun997/deploy-everything/internal/auth" "github.com/igun997/deploy-everything/internal/easypanel" @@ -36,6 +39,13 @@ func main() { // Easypanel client ep := easypanel.NewClient(endpoint, token) + // Probe the panel version so route drift shows up at boot instead of on the + // first failing RPC. Never fatal: a slow or briefly unreachable panel should + // not stop the server from serving. + if !envBool("EASYPANEL_SKIP_VERSION_CHECK") { + reportPanelVersion(ep, endpoint) + } + // Auth tokens tokens := strings.Split(grpcTokens, ",") for i := range tokens { @@ -68,6 +78,39 @@ func main() { } } +// envBool reads a boolean switch. Anything Go's parser rejects (including an +// unset value) counts as false, so EASYPANEL_SKIP_VERSION_CHECK=0 keeps the +// probe enabled. +func envBool(key string) bool { + v, err := strconv.ParseBool(os.Getenv(key)) + return err == nil && v +} + +// reportPanelVersion logs the panel version and whether it falls inside the +// verified support window. Set EASYPANEL_SKIP_VERSION_CHECK to skip the probe. +func reportPanelVersion(ep *easypanel.Client, endpoint string) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + ver, err := ep.PanelVersion(ctx) + if err != nil { + log.Printf("warning: could not read Easypanel version from %s: %v", endpoint, err) + return + } + + tooOld, untested := easypanel.SupportRange(ver) + switch { + case tooOld: + log.Printf("warning: Easypanel %s is older than the supported minimum %s; routes may be missing", + ver, easypanel.MinSupportedVersion) + case untested: + log.Printf("warning: Easypanel %s is newer than the newest tested release %s; run scripts/panel-surface.sh to refresh the pinned route surface", + ver, easypanel.MaxTestedVersion) + default: + log.Printf("Easypanel %s (supported range %s..%s)", ver, easypanel.MinSupportedVersion, easypanel.MaxTestedVersion) + } +} + func mustEnv(key string) string { v := os.Getenv(key) if v == "" { diff --git a/cmd/grpc-server/main_test.go b/cmd/grpc-server/main_test.go new file mode 100644 index 0000000..b29085e --- /dev/null +++ b/cmd/grpc-server/main_test.go @@ -0,0 +1,22 @@ +package main + +import "testing" + +func TestEnvBool(t *testing.T) { + cases := map[string]bool{ + "": false, + "0": false, + "false": false, + "no": false, // not a Go bool literal, so it stays off + "1": true, + "true": true, + "TRUE": true, + "t": true, + } + for value, want := range cases { + t.Setenv("EASYPANEL_SKIP_VERSION_CHECK", value) + if got := envBool("EASYPANEL_SKIP_VERSION_CHECK"); got != want { + t.Errorf("envBool(%q) = %v, want %v", value, got, want) + } + } +} diff --git a/cmd/services.go b/cmd/services.go index b7af5f7..e528643 100644 --- a/cmd/services.go +++ b/cmd/services.go @@ -581,6 +581,13 @@ var servicesLogsCmd = &cobra.Command{ svcType, _ := cmd.Flags().GetString("type") composeService, _ := cmd.Flags().GetString("compose-service") limit, _ := cmd.Flags().GetInt("limit") + if limit <= 0 { + limit = 200 + } + // The panel caps the log limit at 1000 with a validation error. + if limit > 1000 { + limit = 1000 + } input := map[string]any{ "projectName": args[0], @@ -598,7 +605,7 @@ var servicesLogsCmd = &cobra.Command{ var resp TRPCResponse[logQueryResult] if err := c.call(context.Background(), route, input, &resp); err != nil { - return err + return annotateLogError(err) } for _, line := range flattenLogLines(resp.JSON) { fmt.Println(line) @@ -607,6 +614,22 @@ var servicesLogsCmd = &cobra.Command{ }, } +// annotateLogError explains the panel's opaque log failures. The panel serves +// logs only from its own Loki deployment, so a failed internal fetch means log +// aggregation is off or easypanel-loki is down. +func annotateLogError(err error) error { + msg := strings.ToLower(err.Error()) + for _, marker := range []string{"fetch failed", "loki http", "loki query", "econnrefused", "enotfound"} { + if strings.Contains(msg, marker) { + return fmt.Errorf("%w\n\nThe panel could not reach its log store. "+ + "Logs come from Easypanel's own Loki service, which only exists when log "+ + "aggregation is enabled (Settings -> Logs, requires a license with advanced "+ + "monitoring). Check with: deploy-everything settings logs", err) + } + } + return err +} + // logQueryResult mirrors the panel's log store response: label-grouped streams // each holding [timestamp, line] pairs. type logQueryResult struct { diff --git a/cmd/settings.go b/cmd/settings.go index c75951e..121c9ad 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/igun997/deploy-everything/internal/easypanel" "github.com/spf13/cobra" ) @@ -33,6 +34,57 @@ func init() { settingsCmd.AddCommand(settingsSetLECmd) settingsCmd.AddCommand(settingsRestartCmd) settingsCmd.AddCommand(settingsChangeCredsCmd) + settingsCmd.AddCommand(settingsLogsCmd) + settingsCmd.AddCommand(settingsVersionCmd) +} + +// logAggregationSettings mirrors the panel's `settings.logs` record. The panel +// returns null when log aggregation was never configured. +type logAggregationSettings struct { + Enabled bool `json:"enabled"` + RetentionType string `json:"retentionType"` + RetentionValue int `json:"retentionValue"` +} + +var settingsLogsCmd = &cobra.Command{ + Use: "logs", + Short: "Show log aggregation status (services logs needs it enabled)", + RunE: func(cmd *cobra.Command, args []string) error { + c := newPanelClient() + var resp TRPCResponse[*logAggregationSettings] + if err := c.call(context.Background(), "/api/trpc/logs.getSettings", nil, &resp); err != nil { + return err + } + if resp.JSON == nil { + fmt.Println("log aggregation: not configured (Loki is not deployed, so service logs are unavailable)") + return nil + } + fmt.Printf("log aggregation: enabled=%v retention=%d %s\n", + resp.JSON.Enabled, resp.JSON.RetentionValue, resp.JSON.RetentionType) + return nil + }, +} + +var settingsVersionCmd = &cobra.Command{ + Use: "panel-version", + Short: "Show the Easypanel version this CLI is talking to", + RunE: func(cmd *cobra.Command, args []string) error { + ep := easypanel.NewClient(getEndpoint(), getAPIKey()) + ver, err := ep.PanelVersion(context.Background()) + if err != nil { + return err + } + tooOld, untested := easypanel.SupportRange(ver) + fmt.Printf("Easypanel %s (supported %s..%s)\n", ver, + easypanel.MinSupportedVersion, easypanel.MaxTestedVersion) + if tooOld { + fmt.Println("warning: older than the supported minimum, some routes may be missing") + } + if untested { + fmt.Println("warning: newer than the newest tested release") + } + return nil + }, } var settingsIPCmd = &cobra.Command{ diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index fbd3e8d..155f797 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -5,7 +5,10 @@ This guide covers deploying the `deploy-everything` gRPC server and using both i - **gRPC API** — deploy prebuilt container images or complete Compose stacks. - **CLI** — deploy images, inline Dockerfiles, Git/GitHub sources, and Compose sources. -Examples use release `0.0.2`. +Examples use release `0.1.0`. + +Supported Easypanel versions: `2.32.0` .. `2.33.1`. Check what you are pointed at with +`deploy-everything settings panel-version`. ## 1. Prerequisites @@ -27,6 +30,8 @@ cp .env.example .env EASYPANEL_ENDPOINT=https://panel.example.com EASYPANEL_API_KEY=replace-with-panel-api-key EASYPANEL_HTTP_TIMEOUT=5m +# Optional: skip the startup panel version / support-range probe. +# EASYPANEL_SKIP_VERSION_CHECK=1 GRPC_PORT=50051 GRPC_AUTH_TOKENS=replace-with-long-random-client-token @@ -56,21 +61,21 @@ The gRPC deployment RPCs create services inside projects; they do not create pro ### Docker ```bash -docker pull ghcr.io/igun997/deploy-everything:0.0.2 +docker pull ghcr.io/igun997/deploy-everything:0.1.0 docker run -d \ --name deploy-everything \ --restart unless-stopped \ -p 50051:50051 \ --env-file .env \ - ghcr.io/igun997/deploy-everything:0.0.2 + ghcr.io/igun997/deploy-everything:0.1.0 ``` Verify: ```bash docker logs deploy-everything -docker run --rm ghcr.io/igun997/deploy-everything:0.0.2 version +docker run --rm ghcr.io/igun997/deploy-everything:0.1.0 version ``` ### Docker Compose @@ -79,7 +84,7 @@ docker run --rm ghcr.io/igun997/deploy-everything:0.0.2 version # compose.grpc-server.yml services: deploy-everything: - image: ghcr.io/igun997/deploy-everything:0.0.2 + image: ghcr.io/igun997/deploy-everything:0.1.0 restart: unless-stopped ports: - "50051:50051" @@ -326,6 +331,31 @@ cat <<'JSON' | paas_call ListServices JSON ``` +`GetServiceStatus` returns stored configuration plus live runtime state: + +| Field | Meaning | +|-------|---------| +| `status` | `running`, `stopped`, or `unknown` when the panel refuses the container query | +| `runningContainers` | Count of running containers backing the service | +| `containers[]` | `id`, `name`, `image`, `state`, `status` ("Up 2 hours"), `created` | +| `composeServices[]` | Internal compose service names (compose only) | +| `env` | Keys with `` values unless the request sets `includeEnv` | + +The panel lists only running containers, so an empty `containers` list means the +service is stopped. Compose services carry no configured image, so `image` falls +back to the running container's image. + +#### Reading env values + +Env holds secrets and every `GRPC_AUTH_TOKENS` entry can read every project, so +values are redacted by default. Ask for them explicitly: + +```bash +cat <<'JSON' | paas_call GetServiceStatus +{"project":"demo","service":"api","serviceType":"app","includeEnv":true} +JSON +``` + ### Environment `UpdateEnv` replaces the saved env content and automatically deploys app/compose services. @@ -429,7 +459,41 @@ cat <<'JSON' | paas_call GetLogs JSON ``` -Response includes flat `logs` text and structured `entries` (`timestamp`, `line`, `level`, `stream`). Easypanel's logging backend must be enabled; otherwise panel can return `fetch failed`. +Response includes flat `logs` text and structured `entries` (`timestamp`, `line`, `level`, `stream`). + +Optional filters map onto the panel's Loki query: + +| Field | Meaning | +|-------|---------| +| `limit` | Max lines, default 200, clamped to the panel maximum of 1000 | +| `stream` | `stdout` or `stderr` | +| `levels` | Keep only these detected levels, e.g. `["warn","error"]` | +| `search` | Case-insensitive substring filter applied by the log store | +| `start` / `end` | Window bounds (unix nanoseconds or RFC3339). `start` switches to oldest-first | + +```bash +cat <<'JSON' | paas_call GetLogs +{ + "project": "demo", + "service": "api", + "serviceType": "app", + "stream": "stderr", + "levels": ["error"], + "search": "timeout", + "limit": 100 +} +JSON +``` + +**Logs require panel log aggregation.** Easypanel serves logs only from its own +Loki service and exposes no other log source. Loki and Promtail are deployed only +when log aggregation is enabled under **Settings -> Logs**, which the panel gates +behind a license with advanced monitoring. Without it, `GetLogs` returns +`FailedPrecondition` explaining exactly that. Check the current state with: + +```bash +$CLI settings logs +``` ### Scale and deploy settings @@ -506,14 +570,14 @@ Use the release binary: ```bash # Download archive from: -# https://github.com/igun997/deploy-everything/releases/tag/v0.0.2 +# https://github.com/igun997/deploy-everything/releases/tag/v0.1.0 ``` Or run the CLI image: ```bash docker run --rm --env-file .env \ - ghcr.io/igun997/deploy-everything-cli:0.0.2 version + ghcr.io/igun997/deploy-everything-cli:0.1.0 version ``` For local commands: @@ -528,7 +592,7 @@ Examples below use `$CLI`. If using Docker for every command, replace `$CLI` wit ```bash docker run --rm --env-file .env \ - ghcr.io/igun997/deploy-everything-cli:0.0.2 + ghcr.io/igun997/deploy-everything-cli:0.1.0 ``` ## 9. CLI: normal app deployment cases @@ -776,7 +840,35 @@ Large Compose pulls/builds can take several minutes. ### Logs return `fetch failed` -Enable/configure Easypanel's logging backend. `logs.getSettings` returning null indicates panel log storage is not configured. +The panel could not reach its Loki service. Logs come from Easypanel's own log +aggregation stack (`easypanel-loki` + `easypanel-promtail`), which is only deployed +when log aggregation is enabled under **Settings -> Logs** (requires a license with +advanced monitoring). There is no `docker logs` fallback in the panel API. + +```bash +$CLI settings logs +# log aggregation: not configured (Loki is not deployed, so service logs are unavailable) +``` + +The gRPC `GetLogs` prechecks this and returns `FailedPrecondition` with the reason +instead of the panel's raw `fetch failed`. + +### Panel route missing or unexpected `NOT_FOUND` + +Check the panel version against the supported window: + +```bash +$CLI settings panel-version +# Easypanel 2.33.0 (supported 2.32.0..2.33.1) +``` + +After a panel upgrade past the tested maximum, re-pin the route surface and run the +compatibility test: + +```bash +scripts/panel-surface.sh 2.32.0 2.32.2 2.33.0 +go test ./internal/easypanel/ -run TestRequiredRoutes -v +``` ### gRPC `Unauthenticated` @@ -791,5 +883,8 @@ Send exact token configured in `GRPC_AUTH_TOKENS`: ```bash ./deploy-everything version ./grpc-server version -docker run --rm ghcr.io/igun997/deploy-everything-cli:0.0.2 version +docker run --rm ghcr.io/igun997/deploy-everything-cli:0.1.0 version + +# Easypanel version + supported range +./deploy-everything settings panel-version ``` diff --git a/grpc_e2e_test.go b/grpc_e2e_test.go index f7c59c7..bc500e0 100644 --- a/grpc_e2e_test.go +++ b/grpc_e2e_test.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "os" + "strings" "testing" "time" @@ -14,8 +15,10 @@ import ( pb "github.com/igun997/deploy-everything/proto" "github.com/joho/godotenv" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" + grpcstatus "google.golang.org/grpc/status" ) func init() { @@ -447,3 +450,101 @@ func TestGRPC_UpdateResources(t *testing.T) { } t.Log(" ✓ destroyed") } + +// TestGRPC_RuntimeStatusAndLogs covers the runtime half of GetServiceStatus and +// the log path. Log aggregation is a licensed panel feature, so the log +// assertions adapt: a panel without it must answer FailedPrecondition rather +// than leaking the panel's opaque "fetch failed". +func TestGRPC_RuntimeStatusAndLogs(t *testing.T) { + client, cleanup := startTestServer(t) + defer cleanup() + + ctx := authCtx() + project := "pods" + service := "status-e2e" + + client.DestroyService(ctx, &pb.DestroyServiceRequest{ + Project: project, Service: service, ServiceType: "app", + }) + + if _, err := client.DeployContainer(ctx, &pb.DeployContainerRequest{ + Project: project, + Service: service, + Image: "nginx:alpine", + Port: 80, + Env: map[string]string{"API_KEY": "e2e-secret-value"}, + }); err != nil { + t.Fatalf("DeployContainer: %v", err) + } + defer client.DestroyService(ctx, &pb.DestroyServiceRequest{ + Project: project, Service: service, ServiceType: "app", + }) + + // Swarm needs a moment to report a running task. + var status *pb.GetServiceStatusResponse + for attempt := 0; attempt < 10; attempt++ { + var err error + status, err = client.GetServiceStatus(ctx, &pb.GetServiceStatusRequest{ + Project: project, Service: service, ServiceType: "app", + }) + if err != nil { + t.Fatalf("GetServiceStatus: %v", err) + } + if status.Status == "running" { + break + } + time.Sleep(3 * time.Second) + } + + if status.Status != "running" { + t.Fatalf(" ✗ expected running status, got %q (containers=%d)", status.Status, status.RunningContainers) + } + if status.RunningContainers < 1 || len(status.Containers) < 1 { + t.Fatalf(" ✗ expected at least one container, got %d", status.RunningContainers) + } + c := status.Containers[0] + if c.Id == "" || c.Name == "" || c.State != "running" { + t.Fatalf(" ✗ incomplete container payload: %+v", c) + } + t.Logf(" ✓ status=%s containers=%d first=%s (%s, %s)", + status.Status, status.RunningContainers, c.Name, c.Image, c.Status) + + _, err := client.GetLogs(ctx, &pb.GetLogsRequest{ + Project: project, Service: service, ServiceType: "app", Limit: 10, + }) + switch { + case err == nil: + t.Log(" ✓ GetLogs returned entries (panel log aggregation is enabled)") + case grpcstatus.Code(err) == codes.FailedPrecondition: + t.Logf(" ✓ GetLogs reported the panel precondition: %v", grpcstatus.Convert(err).Message()) + default: + t.Fatalf("GetLogs: unexpected error: %v", err) + } + + // Guardrail: an unsupported stream value must never reach the panel. + _, err = client.GetLogs(ctx, &pb.GetLogsRequest{ + Project: project, Service: service, ServiceType: "app", Stream: "stdio", + }) + if grpcstatus.Code(err) != codes.InvalidArgument { + t.Fatalf("want InvalidArgument for a bad stream, got %v", err) + } + t.Log(" ✓ invalid stream rejected locally") + + // Env values are secrets: they must not come back unless asked for. + if strings.Contains(status.Env, "e2e-secret-value") { + t.Fatalf(" ✗ env value leaked without include_env: %q", status.Env) + } + if !strings.Contains(status.Env, "API_KEY=") { + t.Fatalf(" ✗ expected redacted env key, got %q", status.Env) + } + withEnv, err := client.GetServiceStatus(ctx, &pb.GetServiceStatusRequest{ + Project: project, Service: service, ServiceType: "app", IncludeEnv: true, + }) + if err != nil { + t.Fatalf("GetServiceStatus(include_env): %v", err) + } + if !strings.Contains(withEnv.Env, "e2e-secret-value") { + t.Fatalf(" ✗ include_env should return values, got %q", withEnv.Env) + } + t.Log(" ✓ env redacted by default, returned with include_env") +} diff --git a/internal/easypanel/client.go b/internal/easypanel/client.go index 6b39f7b..9de13f7 100644 --- a/internal/easypanel/client.go +++ b/internal/easypanel/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -18,6 +19,7 @@ type Client struct { baseURL string token string httpClient *http.Client + versionState } // defaultTimeout covers the panel's synchronous deploy calls. deployService @@ -76,6 +78,34 @@ func (e *APIError) Error() string { return fmt.Sprintf("[%s] %s", e.Code, e.Message) } +// logStoreFailureMarkers are the messages the panel surfaces when its own +// fetch to Loki fails. The panel wraps them all as BAD_REQUEST, so the message +// is the only signal. +var logStoreFailureMarkers = []string{ + "fetch failed", // undici could not connect or resolve easypanel-loki + "loki http", // Loki answered with a non-2xx status + "loki query", // Loki answered with status != success + "econnrefused", // surfaced by some Node builds + "enotfound", // DNS failure for the Loki service name + "eai_again", // transient DNS failure +} + +// IsLogStoreUnreachable reports whether err describes the panel failing to +// reach its Loki log store rather than a bad request from this client. +func IsLogStoreUnreachable(err error) bool { + var apiErr *APIError + if !errors.As(err, &apiErr) { + return false + } + msg := strings.ToLower(apiErr.Message) + for _, marker := range logStoreFailureMarkers { + if strings.Contains(msg, marker) { + return true + } + } + return false +} + // trpcErrorEnvelope is how the panel reports errors: nested under "json", with // zod field errors under data.zodErrors. type trpcErrorEnvelope struct { diff --git a/internal/easypanel/routes.go b/internal/easypanel/routes.go new file mode 100644 index 0000000..f903b46 --- /dev/null +++ b/internal/easypanel/routes.go @@ -0,0 +1,94 @@ +package easypanel + +import "fmt" + +// Panel routes used by this project, as `.` paths under +// /api/trpc/. They are listed here so the supported-version compatibility test +// can assert every route still exists in each pinned panel release. +const ( + RouteInspectProject = "projects.inspectProject" + RouteGetDockerContainers = "projects.getDockerContainers" + RouteListDomains = "domains.listDomains" + RouteCreateDomain = "domains.createDomain" + RouteDeleteDomain = "domains.deleteDomain" + RouteLogsGetSettings = "logs.getSettings" + RouteQueryServiceLogs = "logs.queryServiceLogs" + RouteQueryComposeLogs = "logs.queryComposeServiceLogs" + RouteComposeDockerServices = "services.compose.getDockerServices" +) + +// ServiceTypes are the panel service routers this project drives. The panel +// exposes one router per service type and their procedure sets differ, so +// per-type routes are built with ServiceRoute. +var ServiceTypes = []string{"app", "compose"} + +// Per-type procedure names. Not every service router exposes all of them; +// appOnlyProcedures records the ones that only exist under services.app. +const ( + ProcCreateService = "createService" + ProcDeployService = "deployService" + ProcDestroyService = "destroyService" + ProcInspectService = "inspectService" + ProcRestartService = "restartService" + ProcStartService = "startService" + ProcStopService = "stopService" + ProcUpdateEnv = "updateEnv" + ProcUpdateDeploy = "updateDeploy" + ProcUpdateResources = "updateResources" + ProcUpdateSourceImage = "updateSourceImage" + ProcUpdateSourceInline = "updateSourceInline" +) + +// ServiceRoute builds `services..`. +func ServiceRoute(svcType, procedure string) string { + return fmt.Sprintf("services.%s.%s", svcType, procedure) +} + +// AppRoute builds a route on the app service router. +func AppRoute(procedure string) string { return ServiceRoute("app", procedure) } + +// ComposeRoute builds a route on the compose service router. +func ComposeRoute(procedure string) string { return ServiceRoute("compose", procedure) } + +// sharedServiceProcedures exist under every service router this project uses. +var sharedServiceProcedures = []string{ + ProcCreateService, + ProcDeployService, + ProcDestroyService, + ProcInspectService, + ProcRestartService, + ProcStartService, + ProcStopService, + ProcUpdateEnv, +} + +// typeOnlyProcedures are procedures exposed by a single service router. +var typeOnlyProcedures = map[string][]string{ + "app": {ProcUpdateDeploy, ProcUpdateResources, ProcUpdateSourceImage}, + "compose": {ProcUpdateSourceInline}, +} + +// RequiredRoutes lists every panel route this project can call. The +// compatibility test asserts all of them exist in each supported panel release. +func RequiredRoutes() []string { + routes := []string{ + RouteInspectProject, + RouteGetDockerContainers, + RouteListDomains, + RouteCreateDomain, + RouteDeleteDomain, + RouteLogsGetSettings, + RouteQueryServiceLogs, + RouteQueryComposeLogs, + RouteComposeDockerServices, + } + for _, svcType := range ServiceTypes { + for _, proc := range sharedServiceProcedures { + routes = append(routes, ServiceRoute(svcType, proc)) + } + for _, proc := range typeOnlyProcedures[svcType] { + routes = append(routes, ServiceRoute(svcType, proc)) + } + } + return routes +} diff --git a/internal/easypanel/routes_test.go b/internal/easypanel/routes_test.go new file mode 100644 index 0000000..a1363a2 --- /dev/null +++ b/internal/easypanel/routes_test.go @@ -0,0 +1,181 @@ +package easypanel + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "testing" +) + +// panelSurface is the pinned route surface extracted from an Easypanel image. +// Regenerate with scripts/panel-surface.sh. +type panelSurface struct { + PanelVersion string `json:"panelVersion"` + Procedures map[string]struct { + Kind string `json:"kind"` + OperationID string `json:"operationId"` + Required []string `json:"required"` + Optional []string `json:"optional"` + } `json:"procedures"` +} + +func loadSurfaces(t *testing.T) map[string]panelSurface { + t.Helper() + paths, err := filepath.Glob("testdata/panel-surface-*.json") + if err != nil { + t.Fatalf("glob surfaces: %v", err) + } + if len(paths) == 0 { + t.Fatal("no pinned panel surfaces in testdata") + } + out := make(map[string]panelSurface, len(paths)) + for _, p := range paths { + raw, err := os.ReadFile(p) + if err != nil { + t.Fatalf("read %s: %v", p, err) + } + var s panelSurface + if err := json.Unmarshal(raw, &s); err != nil { + t.Fatalf("decode %s: %v", p, err) + } + if s.PanelVersion == "" || len(s.Procedures) == 0 { + t.Fatalf("%s: empty surface", p) + } + out[s.PanelVersion] = s + } + return out +} + +// TestRequiredRoutesExistInEverySupportedPanel is the guard against silent +// panel API drift: every route this project calls must exist in every pinned +// release inside the supported window. +func TestRequiredRoutesExistInEverySupportedPanel(t *testing.T) { + surfaces := loadSurfaces(t) + routes := RequiredRoutes() + if len(routes) == 0 { + t.Fatal("RequiredRoutes is empty") + } + + for version, surface := range surfaces { + for _, route := range routes { + if _, ok := surface.Procedures[route]; !ok { + t.Errorf("panel %s does not expose %s", version, route) + } + } + } +} + +// TestSupportWindowMatchesPinnedSurfaces keeps the documented support range and +// the pinned fixtures from drifting apart. +func TestSupportWindowMatchesPinnedSurfaces(t *testing.T) { + surfaces := loadSurfaces(t) + versions := make([]Version, 0, len(surfaces)) + for raw := range surfaces { + v, err := ParseVersion(raw) + if err != nil { + t.Fatalf("parse pinned version %q: %v", raw, err) + } + versions = append(versions, v) + } + sort.Slice(versions, func(i, j int) bool { return versions[i].Compare(versions[j]) < 0 }) + + oldest, newest := versions[0], versions[len(versions)-1] + minV, err := ParseVersion(MinSupportedVersion) + if err != nil { + t.Fatalf("parse MinSupportedVersion: %v", err) + } + maxV, err := ParseVersion(MaxTestedVersion) + if err != nil { + t.Fatalf("parse MaxTestedVersion: %v", err) + } + + if oldest.Compare(minV) != 0 { + // A floor without a fixture would never be exercised by + // TestRequiredRoutesExistInEverySupportedPanel. + t.Errorf("oldest pinned surface is %s but MinSupportedVersion is %s", oldest, MinSupportedVersion) + } + if newest.Compare(maxV) != 0 { + t.Errorf("newest pinned surface is %s but MaxTestedVersion is %s", newest, MaxTestedVersion) + } +} + +// TestPinnedSurfacesAgreeOnUsedRoutes documents that the routes this project +// depends on take the same inputs in every supported release. If a future panel +// changes one, this test points at the exact field difference. +func TestPinnedSurfacesAgreeOnUsedRoutes(t *testing.T) { + surfaces := loadSurfaces(t) + type shape struct { + required string + optional string + } + + for _, route := range RequiredRoutes() { + seen := map[shape][]string{} + for version, surface := range surfaces { + proc, ok := surface.Procedures[route] + if !ok { + continue // reported by TestRequiredRoutesExistInEverySupportedPanel + } + key := shape{ + required: join(proc.Required), + optional: join(proc.Optional), + } + seen[key] = append(seen[key], version) + } + if len(seen) > 1 { + t.Errorf("%s input shape differs across panels: %v", route, seen) + } + } +} + +func join(in []string) string { + sorted := append([]string(nil), in...) + sort.Strings(sorted) + out := "" + for i, s := range sorted { + if i > 0 { + out += "," + } + out += s + } + return out +} + +func TestParseVersion(t *testing.T) { + cases := map[string]Version{ + "2.33.0": {Major: 2, Minor: 33, Patch: 0}, + "v2.32.2": {Major: 2, Minor: 32, Patch: 2}, + "2.33.1-canary": {Major: 2, Minor: 33, Patch: 1}, + "2.34": {Major: 2, Minor: 34, Patch: 0}, + "3": {Major: 3, Minor: 0, Patch: 0}, + } + for raw, want := range cases { + got, err := ParseVersion(raw) + if err != nil { + t.Fatalf("ParseVersion(%q): %v", raw, err) + } + if got.Major != want.Major || got.Minor != want.Minor || got.Patch != want.Patch { + t.Errorf("ParseVersion(%q) = %d.%d.%d, want %d.%d.%d", + raw, got.Major, got.Minor, got.Patch, want.Major, want.Minor, want.Patch) + } + } + if _, err := ParseVersion("not-a-version"); err == nil { + t.Error("expected error for non-numeric version") + } +} + +func TestSupportRange(t *testing.T) { + tooOld, untested := SupportRange(Version{Major: 2, Minor: 31, Patch: 0}) + if !tooOld || untested { + t.Errorf("2.31.0: tooOld=%v untested=%v, want true/false", tooOld, untested) + } + tooOld, untested = SupportRange(Version{Major: 2, Minor: 33, Patch: 0}) + if tooOld || untested { + t.Errorf("2.33.0: tooOld=%v untested=%v, want false/false", tooOld, untested) + } + tooOld, untested = SupportRange(Version{Major: 2, Minor: 34, Patch: 0}) + if tooOld || !untested { + t.Errorf("2.34.0: tooOld=%v untested=%v, want false/true", tooOld, untested) + } +} diff --git a/internal/easypanel/testdata/panel-surface-2.32.0.json b/internal/easypanel/testdata/panel-surface-2.32.0.json new file mode 100644 index 0000000..8a0b1f3 --- /dev/null +++ b/internal/easypanel/testdata/panel-surface-2.32.0.json @@ -0,0 +1 @@ +{"panelVersion":"2.32.0","procedures":{"actions.getAction":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"actions.killAction":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"actions.listActions":{"kind":null,"operationId":null,"optional":["limit","projectName","serviceName","type"],"required":[]},"auth.getSession":{"kind":null,"operationId":null,"optional":[],"required":[]},"auth.getUser":{"kind":null,"operationId":null,"optional":[],"required":[]},"auth.login":{"kind":null,"operationId":null,"optional":["code","rememberMe"],"required":["email","password"]},"auth.logout":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getBasicSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getCustomCodeSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getErrorPageSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getInterfaceSettingsPublic":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getLinksSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getLogoSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getOtherLinksSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.setBasicSettings":{"kind":null,"operationId":null,"optional":[],"required":["hideIp","hideNotes","serverColor","serverName"]},"branding.setCustomCodeSettings":{"kind":null,"operationId":null,"optional":["customCode"],"required":[]},"branding.setErrorPageSettings":{"kind":null,"operationId":null,"optional":["customCss"],"required":["hideLinks","hideLogo"]},"branding.setLinksSettings":{"kind":null,"operationId":null,"optional":[],"required":["hideChangelogLink","hideDiscordLink","hideDocumentationLink","hideFeedbackLink","hideOtherLinks"]},"branding.setLogoSettings":{"kind":null,"operationId":null,"optional":["darkLogo","darkLogoMark","lightLogo","lightLogoMark"],"required":[]},"certificates.listCertificates":{"kind":null,"operationId":null,"optional":[],"required":[]},"certificates.removeCertificate":{"kind":null,"operationId":null,"optional":[],"required":["domain"]},"cloudflareTunnel.createTunnelRule":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.deleteTunnelRule":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"cloudflareTunnel.getConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.getTunnelRules":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"cloudflareTunnel.listAccounts":{"kind":null,"operationId":null,"optional":[],"required":["apiToken"]},"cloudflareTunnel.listTunnels":{"kind":null,"operationId":null,"optional":[],"required":["accountId","apiToken"]},"cloudflareTunnel.listZones":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.setConfig":{"kind":null,"operationId":null,"optional":["accountId","apiToken","tunnelId"],"required":[]},"cloudflareTunnel.startTunnel":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.stopTunnel":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.updateTunnelRule":{"kind":null,"operationId":null,"optional":[],"required":[]},"cluster.addWorkerCommand":{"kind":null,"operationId":null,"optional":[],"required":[]},"cluster.listNodes":{"kind":null,"operationId":null,"optional":[],"required":[]},"cluster.removeNode":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"databaseBackups.createDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":[]},"databaseBackups.deleteDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"databaseBackups.getServiceDatabases":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"databaseBackups.listDatabaseBackups":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"databaseBackups.restoreDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":["databaseName","path","projectName","serviceName","storageProviderId"]},"databaseBackups.runDatabaseBackup":{"kind":null,"operationId":null,"optional":["name"],"required":["id"]},"databaseBackups.updateDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":[]},"dockerBuilders.createDockerBuilder":{"kind":null,"operationId":null,"optional":["cpus","memory","memorySwap"],"required":["name"]},"dockerBuilders.listDockerBuilders":{"kind":null,"operationId":null,"optional":[],"required":[]},"dockerBuilders.removeDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"dockerBuilders.stopDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"dockerBuilders.useDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"domains.createDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"domains.deleteDomain":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"domains.getPrimaryDomain":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"domains.listDomains":{"kind":null,"operationId":null,"optional":["projectName","serviceName"],"required":[]},"domains.setPrimaryDomain":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"domains.updateDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"git.generateKey":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"git.getPublicKey":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"github.searchBranches":{"kind":null,"operationId":null,"optional":["search"],"required":["owner","repo"]},"github.searchRepos":{"kind":null,"operationId":null,"optional":[],"required":[]},"lemonLicense.activate":{"kind":null,"operationId":null,"optional":[],"required":["licenseKey"]},"lemonLicense.activateByOrder":{"kind":null,"operationId":null,"optional":[],"required":["identifier","orderId"]},"lemonLicense.deactivate":{"kind":null,"operationId":null,"optional":[],"required":[]},"lemonLicense.getLicensePayload":{"kind":null,"operationId":null,"optional":[],"required":[]},"logs.getSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"logs.getStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"logs.queryComposeServiceLogs":{"kind":null,"operationId":null,"optional":["composeInternalService","end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.queryServiceLogs":{"kind":null,"operationId":null,"optional":["end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.updateSettings":{"kind":null,"operationId":null,"optional":[],"required":["enabled","retentionType","retentionValue"]},"metrics.getAllServicesStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"metrics.getServiceStats":{"kind":null,"operationId":null,"optional":["range","step"],"required":["projectName","serviceName"]},"metrics.getSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"metrics.getStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"metrics.getSystemStats":{"kind":null,"operationId":null,"optional":["range","step"],"required":[]},"metrics.updateSettings":{"kind":null,"operationId":null,"optional":[],"required":["enabled","retentionType","retentionValue","scrapeInterval"]},"middlewares.createMiddleware":{"kind":null,"operationId":null,"optional":[],"required":[]},"middlewares.destroyMiddleware":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"middlewares.listMiddlewares":{"kind":null,"operationId":null,"optional":[],"required":[]},"middlewares.updateMiddleware":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getAdvancedStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getDockerTaskStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getMonitorTableData":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getServiceStats":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"monitorOld.getStorageStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getSystemStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"mounts.createMount":{"kind":null,"operationId":null,"optional":[],"required":[]},"mounts.deleteMount":{"kind":null,"operationId":null,"optional":[],"required":[]},"mounts.listMounts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"mounts.updateMount":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.createNotificationChannel":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.destroyNotificationChannel":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.listNotificationChannels":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.sendTestNotification":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.updateNotificationChannel":{"kind":null,"operationId":null,"optional":[],"required":[]},"portalLicense.activate":{"kind":null,"operationId":null,"optional":[],"required":[]},"portalLicense.deactivate":{"kind":null,"operationId":null,"optional":[],"required":[]},"portalLicense.getLicensePayload":{"kind":null,"operationId":null,"optional":[],"required":[]},"ports.createPort":{"kind":null,"operationId":null,"optional":[],"required":[]},"ports.deleteAllPorts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"ports.deletePort":{"kind":null,"operationId":null,"optional":[],"required":[]},"ports.listPorts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"ports.updatePort":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.canCreateProject":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.createProject":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"projects.destroyProject":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"projects.getDockerContainers":{"kind":null,"operationId":null,"optional":[],"required":["service"]},"projects.inspectProject":{"kind":null,"operationId":null,"optional":[],"required":["projectName"]},"projects.listProjects":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.listProjectsAndServices":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.updateAccess":{"kind":null,"operationId":null,"optional":[],"required":["active","projectName","userId"]},"projects.updateProjectEnv":{"kind":null,"operationId":null,"optional":["env"],"required":["projectName"]},"server.reboot":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.deployService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.disableGithubDeploy":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.enableGithubDeploy":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.getExposedPorts":{"kind":null,"operationId":null,"optional":["projectName","serviceName"],"required":[]},"services.app.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.refreshDeployToken":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateBuild":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateDeploy":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateMaintenance":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateScripts":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceDockerfile":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceGit":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceGithub":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceImage":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.uploadCodeArchive":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.gitClone":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.initService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.listPresets":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.loadPreset":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.rebuildDockerImage":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.refreshDeployToken":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.runDeployScript":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.runScript":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateDeployScript":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateGitConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateIde":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateModules":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateNginx":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateNodejs":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updatePhp":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateProcesses":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updatePython":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateRuby":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateScripts":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.common.getNotes":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.common.getServiceError":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.common.rename":{"kind":null,"operationId":null,"optional":[],"required":["newProjectName","newServiceName","oldProjectName","oldServiceName"]},"services.common.setNotes":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.deployService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.getDockerServices":{"kind":null,"operationId":null,"optional":["projectName","serviceName"],"required":[]},"services.compose.getIssues":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.refreshDeployToken":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateMaintenance":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateSourceGit":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateSourceInline":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.disablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.enablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.disableMongoExpress":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.enableMongoExpress":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.disablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.enablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.disablePgWeb":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.enablePgWeb":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.disableRedisCommander":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.enableRedisCommander":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.activatePlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.activateTheme":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.createOption":{"kind":null,"operationId":null,"optional":[],"required":["name","value"]},"services.wordpress.createRole":{"kind":null,"operationId":null,"optional":[],"required":["display_name","name"]},"services.wordpress.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.createUser":{"kind":null,"operationId":null,"optional":[],"required":["display_name","password","roles","user_email"]},"services.wordpress.dbOptimize":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.deactivatePlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.deleteOption":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.deleteRole":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.deleteTransient":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.deleteUser":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.flushCache":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getDatabaseServices":{"kind":null,"operationId":null,"optional":[],"required":["projectName"]},"services.wordpress.getMaintenanceMode":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getOptions":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getPlugins":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getProfile":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.getRoles":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getThemes":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getUsers":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getWpConfig":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.gitClone":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.initService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.installPlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.installTheme":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.mediaRegenerate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.rebuildDockerImage":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.runScript":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchPlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchReplace":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchReplaceDryRun":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchTheme":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateGitConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateIde":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateMaintenanceMode":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateNginx":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateOption":{"kind":null,"operationId":null,"optional":[],"required":["name","value"]},"services.wordpress.updatePhp":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateScripts":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateUser":{"kind":null,"operationId":null,"optional":["password"],"required":["ID","display_name","roles","user_email"]},"services.wordpress.updateWpConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateWpCore":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"settings.changeCredentials":{"kind":null,"operationId":null,"optional":[],"required":["email","newPassword","oldPassword"]},"settings.checkDockerUpdate":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.checkForUpdates":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.cleanupDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.cleanupDockerImages":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getDailyDockerCleanup":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getDemoMode":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getDockerVersion":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getGithubToken":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getGoogleAnalyticsMeasurementId":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getLetsEncryptEmail":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getPanelDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getServerIp":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getServiceDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getTelemetryDisabled":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.refreshServerIp":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.restartEasypanel":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.setDailyDockerCleanup":{"kind":null,"operationId":null,"optional":[],"required":["dailyDockerCleanup"]},"settings.setGithubToken":{"kind":null,"operationId":null,"optional":["githubToken"],"required":[]},"settings.setGoogleAnalyticsMeasurementId":{"kind":null,"operationId":null,"optional":["measurementId"],"required":[]},"settings.setLetsEncryptEmail":{"kind":null,"operationId":null,"optional":[],"required":["letsEncryptEmail"]},"settings.setPanelDomain":{"kind":null,"operationId":null,"optional":[],"required":["customPanelDomain","serveOnIp"]},"settings.setServiceDomain":{"kind":null,"operationId":null,"optional":[],"required":["customServiceDomain"]},"settings.setTelemetryDisabled":{"kind":null,"operationId":null,"optional":[],"required":["disabled"]},"settings.systemPrune":{"kind":null,"operationId":null,"optional":[],"required":[]},"setup.getStatus":{"kind":null,"operationId":null,"optional":[],"required":[]},"setup.setup":{"kind":null,"operationId":null,"optional":[],"required":["email","password","source","subscribe","terms"]},"storageProviders.common.list":{"kind":null,"operationId":null,"optional":[],"required":[]},"storageProviders.common.listOptions":{"kind":null,"operationId":null,"optional":[],"required":[]},"storageProviders.dropbox.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"storageProviders.dropbox.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.dropbox.disconnectProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.dropbox.updateProvider":{"kind":null,"operationId":null,"optional":["name"],"required":["id"]},"storageProviders.ftp.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["host","name","password","port","username"]},"storageProviders.ftp.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.ftp.updateProvider":{"kind":null,"operationId":null,"optional":["port"],"required":["host","id","name","password","username"]},"storageProviders.google.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"storageProviders.google.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.google.disconnectProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.google.updateProvider":{"kind":null,"operationId":null,"optional":["name"],"required":["id"]},"storageProviders.local.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["name","path"]},"storageProviders.local.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.local.updateProvider":{"kind":null,"operationId":null,"optional":[],"required":["id","name","path"]},"storageProviders.s3.createProvider":{"kind":null,"operationId":null,"optional":["endpoint"],"required":["accessKeyId","bucket","name","region","secretAccessKey","storageClass","subtype"]},"storageProviders.s3.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.s3.updateProvider":{"kind":null,"operationId":null,"optional":["endpoint"],"required":["accessKeyId","bucket","id","name","region","secretAccessKey","storageClass"]},"storageProviders.sftp.createProvider":{"kind":null,"operationId":null,"optional":["port"],"required":["host","name","password","username"]},"storageProviders.sftp.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.sftp.updateProvider":{"kind":null,"operationId":null,"optional":["port"],"required":["host","id","name","password","username"]},"subscription.onInvalidateActions":{"kind":null,"operationId":null,"optional":[],"required":[]},"templates.createFromSchema":{"kind":null,"operationId":null,"optional":["name"],"required":["projectName","schema"]},"traefik.getCustomConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.getDashboard":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.getEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.restart":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.setCustomConfig":{"kind":null,"operationId":null,"optional":["config"],"required":[]},"traefik.setEnv":{"kind":null,"operationId":null,"optional":["env"],"required":[]},"twoFactor.configure":{"kind":null,"operationId":null,"optional":[],"required":[]},"twoFactor.disable":{"kind":null,"operationId":null,"optional":[],"required":[]},"twoFactor.enable":{"kind":null,"operationId":null,"optional":[],"required":["code"]},"update.getStatus":{"kind":null,"operationId":null,"optional":[],"required":[]},"update.update":{"kind":null,"operationId":null,"optional":[],"required":[]},"users.createUser":{"kind":null,"operationId":null,"optional":[],"required":["admin","email","password"]},"users.destroyUser":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"users.generateApiToken":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"users.listUsers":{"kind":null,"operationId":null,"optional":[],"required":[]},"users.revokeApiToken":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"users.updateUser":{"kind":null,"operationId":null,"optional":["password"],"required":["admin","id"]},"volumeBackups.createVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":[]},"volumeBackups.destroyVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"volumeBackups.listVolumeBackups":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"volumeBackups.listVolumeMounts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"volumeBackups.runVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"volumeBackups.updateVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":[]}}} diff --git a/internal/easypanel/testdata/panel-surface-2.32.2.json b/internal/easypanel/testdata/panel-surface-2.32.2.json new file mode 100644 index 0000000..7383e0a --- /dev/null +++ b/internal/easypanel/testdata/panel-surface-2.32.2.json @@ -0,0 +1 @@ +{"panelVersion":"2.32.2","procedures":{"actions.getAction":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"actions.killAction":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"actions.listActions":{"kind":null,"operationId":null,"optional":["limit","projectName","serviceName","type"],"required":[]},"auth.getSession":{"kind":null,"operationId":null,"optional":[],"required":[]},"auth.getUser":{"kind":null,"operationId":null,"optional":[],"required":[]},"auth.login":{"kind":null,"operationId":null,"optional":["code","rememberMe"],"required":["email","password"]},"auth.logout":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getBasicSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getCustomCodeSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getErrorPageSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getInterfaceSettingsPublic":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getLinksSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getLogoSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.getOtherLinksSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"branding.setBasicSettings":{"kind":null,"operationId":null,"optional":[],"required":["hideIp","hideNotes","serverColor","serverName"]},"branding.setCustomCodeSettings":{"kind":null,"operationId":null,"optional":["customCode"],"required":[]},"branding.setErrorPageSettings":{"kind":null,"operationId":null,"optional":["customCss"],"required":["hideLinks","hideLogo"]},"branding.setLinksSettings":{"kind":null,"operationId":null,"optional":[],"required":["hideChangelogLink","hideDiscordLink","hideDocumentationLink","hideFeedbackLink","hideOtherLinks"]},"branding.setLogoSettings":{"kind":null,"operationId":null,"optional":["darkLogo","darkLogoMark","lightLogo","lightLogoMark"],"required":[]},"certificates.listCertificates":{"kind":null,"operationId":null,"optional":[],"required":[]},"certificates.removeCertificate":{"kind":null,"operationId":null,"optional":[],"required":["domain"]},"cloudflareTunnel.createTunnelRule":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.deleteTunnelRule":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"cloudflareTunnel.getConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.getTunnelRules":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"cloudflareTunnel.listAccounts":{"kind":null,"operationId":null,"optional":[],"required":["apiToken"]},"cloudflareTunnel.listTunnels":{"kind":null,"operationId":null,"optional":[],"required":["accountId","apiToken"]},"cloudflareTunnel.listZones":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.setConfig":{"kind":null,"operationId":null,"optional":["accountId","apiToken","tunnelId"],"required":[]},"cloudflareTunnel.startTunnel":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.stopTunnel":{"kind":null,"operationId":null,"optional":[],"required":[]},"cloudflareTunnel.updateTunnelRule":{"kind":null,"operationId":null,"optional":[],"required":[]},"cluster.addWorkerCommand":{"kind":null,"operationId":null,"optional":[],"required":[]},"cluster.listNodes":{"kind":null,"operationId":null,"optional":[],"required":[]},"cluster.removeNode":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"databaseBackups.createDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":[]},"databaseBackups.deleteDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"databaseBackups.getServiceDatabases":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"databaseBackups.listDatabaseBackups":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"databaseBackups.restoreDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":["databaseName","path","projectName","serviceName","storageProviderId"]},"databaseBackups.runDatabaseBackup":{"kind":null,"operationId":null,"optional":["name"],"required":["id"]},"databaseBackups.updateDatabaseBackup":{"kind":null,"operationId":null,"optional":[],"required":[]},"dockerBuilders.createDockerBuilder":{"kind":null,"operationId":null,"optional":["cpus","memory","memorySwap"],"required":["name"]},"dockerBuilders.listDockerBuilders":{"kind":null,"operationId":null,"optional":[],"required":[]},"dockerBuilders.removeDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"dockerBuilders.stopDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"dockerBuilders.useDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"domains.createDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"domains.deleteDomain":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"domains.getPrimaryDomain":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"domains.listDomains":{"kind":null,"operationId":null,"optional":["projectName","serviceName"],"required":[]},"domains.setPrimaryDomain":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"domains.updateDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"git.generateKey":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"git.getPublicKey":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"github.searchBranches":{"kind":null,"operationId":null,"optional":["search"],"required":["owner","repo"]},"github.searchRepos":{"kind":null,"operationId":null,"optional":[],"required":[]},"lemonLicense.activate":{"kind":null,"operationId":null,"optional":[],"required":["licenseKey"]},"lemonLicense.activateByOrder":{"kind":null,"operationId":null,"optional":[],"required":["identifier","orderId"]},"lemonLicense.deactivate":{"kind":null,"operationId":null,"optional":[],"required":[]},"lemonLicense.getLicenseKey":{"kind":null,"operationId":null,"optional":[],"required":[]},"lemonLicense.getLicensePayload":{"kind":null,"operationId":null,"optional":[],"required":[]},"logs.getSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"logs.getStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"logs.queryComposeServiceLogs":{"kind":null,"operationId":null,"optional":["composeInternalService","end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.queryServiceLogs":{"kind":null,"operationId":null,"optional":["end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.updateSettings":{"kind":null,"operationId":null,"optional":[],"required":["enabled","retentionType","retentionValue"]},"metrics.getAllServicesStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"metrics.getServiceStats":{"kind":null,"operationId":null,"optional":["range","step"],"required":["projectName","serviceName"]},"metrics.getSettings":{"kind":null,"operationId":null,"optional":[],"required":[]},"metrics.getStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"metrics.getSystemStats":{"kind":null,"operationId":null,"optional":["range","step"],"required":[]},"metrics.updateSettings":{"kind":null,"operationId":null,"optional":[],"required":["enabled","retentionType","retentionValue","scrapeInterval"]},"middlewares.createMiddleware":{"kind":null,"operationId":null,"optional":[],"required":[]},"middlewares.destroyMiddleware":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"middlewares.listMiddlewares":{"kind":null,"operationId":null,"optional":[],"required":[]},"middlewares.updateMiddleware":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getAdvancedStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getDockerTaskStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getMonitorTableData":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getServiceStats":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"monitorOld.getStorageStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"monitorOld.getSystemStats":{"kind":null,"operationId":null,"optional":[],"required":[]},"mounts.createMount":{"kind":null,"operationId":null,"optional":[],"required":[]},"mounts.deleteMount":{"kind":null,"operationId":null,"optional":[],"required":[]},"mounts.listMounts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"mounts.updateMount":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.createNotificationChannel":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.destroyNotificationChannel":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.listNotificationChannels":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.sendTestNotification":{"kind":null,"operationId":null,"optional":[],"required":[]},"notifications.updateNotificationChannel":{"kind":null,"operationId":null,"optional":[],"required":[]},"portalLicense.activate":{"kind":null,"operationId":null,"optional":[],"required":[]},"portalLicense.deactivate":{"kind":null,"operationId":null,"optional":[],"required":[]},"portalLicense.getLicensePayload":{"kind":null,"operationId":null,"optional":[],"required":[]},"ports.createPort":{"kind":null,"operationId":null,"optional":[],"required":[]},"ports.deleteAllPorts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"ports.deletePort":{"kind":null,"operationId":null,"optional":[],"required":[]},"ports.listPorts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"ports.updatePort":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.canCreateProject":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.createProject":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"projects.destroyProject":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"projects.getDockerContainers":{"kind":null,"operationId":null,"optional":[],"required":["service"]},"projects.inspectProject":{"kind":null,"operationId":null,"optional":[],"required":["projectName"]},"projects.listProjects":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.listProjectsAndServices":{"kind":null,"operationId":null,"optional":[],"required":[]},"projects.updateAccess":{"kind":null,"operationId":null,"optional":[],"required":["active","projectName","userId"]},"projects.updateProjectEnv":{"kind":null,"operationId":null,"optional":["env"],"required":["projectName"]},"server.reboot":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.deployService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.disableGithubDeploy":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.enableGithubDeploy":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.getExposedPorts":{"kind":null,"operationId":null,"optional":["projectName","serviceName"],"required":[]},"services.app.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.refreshDeployToken":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.app.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateBuild":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateDeploy":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateMaintenance":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateScripts":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceDockerfile":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceGit":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceGithub":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.updateSourceImage":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.app.uploadCodeArchive":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.gitClone":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.initService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.listPresets":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.loadPreset":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.rebuildDockerImage":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.refreshDeployToken":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.runDeployScript":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.runScript":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.box.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateDeployScript":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateGitConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateIde":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateModules":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateNginx":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateNodejs":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updatePhp":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateProcesses":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updatePython":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateRuby":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.box.updateScripts":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.common.getNotes":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.common.getServiceError":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.common.rename":{"kind":null,"operationId":null,"optional":[],"required":["newProjectName","newServiceName","oldProjectName","oldServiceName"]},"services.common.setNotes":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.deployService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.getDockerServices":{"kind":null,"operationId":null,"optional":["projectName","serviceName"],"required":[]},"services.compose.getIssues":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.refreshDeployToken":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.compose.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateMaintenance":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateSourceGit":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.compose.updateSourceInline":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.disablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.enablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mariadb.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mariadb.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.disableMongoExpress":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.enableMongoExpress":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mongo.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mongo.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.disablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.enablePhpMyAdmin":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.mysql.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.mysql.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.disablePgWeb":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.enablePgWeb":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.postgres.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.postgres.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.disableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.disableRedisCommander":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.disableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.enableDbGate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.enableRedisCommander":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.enableService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.exposeService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.redis.updateAdvanced":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.updateCredentials":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.redis.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.activatePlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.activateTheme":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.createOption":{"kind":null,"operationId":null,"optional":[],"required":["name","value"]},"services.wordpress.createRole":{"kind":null,"operationId":null,"optional":[],"required":["display_name","name"]},"services.wordpress.createService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.createUser":{"kind":null,"operationId":null,"optional":[],"required":["display_name","password","roles","user_email"]},"services.wordpress.dbOptimize":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.deactivatePlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.deleteOption":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.deleteRole":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.deleteTransient":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.deleteUser":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.destroyService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.flushCache":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getDatabaseServices":{"kind":null,"operationId":null,"optional":[],"required":["projectName"]},"services.wordpress.getMaintenanceMode":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getOptions":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getPlugins":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getProfile":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.getRoles":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getThemes":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getUsers":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.getWpConfig":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.gitClone":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.initService":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.inspectService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.installPlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.installTheme":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.mediaRegenerate":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.rebuildDockerImage":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.restartService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.runScript":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchPlugin":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchReplace":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchReplaceDryRun":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.searchTheme":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.startService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.stopService":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"services.wordpress.updateBasicAuth":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateGitConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateIde":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateMaintenanceMode":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateNginx":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateOption":{"kind":null,"operationId":null,"optional":[],"required":["name","value"]},"services.wordpress.updatePhp":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateRedirects":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateResources":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateScripts":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateUser":{"kind":null,"operationId":null,"optional":["password"],"required":["ID","display_name","roles","user_email"]},"services.wordpress.updateWpConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"services.wordpress.updateWpCore":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"settings.changeCredentials":{"kind":null,"operationId":null,"optional":[],"required":["email","newPassword","oldPassword"]},"settings.checkDockerUpdate":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.checkForUpdates":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.cleanupDockerBuilder":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.cleanupDockerImages":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getDailyDockerCleanup":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getDemoMode":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getDockerVersion":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getGithubToken":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getGoogleAnalyticsMeasurementId":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getLetsEncryptEmail":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getPanelDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getServerIp":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getServiceDomain":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.getTelemetryDisabled":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.refreshServerIp":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.restartEasypanel":{"kind":null,"operationId":null,"optional":[],"required":[]},"settings.setDailyDockerCleanup":{"kind":null,"operationId":null,"optional":[],"required":["dailyDockerCleanup"]},"settings.setGithubToken":{"kind":null,"operationId":null,"optional":["githubToken"],"required":[]},"settings.setGoogleAnalyticsMeasurementId":{"kind":null,"operationId":null,"optional":["measurementId"],"required":[]},"settings.setLetsEncryptEmail":{"kind":null,"operationId":null,"optional":[],"required":["letsEncryptEmail"]},"settings.setPanelDomain":{"kind":null,"operationId":null,"optional":[],"required":["customPanelDomain","serveOnIp"]},"settings.setServiceDomain":{"kind":null,"operationId":null,"optional":[],"required":["customServiceDomain"]},"settings.setTelemetryDisabled":{"kind":null,"operationId":null,"optional":[],"required":["disabled"]},"settings.systemPrune":{"kind":null,"operationId":null,"optional":[],"required":[]},"setup.getStatus":{"kind":null,"operationId":null,"optional":[],"required":[]},"setup.setup":{"kind":null,"operationId":null,"optional":[],"required":["email","password","source","subscribe","terms"]},"storageProviders.common.list":{"kind":null,"operationId":null,"optional":[],"required":[]},"storageProviders.common.listOptions":{"kind":null,"operationId":null,"optional":[],"required":[]},"storageProviders.dropbox.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"storageProviders.dropbox.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.dropbox.disconnectProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.dropbox.updateProvider":{"kind":null,"operationId":null,"optional":["name"],"required":["id"]},"storageProviders.ftp.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["host","name","password","port","username"]},"storageProviders.ftp.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.ftp.updateProvider":{"kind":null,"operationId":null,"optional":["port"],"required":["host","id","name","password","username"]},"storageProviders.google.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["name"]},"storageProviders.google.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.google.disconnectProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.google.updateProvider":{"kind":null,"operationId":null,"optional":["name"],"required":["id"]},"storageProviders.local.createProvider":{"kind":null,"operationId":null,"optional":[],"required":["name","path"]},"storageProviders.local.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.local.updateProvider":{"kind":null,"operationId":null,"optional":[],"required":["id","name","path"]},"storageProviders.s3.createProvider":{"kind":null,"operationId":null,"optional":["endpoint"],"required":["accessKeyId","bucket","name","region","secretAccessKey","storageClass","subtype"]},"storageProviders.s3.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.s3.updateProvider":{"kind":null,"operationId":null,"optional":["endpoint"],"required":["accessKeyId","bucket","id","name","region","secretAccessKey","storageClass"]},"storageProviders.sftp.createProvider":{"kind":null,"operationId":null,"optional":["port"],"required":["host","name","password","username"]},"storageProviders.sftp.deleteProvider":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"storageProviders.sftp.updateProvider":{"kind":null,"operationId":null,"optional":["port"],"required":["host","id","name","password","username"]},"subscription.onInvalidateActions":{"kind":null,"operationId":null,"optional":[],"required":[]},"templates.createFromSchema":{"kind":null,"operationId":null,"optional":["name"],"required":["projectName","schema"]},"traefik.getCustomConfig":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.getDashboard":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.getEnv":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.restart":{"kind":null,"operationId":null,"optional":[],"required":[]},"traefik.setCustomConfig":{"kind":null,"operationId":null,"optional":["config"],"required":[]},"traefik.setEnv":{"kind":null,"operationId":null,"optional":["env"],"required":[]},"twoFactor.configure":{"kind":null,"operationId":null,"optional":[],"required":[]},"twoFactor.disable":{"kind":null,"operationId":null,"optional":[],"required":[]},"twoFactor.enable":{"kind":null,"operationId":null,"optional":[],"required":["code"]},"update.getStatus":{"kind":null,"operationId":null,"optional":[],"required":[]},"update.update":{"kind":null,"operationId":null,"optional":[],"required":[]},"users.createUser":{"kind":null,"operationId":null,"optional":[],"required":["admin","email","password"]},"users.destroyUser":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"users.generateApiToken":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"users.listUsers":{"kind":null,"operationId":null,"optional":[],"required":[]},"users.revokeApiToken":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"users.updateUser":{"kind":null,"operationId":null,"optional":["password"],"required":["admin","id"]},"volumeBackups.createVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":[]},"volumeBackups.destroyVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"volumeBackups.listVolumeBackups":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"volumeBackups.listVolumeMounts":{"kind":null,"operationId":null,"optional":[],"required":["projectName","serviceName"]},"volumeBackups.runVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":["id"]},"volumeBackups.updateVolumeBackup":{"kind":null,"operationId":null,"optional":[],"required":[]}}} diff --git a/internal/easypanel/testdata/panel-surface-2.33.0.json b/internal/easypanel/testdata/panel-surface-2.33.0.json new file mode 100644 index 0000000..1c146f2 --- /dev/null +++ b/internal/easypanel/testdata/panel-surface-2.33.0.json @@ -0,0 +1 @@ +{"panelVersion":"2.33.0","procedures":{"actions.getAction":{"kind":"query","operationId":"getAction","optional":[],"required":["id"]},"actions.killAction":{"kind":"mutation","operationId":"killAction","optional":[],"required":["id"]},"actions.listActions":{"kind":"query","operationId":"listActions","optional":["limit","projectName","serviceName","type"],"required":[]},"auth.getSession":{"kind":"query","operationId":"getSession","optional":[],"required":[]},"auth.getUser":{"kind":"query","operationId":"getUser","optional":[],"required":[]},"auth.login":{"kind":"mutation","operationId":"login","optional":["code","rememberMe"],"required":["email","password"]},"auth.logout":{"kind":"mutation","operationId":"logout","optional":[],"required":[]},"branding.getBasicSettings":{"kind":"query","operationId":"getBasicSettings","optional":[],"required":[]},"branding.getCustomCodeSettings":{"kind":"query","operationId":"getCustomCodeSettings","optional":[],"required":[]},"branding.getErrorPageSettings":{"kind":"query","operationId":"getErrorPageSettings","optional":[],"required":[]},"branding.getInterfaceSettingsPublic":{"kind":"query","operationId":"getInterfaceSettingsPublic","optional":[],"required":[]},"branding.getLinksSettings":{"kind":"query","operationId":"getLinksSettings","optional":[],"required":[]},"branding.getLogoSettings":{"kind":"query","operationId":"getLogoSettings","optional":[],"required":[]},"branding.getOtherLinksSettings":{"kind":"query","operationId":"getOtherLinksSettings","optional":[],"required":[]},"branding.setBasicSettings":{"kind":"mutation","operationId":"setBasicSettings","optional":[],"required":["hideIp","hideNotes","serverColor","serverName"]},"branding.setCustomCodeSettings":{"kind":"mutation","operationId":"setCustomCodeSettings","optional":["customCode"],"required":[]},"branding.setErrorPageSettings":{"kind":"mutation","operationId":"setErrorPageSettings","optional":["customCss"],"required":["hideLinks","hideLogo"]},"branding.setLinksSettings":{"kind":"mutation","operationId":"setLinksSettings","optional":[],"required":["hideChangelogLink","hideDiscordLink","hideDocumentationLink","hideFeedbackLink","hideOtherLinks"]},"branding.setLogoSettings":{"kind":"mutation","operationId":"setLogoSettings","optional":["darkLogo","darkLogoMark","lightLogo","lightLogoMark"],"required":[]},"certificates.listCertificates":{"kind":"query","operationId":"listCertificates","optional":[],"required":[]},"certificates.removeCertificate":{"kind":"mutation","operationId":"removeCertificate","optional":[],"required":["domain"]},"cloudflareTunnel.createTunnelRule":{"kind":"mutation","operationId":"createTunnelRule","optional":[],"required":[]},"cloudflareTunnel.deleteTunnelRule":{"kind":"mutation","operationId":"deleteTunnelRule","optional":[],"required":["id"]},"cloudflareTunnel.getConfig":{"kind":"query","operationId":"getConfig","optional":[],"required":[]},"cloudflareTunnel.getTunnelRules":{"kind":"query","operationId":"getTunnelRules","optional":[],"required":["projectName","serviceName"]},"cloudflareTunnel.listAccounts":{"kind":"query","operationId":"listAccounts","optional":[],"required":["apiToken"]},"cloudflareTunnel.listTunnels":{"kind":"query","operationId":"listTunnels","optional":[],"required":["accountId","apiToken"]},"cloudflareTunnel.listZones":{"kind":"query","operationId":"listZones","optional":[],"required":[]},"cloudflareTunnel.setConfig":{"kind":"mutation","operationId":"setConfig","optional":["accountId","apiToken","tunnelId"],"required":[]},"cloudflareTunnel.startTunnel":{"kind":"mutation","operationId":"startTunnel","optional":[],"required":[]},"cloudflareTunnel.stopTunnel":{"kind":"mutation","operationId":"stopTunnel","optional":[],"required":[]},"cloudflareTunnel.updateTunnelRule":{"kind":"mutation","operationId":"updateTunnelRule","optional":[],"required":[]},"cluster.addWorkerCommand":{"kind":"query","operationId":"addWorkerCommand","optional":[],"required":[]},"cluster.listNodes":{"kind":"query","operationId":"listNodes","optional":[],"required":[]},"cluster.removeNode":{"kind":"mutation","operationId":"removeNode","optional":[],"required":["id"]},"databaseBackups.createDatabaseBackup":{"kind":"mutation","operationId":"createDatabaseBackup","optional":[],"required":[]},"databaseBackups.deleteDatabaseBackup":{"kind":"mutation","operationId":"deleteDatabaseBackup","optional":[],"required":["id"]},"databaseBackups.getServiceDatabases":{"kind":"query","operationId":"getServiceDatabases","optional":[],"required":["projectName","serviceName"]},"databaseBackups.listDatabaseBackups":{"kind":"query","operationId":"listDatabaseBackups","optional":[],"required":["projectName","serviceName"]},"databaseBackups.restoreDatabaseBackup":{"kind":"mutation","operationId":"restoreDatabaseBackup","optional":[],"required":["databaseName","path","projectName","serviceName","storageProviderId"]},"databaseBackups.runDatabaseBackup":{"kind":"mutation","operationId":"runDatabaseBackup","optional":["name"],"required":["id"]},"databaseBackups.updateDatabaseBackup":{"kind":"mutation","operationId":"updateDatabaseBackup","optional":[],"required":[]},"dockerBuilders.createDockerBuilder":{"kind":"mutation","operationId":"createDockerBuilder","optional":["cpus","memory","memorySwap"],"required":["name"]},"dockerBuilders.listDockerBuilders":{"kind":"query","operationId":"listDockerBuilders","optional":[],"required":[]},"dockerBuilders.removeDockerBuilder":{"kind":"mutation","operationId":"removeDockerBuilder","optional":[],"required":["name"]},"dockerBuilders.stopDockerBuilder":{"kind":"mutation","operationId":"stopDockerBuilder","optional":[],"required":["name"]},"dockerBuilders.useDockerBuilder":{"kind":"mutation","operationId":"useDockerBuilder","optional":[],"required":["name"]},"domains.createDomain":{"kind":"mutation","operationId":"createDomain","optional":[],"required":[]},"domains.deleteDomain":{"kind":"mutation","operationId":"deleteDomain","optional":[],"required":["id"]},"domains.getPrimaryDomain":{"kind":"query","operationId":"getPrimaryDomain","optional":[],"required":["projectName","serviceName"]},"domains.listDomains":{"kind":"query","operationId":"listDomains","optional":["projectName","serviceName"],"required":[]},"domains.setPrimaryDomain":{"kind":"mutation","operationId":"setPrimaryDomain","optional":[],"required":["id"]},"domains.updateDomain":{"kind":"mutation","operationId":"updateDomain","optional":[],"required":[]},"git.generateKey":{"kind":"mutation","operationId":"generateKey","optional":[],"required":["projectName","serviceName"]},"git.getPublicKey":{"kind":"query","operationId":"getPublicKey","optional":[],"required":["projectName","serviceName"]},"github.searchBranches":{"kind":"query","operationId":"searchBranches","optional":["search"],"required":["owner","repo"]},"github.searchRepos":{"kind":"query","operationId":"searchRepos","optional":[],"required":[]},"lemonLicense.activate":{"kind":"mutation","operationId":"activateLemonLicense","optional":[],"required":["licenseKey"]},"lemonLicense.activateByOrder":{"kind":"mutation","operationId":"activateByOrder","optional":[],"required":["identifier","orderId"]},"lemonLicense.deactivate":{"kind":"mutation","operationId":"deactivateLemonLicense","optional":[],"required":[]},"lemonLicense.getLicenseKey":{"kind":"query","operationId":"getLicenseKey","optional":[],"required":[]},"lemonLicense.getLicensePayload":{"kind":"query","operationId":"getLemonLicensePayload","optional":[],"required":[]},"logs.getSettings":{"kind":"query","operationId":"getLogsSettings","optional":[],"required":[]},"logs.getStats":{"kind":"query","operationId":"getLogsStats","optional":[],"required":[]},"logs.queryComposeServiceLogs":{"kind":"query","operationId":"queryComposeServiceLogs","optional":["composeInternalService","end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.queryServiceLogs":{"kind":"query","operationId":"queryServiceLogs","optional":["end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.updateSettings":{"kind":"mutation","operationId":"updateLogsSettings","optional":[],"required":["enabled","retentionType","retentionValue"]},"metrics.getAllServicesStats":{"kind":"query","operationId":"getAllServicesStats","optional":[],"required":[]},"metrics.getServiceStats":{"kind":"query","operationId":"getMetricsServiceStats","optional":["range","step"],"required":["projectName","serviceName"]},"metrics.getSettings":{"kind":"query","operationId":"getMetricsSettings","optional":[],"required":[]},"metrics.getStats":{"kind":"query","operationId":"getMetricsStats","optional":[],"required":[]},"metrics.getSystemStats":{"kind":"query","operationId":"getMetricsSystemStats","optional":["range","step"],"required":[]},"metrics.updateSettings":{"kind":"mutation","operationId":"updateMetricsSettings","optional":[],"required":["enabled","retentionType","retentionValue","scrapeInterval"]},"middlewares.createMiddleware":{"kind":"mutation","operationId":"createMiddleware","optional":[],"required":[]},"middlewares.destroyMiddleware":{"kind":"mutation","operationId":"destroyMiddleware","optional":[],"required":["id"]},"middlewares.listMiddlewares":{"kind":"query","operationId":"listMiddlewares","optional":[],"required":[]},"middlewares.updateMiddleware":{"kind":"mutation","operationId":"updateMiddleware","optional":[],"required":[]},"monitorOld.getAdvancedStats":{"kind":"query","operationId":"getAdvancedStats","optional":[],"required":[]},"monitorOld.getDockerTaskStats":{"kind":"query","operationId":"getDockerTaskStats","optional":[],"required":[]},"monitorOld.getMonitorTableData":{"kind":"query","operationId":"getMonitorTableData","optional":[],"required":[]},"monitorOld.getServiceStats":{"kind":"query","operationId":"getLegacyMonitorServiceStats","optional":[],"required":["projectName","serviceName"]},"monitorOld.getStorageStats":{"kind":"query","operationId":"getStorageStats","optional":[],"required":[]},"monitorOld.getSystemStats":{"kind":"query","operationId":"getLegacyMonitorSystemStats","optional":[],"required":[]},"mounts.createMount":{"kind":"mutation","operationId":"createMount","optional":[],"required":[]},"mounts.deleteMount":{"kind":"mutation","operationId":"deleteMount","optional":[],"required":[]},"mounts.listMounts":{"kind":"query","operationId":"listMounts","optional":[],"required":["projectName","serviceName"]},"mounts.updateMount":{"kind":"mutation","operationId":"updateMount","optional":[],"required":[]},"notifications.createNotificationChannel":{"kind":"mutation","operationId":"createNotificationChannel","optional":[],"required":[]},"notifications.destroyNotificationChannel":{"kind":"mutation","operationId":"destroyNotificationChannel","optional":[],"required":[]},"notifications.listNotificationChannels":{"kind":"query","operationId":"listNotificationChannels","optional":[],"required":[]},"notifications.sendTestNotification":{"kind":"mutation","operationId":"sendTestNotification","optional":[],"required":[]},"notifications.updateNotificationChannel":{"kind":"mutation","operationId":"updateNotificationChannel","optional":[],"required":[]},"portalLicense.activate":{"kind":"mutation","operationId":"activatePortalLicense","optional":[],"required":[]},"portalLicense.deactivate":{"kind":"mutation","operationId":"deactivatePortalLicense","optional":[],"required":[]},"portalLicense.getLicensePayload":{"kind":"query","operationId":"getPortalLicensePayload","optional":[],"required":[]},"ports.createPort":{"kind":"mutation","operationId":"createPort","optional":[],"required":[]},"ports.deleteAllPorts":{"kind":"mutation","operationId":"deleteAllPorts","optional":[],"required":["projectName","serviceName"]},"ports.deletePort":{"kind":"mutation","operationId":"deletePort","optional":[],"required":[]},"ports.listPorts":{"kind":"query","operationId":"listPorts","optional":[],"required":["projectName","serviceName"]},"ports.updatePort":{"kind":"mutation","operationId":"updatePort","optional":[],"required":[]},"projects.canCreateProject":{"kind":"query","operationId":"canCreateProject","optional":[],"required":[]},"projects.createProject":{"kind":"mutation","operationId":"createProject","optional":[],"required":["name"]},"projects.destroyProject":{"kind":"mutation","operationId":"destroyProject","optional":[],"required":["name"]},"projects.getDockerContainers":{"kind":"query","operationId":"getDockerContainers","optional":[],"required":["service"]},"projects.inspectProject":{"kind":"query","operationId":"inspectProject","optional":[],"required":["projectName"]},"projects.listProjects":{"kind":"query","operationId":"listProjects","optional":[],"required":[]},"projects.listProjectsAndServices":{"kind":"query","operationId":"listProjectsAndServices","optional":[],"required":[]},"projects.updateAccess":{"kind":"mutation","operationId":"updateAccess","optional":[],"required":["active","projectName","userId"]},"projects.updateProjectEnv":{"kind":"mutation","operationId":"updateProjectEnv","optional":["env"],"required":["projectName"]},"server.reboot":{"kind":"mutation","operationId":"reboot","optional":[],"required":[]},"services.app.createService":{"kind":"mutation","operationId":"createAppService","optional":[],"required":[]},"services.app.deployService":{"kind":"mutation","operationId":"deployAppService","optional":[],"required":[]},"services.app.destroyService":{"kind":"mutation","operationId":"destroyAppService","optional":[],"required":["projectName","serviceName"]},"services.app.disableGithubDeploy":{"kind":"mutation","operationId":"disableAppGithubDeploy","optional":[],"required":["projectName","serviceName"]},"services.app.enableGithubDeploy":{"kind":"mutation","operationId":"enableAppGithubDeploy","optional":[],"required":["projectName","serviceName"]},"services.app.getExposedPorts":{"kind":"query","operationId":"getAppExposedPorts","optional":["projectName","serviceName"],"required":[]},"services.app.inspectService":{"kind":"query","operationId":"inspectAppService","optional":[],"required":["projectName","serviceName"]},"services.app.refreshDeployToken":{"kind":"mutation","operationId":"refreshAppDeployToken","optional":[],"required":["projectName","serviceName"]},"services.app.restartService":{"kind":"mutation","operationId":"restartAppService","optional":[],"required":["projectName","serviceName"]},"services.app.startService":{"kind":"mutation","operationId":"startAppService","optional":[],"required":["projectName","serviceName"]},"services.app.stopService":{"kind":"mutation","operationId":"stopAppService","optional":[],"required":["projectName","serviceName"]},"services.app.updateBasicAuth":{"kind":"mutation","operationId":"updateAppBasicAuth","optional":[],"required":[]},"services.app.updateBuild":{"kind":"mutation","operationId":"updateAppBuild","optional":[],"required":[]},"services.app.updateDeploy":{"kind":"mutation","operationId":"updateAppDeploy","optional":[],"required":[]},"services.app.updateEnv":{"kind":"mutation","operationId":"updateAppEnv","optional":[],"required":[]},"services.app.updateMaintenance":{"kind":"mutation","operationId":"updateAppMaintenance","optional":[],"required":[]},"services.app.updateRedirects":{"kind":"mutation","operationId":"updateAppRedirects","optional":[],"required":[]},"services.app.updateResources":{"kind":"mutation","operationId":"updateAppResources","optional":[],"required":[]},"services.app.updateScripts":{"kind":"mutation","operationId":"updateAppScripts","optional":[],"required":[]},"services.app.updateSourceDockerfile":{"kind":"mutation","operationId":"updateAppSourceDockerfile","optional":[],"required":[]},"services.app.updateSourceGit":{"kind":"mutation","operationId":"updateAppSourceGit","optional":[],"required":[]},"services.app.updateSourceGithub":{"kind":"mutation","operationId":"updateAppSourceGithub","optional":[],"required":[]},"services.app.updateSourceImage":{"kind":"mutation","operationId":"updateAppSourceImage","optional":[],"required":[]},"services.app.uploadCodeArchive":{"kind":"mutation","operationId":"uploadAppCodeArchive","optional":[],"required":[]},"services.box.createService":{"kind":"mutation","operationId":"createBoxService","optional":[],"required":[]},"services.box.destroyService":{"kind":"mutation","operationId":"destroyBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.gitClone":{"kind":"mutation","operationId":"gitBoxClone","optional":[],"required":[]},"services.box.initService":{"kind":"mutation","operationId":"initBoxService","optional":[],"required":[]},"services.box.inspectService":{"kind":"query","operationId":"inspectBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.listPresets":{"kind":"query","operationId":"listBoxPresets","optional":[],"required":[]},"services.box.loadPreset":{"kind":"mutation","operationId":"loadBoxPreset","optional":[],"required":[]},"services.box.rebuildDockerImage":{"kind":"mutation","operationId":"rebuildBoxDockerImage","optional":[],"required":["projectName","serviceName"]},"services.box.refreshDeployToken":{"kind":"mutation","operationId":"refreshBoxDeployToken","optional":[],"required":["projectName","serviceName"]},"services.box.restartService":{"kind":"mutation","operationId":"restartBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.runDeployScript":{"kind":"mutation","operationId":"runBoxDeployScript","optional":[],"required":["projectName","serviceName"]},"services.box.runScript":{"kind":"mutation","operationId":"runBoxScript","optional":[],"required":[]},"services.box.startService":{"kind":"mutation","operationId":"startBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.stopService":{"kind":"mutation","operationId":"stopBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.updateAdvanced":{"kind":"mutation","operationId":"updateBoxAdvanced","optional":[],"required":[]},"services.box.updateBasicAuth":{"kind":"mutation","operationId":"updateBoxBasicAuth","optional":[],"required":[]},"services.box.updateDeployScript":{"kind":"mutation","operationId":"updateBoxDeployScript","optional":[],"required":[]},"services.box.updateEnv":{"kind":"mutation","operationId":"updateBoxEnv","optional":[],"required":[]},"services.box.updateGitConfig":{"kind":"mutation","operationId":"updateBoxGitConfig","optional":[],"required":[]},"services.box.updateIde":{"kind":"mutation","operationId":"updateBoxIde","optional":[],"required":[]},"services.box.updateModules":{"kind":"mutation","operationId":"updateBoxModules","optional":[],"required":[]},"services.box.updateNginx":{"kind":"mutation","operationId":"updateBoxNginx","optional":[],"required":[]},"services.box.updateNodejs":{"kind":"mutation","operationId":"updateBoxNodejs","optional":[],"required":[]},"services.box.updatePhp":{"kind":"mutation","operationId":"updateBoxPhp","optional":[],"required":[]},"services.box.updateProcesses":{"kind":"mutation","operationId":"updateBoxProcesses","optional":[],"required":[]},"services.box.updatePython":{"kind":"mutation","operationId":"updateBoxPython","optional":[],"required":[]},"services.box.updateRedirects":{"kind":"mutation","operationId":"updateBoxRedirects","optional":[],"required":[]},"services.box.updateResources":{"kind":"mutation","operationId":"updateBoxResources","optional":[],"required":[]},"services.box.updateRuby":{"kind":"mutation","operationId":"updateBoxRuby","optional":[],"required":[]},"services.box.updateScripts":{"kind":"mutation","operationId":"updateBoxScripts","optional":[],"required":[]},"services.common.getNotes":{"kind":"query","operationId":"getServiceNotes","optional":[],"required":["projectName","serviceName"]},"services.common.getServiceError":{"kind":"query","operationId":"getServiceError","optional":[],"required":["projectName","serviceName"]},"services.common.rename":{"kind":"mutation","operationId":"renameService","optional":[],"required":["newProjectName","newServiceName","oldProjectName","oldServiceName"]},"services.common.setNotes":{"kind":"mutation","operationId":"setServiceNotes","optional":[],"required":[]},"services.compose.createService":{"kind":"mutation","operationId":"createComposeService","optional":[],"required":[]},"services.compose.deployService":{"kind":"mutation","operationId":"deployComposeService","optional":[],"required":[]},"services.compose.destroyService":{"kind":"mutation","operationId":"destroyComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.getDockerServices":{"kind":"query","operationId":"getComposeDockerServices","optional":["projectName","serviceName"],"required":[]},"services.compose.getIssues":{"kind":"query","operationId":"getComposeIssues","optional":[],"required":["projectName","serviceName"]},"services.compose.inspectService":{"kind":"query","operationId":"inspectComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.refreshDeployToken":{"kind":"mutation","operationId":"refreshComposeDeployToken","optional":[],"required":["projectName","serviceName"]},"services.compose.restartService":{"kind":"mutation","operationId":"restartComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.startService":{"kind":"mutation","operationId":"startComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.stopService":{"kind":"mutation","operationId":"stopComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.updateBasicAuth":{"kind":"mutation","operationId":"updateComposeBasicAuth","optional":[],"required":[]},"services.compose.updateEnv":{"kind":"mutation","operationId":"updateComposeEnv","optional":[],"required":[]},"services.compose.updateMaintenance":{"kind":"mutation","operationId":"updateComposeMaintenance","optional":[],"required":[]},"services.compose.updateRedirects":{"kind":"mutation","operationId":"updateComposeRedirects","optional":[],"required":[]},"services.compose.updateSourceGit":{"kind":"mutation","operationId":"updateComposeSourceGit","optional":[],"required":[]},"services.compose.updateSourceInline":{"kind":"mutation","operationId":"updateComposeSourceInline","optional":[],"required":[]},"services.mariadb.createService":{"kind":"mutation","operationId":"createMariaDBService","optional":[],"required":[]},"services.mariadb.destroyService":{"kind":"mutation","operationId":"destroyMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableDbGate":{"kind":"mutation","operationId":"disableMariaDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mariadb.disablePhpMyAdmin":{"kind":"mutation","operationId":"disableMariaDBPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableService":{"kind":"mutation","operationId":"disableMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableDbGate":{"kind":"mutation","operationId":"enableMariaDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mariadb.enablePhpMyAdmin":{"kind":"mutation","operationId":"enableMariaDBPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableService":{"kind":"mutation","operationId":"enableMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.exposeService":{"kind":"mutation","operationId":"exposeMariaDBService","optional":[],"required":[]},"services.mariadb.inspectService":{"kind":"query","operationId":"inspectMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.updateAdvanced":{"kind":"mutation","operationId":"updateMariaDBAdvanced","optional":[],"required":[]},"services.mariadb.updateCredentials":{"kind":"mutation","operationId":"updateMariaDBCredentials","optional":[],"required":[]},"services.mariadb.updateResources":{"kind":"mutation","operationId":"updateMariaDBResources","optional":[],"required":[]},"services.mongo.createService":{"kind":"mutation","operationId":"createMongoDBService","optional":[],"required":[]},"services.mongo.destroyService":{"kind":"mutation","operationId":"destroyMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.disableDbGate":{"kind":"mutation","operationId":"disableMongoDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mongo.disableMongoExpress":{"kind":"mutation","operationId":"disableMongoDBMongoExpress","optional":[],"required":["projectName","serviceName"]},"services.mongo.disableService":{"kind":"mutation","operationId":"disableMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.enableDbGate":{"kind":"mutation","operationId":"enableMongoDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mongo.enableMongoExpress":{"kind":"mutation","operationId":"enableMongoDBMongoExpress","optional":[],"required":["projectName","serviceName"]},"services.mongo.enableService":{"kind":"mutation","operationId":"enableMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.exposeService":{"kind":"mutation","operationId":"exposeMongoDBService","optional":[],"required":[]},"services.mongo.inspectService":{"kind":"query","operationId":"inspectMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.updateAdvanced":{"kind":"mutation","operationId":"updateMongoDBAdvanced","optional":[],"required":[]},"services.mongo.updateCredentials":{"kind":"mutation","operationId":"updateMongoDBCredentials","optional":[],"required":[]},"services.mongo.updateResources":{"kind":"mutation","operationId":"updateMongoDBResources","optional":[],"required":[]},"services.mysql.createService":{"kind":"mutation","operationId":"createMySQLService","optional":[],"required":[]},"services.mysql.destroyService":{"kind":"mutation","operationId":"destroyMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.disableDbGate":{"kind":"mutation","operationId":"disableMySQLDbGate","optional":[],"required":["projectName","serviceName"]},"services.mysql.disablePhpMyAdmin":{"kind":"mutation","operationId":"disableMySQLPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mysql.disableService":{"kind":"mutation","operationId":"disableMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.enableDbGate":{"kind":"mutation","operationId":"enableMySQLDbGate","optional":[],"required":["projectName","serviceName"]},"services.mysql.enablePhpMyAdmin":{"kind":"mutation","operationId":"enableMySQLPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mysql.enableService":{"kind":"mutation","operationId":"enableMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.exposeService":{"kind":"mutation","operationId":"exposeMySQLService","optional":[],"required":[]},"services.mysql.inspectService":{"kind":"query","operationId":"inspectMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.updateAdvanced":{"kind":"mutation","operationId":"updateMySQLAdvanced","optional":[],"required":[]},"services.mysql.updateCredentials":{"kind":"mutation","operationId":"updateMySQLCredentials","optional":[],"required":[]},"services.mysql.updateResources":{"kind":"mutation","operationId":"updateMySQLResources","optional":[],"required":[]},"services.postgres.createService":{"kind":"mutation","operationId":"createPostgresService","optional":[],"required":[]},"services.postgres.destroyService":{"kind":"mutation","operationId":"destroyPostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.disableDbGate":{"kind":"mutation","operationId":"disablePostgresDbGate","optional":[],"required":["projectName","serviceName"]},"services.postgres.disablePgWeb":{"kind":"mutation","operationId":"disablePostgresPgWeb","optional":[],"required":["projectName","serviceName"]},"services.postgres.disableService":{"kind":"mutation","operationId":"disablePostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.enableDbGate":{"kind":"mutation","operationId":"enablePostgresDbGate","optional":[],"required":["projectName","serviceName"]},"services.postgres.enablePgWeb":{"kind":"mutation","operationId":"enablePostgresPgWeb","optional":[],"required":["projectName","serviceName"]},"services.postgres.enableService":{"kind":"mutation","operationId":"enablePostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.exposeService":{"kind":"mutation","operationId":"exposePostgresService","optional":[],"required":[]},"services.postgres.inspectService":{"kind":"query","operationId":"inspectPostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.updateAdvanced":{"kind":"mutation","operationId":"updatePostgresAdvanced","optional":[],"required":[]},"services.postgres.updateCredentials":{"kind":"mutation","operationId":"updatePostgresCredentials","optional":[],"required":[]},"services.postgres.updateResources":{"kind":"mutation","operationId":"updatePostgresResources","optional":[],"required":[]},"services.redis.createService":{"kind":"mutation","operationId":"createRedisService","optional":[],"required":[]},"services.redis.destroyService":{"kind":"mutation","operationId":"destroyRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.disableDbGate":{"kind":"mutation","operationId":"disableRedisDbGate","optional":[],"required":["projectName","serviceName"]},"services.redis.disableRedisCommander":{"kind":"mutation","operationId":"disableRedisCommander","optional":[],"required":["projectName","serviceName"]},"services.redis.disableService":{"kind":"mutation","operationId":"disableRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.enableDbGate":{"kind":"mutation","operationId":"enableRedisDbGate","optional":[],"required":["projectName","serviceName"]},"services.redis.enableRedisCommander":{"kind":"mutation","operationId":"enableRedisCommander","optional":[],"required":["projectName","serviceName"]},"services.redis.enableService":{"kind":"mutation","operationId":"enableRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.exposeService":{"kind":"mutation","operationId":"exposeRedisService","optional":[],"required":[]},"services.redis.inspectService":{"kind":"query","operationId":"inspectRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.updateAdvanced":{"kind":"mutation","operationId":"updateRedisAdvanced","optional":[],"required":[]},"services.redis.updateCredentials":{"kind":"mutation","operationId":"updateRedisCredentials","optional":[],"required":[]},"services.redis.updateResources":{"kind":"mutation","operationId":"updateRedisResources","optional":[],"required":[]},"services.wordpress.activatePlugin":{"kind":"mutation","operationId":"activateWordPressPlugin","optional":[],"required":[]},"services.wordpress.activateTheme":{"kind":"mutation","operationId":"activateWordPressTheme","optional":[],"required":[]},"services.wordpress.createOption":{"kind":"mutation","operationId":"createWordPressOption","optional":[],"required":["name","value"]},"services.wordpress.createRole":{"kind":"mutation","operationId":"createWordPressRole","optional":[],"required":["display_name","name"]},"services.wordpress.createService":{"kind":"mutation","operationId":"createWordPressService","optional":[],"required":[]},"services.wordpress.createUser":{"kind":"mutation","operationId":"createWordPressUser","optional":[],"required":["display_name","password","roles","user_email"]},"services.wordpress.dbOptimize":{"kind":"mutation","operationId":"dbWordPressOptimize","optional":[],"required":["projectName","serviceName"]},"services.wordpress.deactivatePlugin":{"kind":"mutation","operationId":"deactivateWordPressPlugin","optional":[],"required":[]},"services.wordpress.deleteOption":{"kind":"mutation","operationId":"deleteWordPressOption","optional":[],"required":[]},"services.wordpress.deleteRole":{"kind":"mutation","operationId":"deleteWordPressRole","optional":[],"required":[]},"services.wordpress.deleteTransient":{"kind":"mutation","operationId":"deleteWordPressTransient","optional":[],"required":["projectName","serviceName"]},"services.wordpress.deleteUser":{"kind":"mutation","operationId":"deleteWordPressUser","optional":[],"required":[]},"services.wordpress.destroyService":{"kind":"mutation","operationId":"destroyWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.flushCache":{"kind":"mutation","operationId":"flushWordPressCache","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getDatabaseServices":{"kind":"query","operationId":"getWordPressDatabaseServices","optional":[],"required":["projectName"]},"services.wordpress.getMaintenanceMode":{"kind":"query","operationId":"getWordPressMaintenanceMode","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getOptions":{"kind":"query","operationId":"getWordPressOptions","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getPlugins":{"kind":"query","operationId":"getWordPressPlugins","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getProfile":{"kind":"query","operationId":"getWordPressProfile","optional":[],"required":[]},"services.wordpress.getRoles":{"kind":"query","operationId":"getWordPressRoles","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getThemes":{"kind":"query","operationId":"getWordPressThemes","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getUsers":{"kind":"query","operationId":"getWordPressUsers","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getWpConfig":{"kind":"query","operationId":"getWordPressWpConfig","optional":[],"required":["projectName","serviceName"]},"services.wordpress.gitClone":{"kind":"mutation","operationId":"gitWordPressClone","optional":[],"required":[]},"services.wordpress.initService":{"kind":"mutation","operationId":"initWordPressService","optional":[],"required":[]},"services.wordpress.inspectService":{"kind":"query","operationId":"inspectWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.installPlugin":{"kind":"mutation","operationId":"installWordPressPlugin","optional":[],"required":[]},"services.wordpress.installTheme":{"kind":"mutation","operationId":"installWordPressTheme","optional":[],"required":[]},"services.wordpress.mediaRegenerate":{"kind":"mutation","operationId":"mediaWordPressRegenerate","optional":[],"required":["projectName","serviceName"]},"services.wordpress.rebuildDockerImage":{"kind":"mutation","operationId":"rebuildWordPressDockerImage","optional":[],"required":["projectName","serviceName"]},"services.wordpress.restartService":{"kind":"mutation","operationId":"restartWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.runScript":{"kind":"mutation","operationId":"runWordPressScript","optional":[],"required":[]},"services.wordpress.searchPlugin":{"kind":"query","operationId":"searchWordPressPlugin","optional":[],"required":[]},"services.wordpress.searchReplace":{"kind":"mutation","operationId":"searchWordPressReplace","optional":[],"required":[]},"services.wordpress.searchReplaceDryRun":{"kind":"query","operationId":"searchWordPressReplaceDryRun","optional":[],"required":[]},"services.wordpress.searchTheme":{"kind":"query","operationId":"searchWordPressTheme","optional":[],"required":[]},"services.wordpress.startService":{"kind":"mutation","operationId":"startWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.stopService":{"kind":"mutation","operationId":"stopWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.updateBasicAuth":{"kind":"mutation","operationId":"updateWordPressBasicAuth","optional":[],"required":[]},"services.wordpress.updateEnv":{"kind":"mutation","operationId":"updateWordPressEnv","optional":[],"required":[]},"services.wordpress.updateGitConfig":{"kind":"mutation","operationId":"updateWordPressGitConfig","optional":[],"required":[]},"services.wordpress.updateIde":{"kind":"mutation","operationId":"updateWordPressIde","optional":[],"required":[]},"services.wordpress.updateMaintenanceMode":{"kind":"mutation","operationId":"updateWordPressMaintenanceMode","optional":[],"required":[]},"services.wordpress.updateNginx":{"kind":"mutation","operationId":"updateWordPressNginx","optional":[],"required":[]},"services.wordpress.updateOption":{"kind":"mutation","operationId":"updateWordPressOption","optional":[],"required":["name","value"]},"services.wordpress.updatePhp":{"kind":"mutation","operationId":"updateWordPressPhp","optional":[],"required":[]},"services.wordpress.updateRedirects":{"kind":"mutation","operationId":"updateWordPressRedirects","optional":[],"required":[]},"services.wordpress.updateResources":{"kind":"mutation","operationId":"updateWordPressResources","optional":[],"required":[]},"services.wordpress.updateScripts":{"kind":"mutation","operationId":"updateWordPressScripts","optional":[],"required":[]},"services.wordpress.updateUser":{"kind":"mutation","operationId":"updateWordPressUser","optional":["password"],"required":["ID","display_name","roles","user_email"]},"services.wordpress.updateWpConfig":{"kind":"mutation","operationId":"updateWordPressWpConfig","optional":[],"required":[]},"services.wordpress.updateWpCore":{"kind":"mutation","operationId":"updateWordPressWpCore","optional":[],"required":["projectName","serviceName"]},"settings.changeCredentials":{"kind":"mutation","operationId":"changeCredentials","optional":[],"required":["email","newPassword","oldPassword"]},"settings.checkDockerUpdate":{"kind":"query","operationId":"checkDockerUpdate","optional":[],"required":[]},"settings.checkForUpdates":{"kind":"query","operationId":"checkForUpdates","optional":[],"required":[]},"settings.cleanupDockerBuilder":{"kind":"mutation","operationId":"cleanupDockerBuilder","optional":[],"required":[]},"settings.cleanupDockerImages":{"kind":"mutation","operationId":"cleanupDockerImages","optional":[],"required":[]},"settings.getDailyDockerCleanup":{"kind":"query","operationId":"getDailyDockerCleanup","optional":[],"required":[]},"settings.getDemoMode":{"kind":"query","operationId":"getDemoMode","optional":[],"required":[]},"settings.getDockerVersion":{"kind":"query","operationId":"getDockerVersion","optional":[],"required":[]},"settings.getGithubToken":{"kind":"query","operationId":"getGithubToken","optional":[],"required":[]},"settings.getGoogleAnalyticsMeasurementId":{"kind":"query","operationId":"getGoogleAnalyticsMeasurementId","optional":[],"required":[]},"settings.getLetsEncryptEmail":{"kind":"query","operationId":"getLetsEncryptEmail","optional":[],"required":[]},"settings.getPanelDomain":{"kind":"query","operationId":"getPanelDomain","optional":[],"required":[]},"settings.getServerIp":{"kind":"query","operationId":"getServerIp","optional":[],"required":[]},"settings.getServiceDomain":{"kind":"query","operationId":"getServiceDomain","optional":[],"required":[]},"settings.getTelemetryDisabled":{"kind":"query","operationId":"getTelemetryDisabled","optional":[],"required":[]},"settings.refreshServerIp":{"kind":"mutation","operationId":"refreshServerIp","optional":[],"required":[]},"settings.restartEasypanel":{"kind":"mutation","operationId":"restartEasypanel","optional":[],"required":[]},"settings.setDailyDockerCleanup":{"kind":"mutation","operationId":"setDailyDockerCleanup","optional":[],"required":["dailyDockerCleanup"]},"settings.setGithubToken":{"kind":"mutation","operationId":"setGithubToken","optional":["githubToken"],"required":[]},"settings.setGoogleAnalyticsMeasurementId":{"kind":"mutation","operationId":"setGoogleAnalyticsMeasurementId","optional":["measurementId"],"required":[]},"settings.setLetsEncryptEmail":{"kind":"mutation","operationId":"setLetsEncryptEmail","optional":[],"required":["letsEncryptEmail"]},"settings.setPanelDomain":{"kind":"mutation","operationId":"setPanelDomain","optional":[],"required":["customPanelDomain","serveOnIp"]},"settings.setServiceDomain":{"kind":"mutation","operationId":"setServiceDomain","optional":[],"required":["customServiceDomain"]},"settings.setTelemetryDisabled":{"kind":"mutation","operationId":"setTelemetryDisabled","optional":[],"required":["disabled"]},"settings.systemPrune":{"kind":"mutation","operationId":"systemPrune","optional":[],"required":[]},"setup.getStatus":{"kind":"query","operationId":"getSetupStatus","optional":[],"required":[]},"setup.setup":{"kind":"mutation","operationId":"setup","optional":[],"required":["email","password","source","subscribe","terms"]},"storageProviders.common.list":{"kind":"query","operationId":"listStorageProviders","optional":[],"required":[]},"storageProviders.common.listOptions":{"kind":"query","operationId":"listStorageProviderOptions","optional":[],"required":[]},"storageProviders.dropbox.createProvider":{"kind":"mutation","operationId":"createDropboxProvider","optional":[],"required":["name"]},"storageProviders.dropbox.deleteProvider":{"kind":"mutation","operationId":"deleteDropboxProvider","optional":[],"required":["id"]},"storageProviders.dropbox.disconnectProvider":{"kind":"mutation","operationId":"disconnectDropboxProvider","optional":[],"required":["id"]},"storageProviders.dropbox.updateProvider":{"kind":"mutation","operationId":"updateDropboxProvider","optional":["name"],"required":["id"]},"storageProviders.ftp.createProvider":{"kind":"mutation","operationId":"createFTProvider","optional":[],"required":["host","name","password","port","username"]},"storageProviders.ftp.deleteProvider":{"kind":"mutation","operationId":"deleteFTProvider","optional":[],"required":["id"]},"storageProviders.ftp.updateProvider":{"kind":"mutation","operationId":"updateFTProvider","optional":["port"],"required":["host","id","name","password","username"]},"storageProviders.google.createProvider":{"kind":"mutation","operationId":"createGoogleProvider","optional":[],"required":["name"]},"storageProviders.google.deleteProvider":{"kind":"mutation","operationId":"deleteGoogleProvider","optional":[],"required":["id"]},"storageProviders.google.disconnectProvider":{"kind":"mutation","operationId":"disconnectGoogleProvider","optional":[],"required":["id"]},"storageProviders.google.updateProvider":{"kind":"mutation","operationId":"updateGoogleProvider","optional":["name"],"required":["id"]},"storageProviders.local.createProvider":{"kind":"mutation","operationId":"createLocalProvider","optional":[],"required":["name","path"]},"storageProviders.local.deleteProvider":{"kind":"mutation","operationId":"deleteLocalProvider","optional":[],"required":["id"]},"storageProviders.local.updateProvider":{"kind":"mutation","operationId":"updateLocalProvider","optional":[],"required":["id","name","path"]},"storageProviders.s3.createProvider":{"kind":"mutation","operationId":"createS3Provider","optional":["endpoint"],"required":["accessKeyId","bucket","name","region","secretAccessKey","storageClass","subtype"]},"storageProviders.s3.deleteProvider":{"kind":"mutation","operationId":"deleteS3Provider","optional":[],"required":["id"]},"storageProviders.s3.updateProvider":{"kind":"mutation","operationId":"updateS3Provider","optional":["endpoint"],"required":["accessKeyId","bucket","id","name","region","secretAccessKey","storageClass"]},"storageProviders.sftp.createProvider":{"kind":"mutation","operationId":"createSFTProvider","optional":["port"],"required":["host","name","password","username"]},"storageProviders.sftp.deleteProvider":{"kind":"mutation","operationId":"deleteSFTProvider","optional":[],"required":["id"]},"storageProviders.sftp.updateProvider":{"kind":"mutation","operationId":"updateSFTProvider","optional":["port"],"required":["host","id","name","password","username"]},"subscription.onInvalidateActions":{"kind":"query","operationId":"onInvalidateActions","optional":[],"required":[]},"templates.createFromSchema":{"kind":"mutation","operationId":"createFromSchema","optional":["name"],"required":["projectName","schema"]},"traefik.getCustomConfig":{"kind":"query","operationId":"getCustomConfig","optional":[],"required":[]},"traefik.getDashboard":{"kind":"mutation","operationId":"getDashboard","optional":[],"required":[]},"traefik.getEnv":{"kind":"query","operationId":"getEnv","optional":[],"required":[]},"traefik.restart":{"kind":"mutation","operationId":"restart","optional":[],"required":[]},"traefik.setCustomConfig":{"kind":"mutation","operationId":"setCustomConfig","optional":["config"],"required":[]},"traefik.setEnv":{"kind":"mutation","operationId":"setEnv","optional":["env"],"required":[]},"twoFactor.configure":{"kind":"mutation","operationId":"configure","optional":[],"required":[]},"twoFactor.disable":{"kind":"mutation","operationId":"disable","optional":[],"required":[]},"twoFactor.enable":{"kind":"mutation","operationId":"enable","optional":[],"required":["code"]},"update.getStatus":{"kind":"query","operationId":"getUpdateStatus","optional":[],"required":[]},"update.update":{"kind":"mutation","operationId":"update","optional":[],"required":[]},"users.createUser":{"kind":"mutation","operationId":"createUser","optional":[],"required":["admin","email","password"]},"users.destroyUser":{"kind":"mutation","operationId":"destroyUser","optional":[],"required":["id"]},"users.generateApiToken":{"kind":"mutation","operationId":"generateApiToken","optional":[],"required":["id"]},"users.listUsers":{"kind":"query","operationId":"listUsers","optional":[],"required":[]},"users.revokeApiToken":{"kind":"mutation","operationId":"revokeApiToken","optional":[],"required":["id"]},"users.updateUser":{"kind":"mutation","operationId":"updateUser","optional":["password"],"required":["admin","id"]},"volumeBackups.createVolumeBackup":{"kind":"mutation","operationId":"createVolumeBackup","optional":[],"required":[]},"volumeBackups.destroyVolumeBackup":{"kind":"mutation","operationId":"destroyVolumeBackup","optional":[],"required":["id"]},"volumeBackups.listVolumeBackups":{"kind":"query","operationId":"listVolumeBackups","optional":[],"required":["projectName","serviceName"]},"volumeBackups.listVolumeMounts":{"kind":"query","operationId":"listVolumeMounts","optional":[],"required":["projectName","serviceName"]},"volumeBackups.runVolumeBackup":{"kind":"mutation","operationId":"runVolumeBackup","optional":[],"required":["id"]},"volumeBackups.updateVolumeBackup":{"kind":"mutation","operationId":"updateVolumeBackup","optional":[],"required":[]}}} diff --git a/internal/easypanel/testdata/panel-surface-2.33.1.json b/internal/easypanel/testdata/panel-surface-2.33.1.json new file mode 100644 index 0000000..30c10ff --- /dev/null +++ b/internal/easypanel/testdata/panel-surface-2.33.1.json @@ -0,0 +1 @@ +{"panelVersion":"2.33.1","procedures":{"actions.getAction":{"kind":"query","operationId":"getAction","optional":[],"required":["id"]},"actions.killAction":{"kind":"mutation","operationId":"killAction","optional":[],"required":["id"]},"actions.listActions":{"kind":"query","operationId":"listActions","optional":["limit","projectName","serviceName","type"],"required":[]},"auth.getSession":{"kind":"query","operationId":"getSession","optional":[],"required":[]},"auth.getUser":{"kind":"query","operationId":"getUser","optional":[],"required":[]},"auth.login":{"kind":"mutation","operationId":"login","optional":["code","rememberMe"],"required":["email","password"]},"auth.logout":{"kind":"mutation","operationId":"logout","optional":[],"required":[]},"branding.getBasicSettings":{"kind":"query","operationId":"getBasicSettings","optional":[],"required":[]},"branding.getCustomCodeSettings":{"kind":"query","operationId":"getCustomCodeSettings","optional":[],"required":[]},"branding.getErrorPageSettings":{"kind":"query","operationId":"getErrorPageSettings","optional":[],"required":[]},"branding.getInterfaceSettingsPublic":{"kind":"query","operationId":"getInterfaceSettingsPublic","optional":[],"required":[]},"branding.getLinksSettings":{"kind":"query","operationId":"getLinksSettings","optional":[],"required":[]},"branding.getLogoSettings":{"kind":"query","operationId":"getLogoSettings","optional":[],"required":[]},"branding.getOtherLinksSettings":{"kind":"query","operationId":"getOtherLinksSettings","optional":[],"required":[]},"branding.setBasicSettings":{"kind":"mutation","operationId":"setBasicSettings","optional":[],"required":["hideIp","hideNotes","serverColor","serverName"]},"branding.setCustomCodeSettings":{"kind":"mutation","operationId":"setCustomCodeSettings","optional":["customCode"],"required":[]},"branding.setErrorPageSettings":{"kind":"mutation","operationId":"setErrorPageSettings","optional":["customCss"],"required":["hideLinks","hideLogo"]},"branding.setLinksSettings":{"kind":"mutation","operationId":"setLinksSettings","optional":[],"required":["hideChangelogLink","hideDiscordLink","hideDocumentationLink","hideFeedbackLink","hideOtherLinks"]},"branding.setLogoSettings":{"kind":"mutation","operationId":"setLogoSettings","optional":["darkLogo","darkLogoMark","lightLogo","lightLogoMark"],"required":[]},"certificates.listCertificates":{"kind":"query","operationId":"listCertificates","optional":[],"required":[]},"certificates.removeCertificate":{"kind":"mutation","operationId":"removeCertificate","optional":[],"required":["domain"]},"cloudflareTunnel.createTunnelRule":{"kind":"mutation","operationId":"createTunnelRule","optional":[],"required":[]},"cloudflareTunnel.deleteTunnelRule":{"kind":"mutation","operationId":"deleteTunnelRule","optional":[],"required":["id"]},"cloudflareTunnel.getConfig":{"kind":"query","operationId":"getConfig","optional":[],"required":[]},"cloudflareTunnel.getTunnelRules":{"kind":"query","operationId":"getTunnelRules","optional":[],"required":["projectName","serviceName"]},"cloudflareTunnel.listAccounts":{"kind":"query","operationId":"listAccounts","optional":[],"required":["apiToken"]},"cloudflareTunnel.listTunnels":{"kind":"query","operationId":"listTunnels","optional":[],"required":["accountId","apiToken"]},"cloudflareTunnel.listZones":{"kind":"query","operationId":"listZones","optional":[],"required":[]},"cloudflareTunnel.setConfig":{"kind":"mutation","operationId":"setConfig","optional":["accountId","apiToken","tunnelId"],"required":[]},"cloudflareTunnel.startTunnel":{"kind":"mutation","operationId":"startTunnel","optional":[],"required":[]},"cloudflareTunnel.stopTunnel":{"kind":"mutation","operationId":"stopTunnel","optional":[],"required":[]},"cloudflareTunnel.updateTunnelRule":{"kind":"mutation","operationId":"updateTunnelRule","optional":[],"required":[]},"cluster.addWorkerCommand":{"kind":"query","operationId":"addWorkerCommand","optional":[],"required":[]},"cluster.listNodes":{"kind":"query","operationId":"listNodes","optional":[],"required":[]},"cluster.removeNode":{"kind":"mutation","operationId":"removeNode","optional":[],"required":["id"]},"databaseBackups.createDatabaseBackup":{"kind":"mutation","operationId":"createDatabaseBackup","optional":[],"required":[]},"databaseBackups.deleteDatabaseBackup":{"kind":"mutation","operationId":"deleteDatabaseBackup","optional":[],"required":["id"]},"databaseBackups.getServiceDatabases":{"kind":"query","operationId":"getServiceDatabases","optional":[],"required":["projectName","serviceName"]},"databaseBackups.listDatabaseBackups":{"kind":"query","operationId":"listDatabaseBackups","optional":[],"required":["projectName","serviceName"]},"databaseBackups.restoreDatabaseBackup":{"kind":"mutation","operationId":"restoreDatabaseBackup","optional":[],"required":["databaseName","path","projectName","serviceName","storageProviderId"]},"databaseBackups.runDatabaseBackup":{"kind":"mutation","operationId":"runDatabaseBackup","optional":["name"],"required":["id"]},"databaseBackups.updateDatabaseBackup":{"kind":"mutation","operationId":"updateDatabaseBackup","optional":[],"required":[]},"dockerBuilders.createDockerBuilder":{"kind":"mutation","operationId":"createDockerBuilder","optional":["cpus","memory","memorySwap"],"required":["name"]},"dockerBuilders.listDockerBuilders":{"kind":"query","operationId":"listDockerBuilders","optional":[],"required":[]},"dockerBuilders.removeDockerBuilder":{"kind":"mutation","operationId":"removeDockerBuilder","optional":[],"required":["name"]},"dockerBuilders.stopDockerBuilder":{"kind":"mutation","operationId":"stopDockerBuilder","optional":[],"required":["name"]},"dockerBuilders.useDockerBuilder":{"kind":"mutation","operationId":"useDockerBuilder","optional":[],"required":["name"]},"domains.createDomain":{"kind":"mutation","operationId":"createDomain","optional":[],"required":[]},"domains.deleteDomain":{"kind":"mutation","operationId":"deleteDomain","optional":[],"required":["id"]},"domains.getPrimaryDomain":{"kind":"query","operationId":"getPrimaryDomain","optional":[],"required":["projectName","serviceName"]},"domains.listDomains":{"kind":"query","operationId":"listDomains","optional":["projectName","serviceName"],"required":[]},"domains.setPrimaryDomain":{"kind":"mutation","operationId":"setPrimaryDomain","optional":[],"required":["id"]},"domains.updateDomain":{"kind":"mutation","operationId":"updateDomain","optional":[],"required":[]},"git.generateKey":{"kind":"mutation","operationId":"generateKey","optional":[],"required":["projectName","serviceName"]},"git.getPublicKey":{"kind":"query","operationId":"getPublicKey","optional":[],"required":["projectName","serviceName"]},"github.searchBranches":{"kind":"query","operationId":"searchBranches","optional":["search"],"required":["owner","repo"]},"github.searchRepos":{"kind":"query","operationId":"searchRepos","optional":[],"required":[]},"lemonLicense.activate":{"kind":"mutation","operationId":"activateLemonLicense","optional":[],"required":["licenseKey"]},"lemonLicense.activateByOrder":{"kind":"mutation","operationId":"activateByOrder","optional":[],"required":["identifier","orderId"]},"lemonLicense.deactivate":{"kind":"mutation","operationId":"deactivateLemonLicense","optional":[],"required":[]},"lemonLicense.getLicenseKey":{"kind":"query","operationId":"getLicenseKey","optional":[],"required":[]},"lemonLicense.getLicensePayload":{"kind":"query","operationId":"getLemonLicensePayload","optional":[],"required":[]},"logs.getSettings":{"kind":"query","operationId":"getLogsSettings","optional":[],"required":[]},"logs.getStats":{"kind":"query","operationId":"getLogsStats","optional":[],"required":[]},"logs.queryComposeServiceLogs":{"kind":"query","operationId":"queryComposeServiceLogs","optional":["composeInternalService","end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.queryServiceLogs":{"kind":"query","operationId":"queryServiceLogs","optional":["end","levels","limit","search","start","stream"],"required":["projectName","serviceName"]},"logs.updateSettings":{"kind":"mutation","operationId":"updateLogsSettings","optional":[],"required":["enabled","retentionType","retentionValue"]},"metrics.getAllServicesStats":{"kind":"query","operationId":"getAllServicesStats","optional":[],"required":[]},"metrics.getServiceStats":{"kind":"query","operationId":"getMetricsServiceStats","optional":["range","step"],"required":["projectName","serviceName"]},"metrics.getSettings":{"kind":"query","operationId":"getMetricsSettings","optional":[],"required":[]},"metrics.getStats":{"kind":"query","operationId":"getMetricsStats","optional":[],"required":[]},"metrics.getSystemStats":{"kind":"query","operationId":"getMetricsSystemStats","optional":["range","step"],"required":[]},"metrics.updateSettings":{"kind":"mutation","operationId":"updateMetricsSettings","optional":[],"required":["enabled","retentionType","retentionValue","scrapeInterval"]},"middlewares.createMiddleware":{"kind":"mutation","operationId":"createMiddleware","optional":[],"required":[]},"middlewares.destroyMiddleware":{"kind":"mutation","operationId":"destroyMiddleware","optional":[],"required":["id"]},"middlewares.listMiddlewares":{"kind":"query","operationId":"listMiddlewares","optional":[],"required":[]},"middlewares.updateMiddleware":{"kind":"mutation","operationId":"updateMiddleware","optional":[],"required":[]},"monitorOld.getAdvancedStats":{"kind":"query","operationId":"getAdvancedStats","optional":[],"required":[]},"monitorOld.getDockerTaskStats":{"kind":"query","operationId":"getDockerTaskStats","optional":[],"required":[]},"monitorOld.getMonitorTableData":{"kind":"query","operationId":"getMonitorTableData","optional":[],"required":[]},"monitorOld.getServiceStats":{"kind":"query","operationId":"getLegacyMonitorServiceStats","optional":[],"required":["projectName","serviceName"]},"monitorOld.getStorageStats":{"kind":"query","operationId":"getStorageStats","optional":[],"required":[]},"monitorOld.getSystemStats":{"kind":"query","operationId":"getLegacyMonitorSystemStats","optional":[],"required":[]},"mounts.createMount":{"kind":"mutation","operationId":"createMount","optional":[],"required":[]},"mounts.deleteMount":{"kind":"mutation","operationId":"deleteMount","optional":[],"required":[]},"mounts.listMounts":{"kind":"query","operationId":"listMounts","optional":[],"required":["projectName","serviceName"]},"mounts.updateMount":{"kind":"mutation","operationId":"updateMount","optional":[],"required":[]},"notifications.createNotificationChannel":{"kind":"mutation","operationId":"createNotificationChannel","optional":[],"required":[]},"notifications.destroyNotificationChannel":{"kind":"mutation","operationId":"destroyNotificationChannel","optional":[],"required":[]},"notifications.listNotificationChannels":{"kind":"query","operationId":"listNotificationChannels","optional":[],"required":[]},"notifications.sendTestNotification":{"kind":"mutation","operationId":"sendTestNotification","optional":[],"required":[]},"notifications.updateNotificationChannel":{"kind":"mutation","operationId":"updateNotificationChannel","optional":[],"required":[]},"portalLicense.activate":{"kind":"mutation","operationId":"activatePortalLicense","optional":[],"required":[]},"portalLicense.deactivate":{"kind":"mutation","operationId":"deactivatePortalLicense","optional":[],"required":[]},"portalLicense.getLicensePayload":{"kind":"query","operationId":"getPortalLicensePayload","optional":[],"required":[]},"ports.createPort":{"kind":"mutation","operationId":"createPort","optional":[],"required":[]},"ports.deleteAllPorts":{"kind":"mutation","operationId":"deleteAllPorts","optional":[],"required":["projectName","serviceName"]},"ports.deletePort":{"kind":"mutation","operationId":"deletePort","optional":[],"required":[]},"ports.listPorts":{"kind":"query","operationId":"listPorts","optional":[],"required":["projectName","serviceName"]},"ports.updatePort":{"kind":"mutation","operationId":"updatePort","optional":[],"required":[]},"projects.canCreateProject":{"kind":"query","operationId":"canCreateProject","optional":[],"required":[]},"projects.createProject":{"kind":"mutation","operationId":"createProject","optional":[],"required":["name"]},"projects.destroyProject":{"kind":"mutation","operationId":"destroyProject","optional":[],"required":["name"]},"projects.getDockerContainers":{"kind":"query","operationId":"getDockerContainers","optional":[],"required":["service"]},"projects.inspectProject":{"kind":"query","operationId":"inspectProject","optional":[],"required":["projectName"]},"projects.listProjects":{"kind":"query","operationId":"listProjects","optional":[],"required":[]},"projects.listProjectsAndServices":{"kind":"query","operationId":"listProjectsAndServices","optional":[],"required":[]},"projects.updateAccess":{"kind":"mutation","operationId":"updateAccess","optional":[],"required":["active","projectName","userId"]},"projects.updateProjectEnv":{"kind":"mutation","operationId":"updateProjectEnv","optional":["env"],"required":["projectName"]},"server.reboot":{"kind":"mutation","operationId":"reboot","optional":[],"required":[]},"services.app.createService":{"kind":"mutation","operationId":"createAppService","optional":[],"required":[]},"services.app.deployService":{"kind":"mutation","operationId":"deployAppService","optional":[],"required":[]},"services.app.destroyService":{"kind":"mutation","operationId":"destroyAppService","optional":[],"required":["projectName","serviceName"]},"services.app.disableGithubDeploy":{"kind":"mutation","operationId":"disableAppGithubDeploy","optional":[],"required":["projectName","serviceName"]},"services.app.enableGithubDeploy":{"kind":"mutation","operationId":"enableAppGithubDeploy","optional":[],"required":["projectName","serviceName"]},"services.app.getExposedPorts":{"kind":"query","operationId":"getAppExposedPorts","optional":["projectName","serviceName"],"required":[]},"services.app.inspectService":{"kind":"query","operationId":"inspectAppService","optional":[],"required":["projectName","serviceName"]},"services.app.refreshDeployToken":{"kind":"mutation","operationId":"refreshAppDeployToken","optional":[],"required":["projectName","serviceName"]},"services.app.restartService":{"kind":"mutation","operationId":"restartAppService","optional":[],"required":["projectName","serviceName"]},"services.app.startService":{"kind":"mutation","operationId":"startAppService","optional":[],"required":["projectName","serviceName"]},"services.app.stopService":{"kind":"mutation","operationId":"stopAppService","optional":[],"required":["projectName","serviceName"]},"services.app.updateBasicAuth":{"kind":"mutation","operationId":"updateAppBasicAuth","optional":[],"required":[]},"services.app.updateBuild":{"kind":"mutation","operationId":"updateAppBuild","optional":[],"required":[]},"services.app.updateDeploy":{"kind":"mutation","operationId":"updateAppDeploy","optional":[],"required":[]},"services.app.updateEnv":{"kind":"mutation","operationId":"updateAppEnv","optional":[],"required":[]},"services.app.updateMaintenance":{"kind":"mutation","operationId":"updateAppMaintenance","optional":[],"required":[]},"services.app.updateRedirects":{"kind":"mutation","operationId":"updateAppRedirects","optional":[],"required":[]},"services.app.updateResources":{"kind":"mutation","operationId":"updateAppResources","optional":[],"required":[]},"services.app.updateScripts":{"kind":"mutation","operationId":"updateAppScripts","optional":[],"required":[]},"services.app.updateSourceDockerfile":{"kind":"mutation","operationId":"updateAppSourceDockerfile","optional":[],"required":[]},"services.app.updateSourceGit":{"kind":"mutation","operationId":"updateAppSourceGit","optional":[],"required":[]},"services.app.updateSourceGithub":{"kind":"mutation","operationId":"updateAppSourceGithub","optional":[],"required":[]},"services.app.updateSourceImage":{"kind":"mutation","operationId":"updateAppSourceImage","optional":[],"required":[]},"services.app.uploadCodeArchive":{"kind":"mutation","operationId":"uploadAppCodeArchive","optional":[],"required":[]},"services.box.createService":{"kind":"mutation","operationId":"createBoxService","optional":[],"required":[]},"services.box.destroyService":{"kind":"mutation","operationId":"destroyBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.gitClone":{"kind":"mutation","operationId":"gitBoxClone","optional":[],"required":[]},"services.box.initService":{"kind":"mutation","operationId":"initBoxService","optional":[],"required":[]},"services.box.inspectService":{"kind":"query","operationId":"inspectBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.listPresets":{"kind":"query","operationId":"listBoxPresets","optional":[],"required":[]},"services.box.loadPreset":{"kind":"mutation","operationId":"loadBoxPreset","optional":[],"required":[]},"services.box.rebuildDockerImage":{"kind":"mutation","operationId":"rebuildBoxDockerImage","optional":[],"required":["projectName","serviceName"]},"services.box.refreshDeployToken":{"kind":"mutation","operationId":"refreshBoxDeployToken","optional":[],"required":["projectName","serviceName"]},"services.box.restartService":{"kind":"mutation","operationId":"restartBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.runDeployScript":{"kind":"mutation","operationId":"runBoxDeployScript","optional":[],"required":["projectName","serviceName"]},"services.box.runScript":{"kind":"mutation","operationId":"runBoxScript","optional":[],"required":[]},"services.box.startService":{"kind":"mutation","operationId":"startBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.stopService":{"kind":"mutation","operationId":"stopBoxService","optional":[],"required":["projectName","serviceName"]},"services.box.updateAdvanced":{"kind":"mutation","operationId":"updateBoxAdvanced","optional":[],"required":[]},"services.box.updateBasicAuth":{"kind":"mutation","operationId":"updateBoxBasicAuth","optional":[],"required":[]},"services.box.updateDeployScript":{"kind":"mutation","operationId":"updateBoxDeployScript","optional":[],"required":[]},"services.box.updateEnv":{"kind":"mutation","operationId":"updateBoxEnv","optional":[],"required":[]},"services.box.updateGitConfig":{"kind":"mutation","operationId":"updateBoxGitConfig","optional":[],"required":[]},"services.box.updateIde":{"kind":"mutation","operationId":"updateBoxIde","optional":[],"required":[]},"services.box.updateModules":{"kind":"mutation","operationId":"updateBoxModules","optional":[],"required":[]},"services.box.updateNginx":{"kind":"mutation","operationId":"updateBoxNginx","optional":[],"required":[]},"services.box.updateNodejs":{"kind":"mutation","operationId":"updateBoxNodejs","optional":[],"required":[]},"services.box.updatePhp":{"kind":"mutation","operationId":"updateBoxPhp","optional":[],"required":[]},"services.box.updateProcesses":{"kind":"mutation","operationId":"updateBoxProcesses","optional":[],"required":[]},"services.box.updatePython":{"kind":"mutation","operationId":"updateBoxPython","optional":[],"required":[]},"services.box.updateRedirects":{"kind":"mutation","operationId":"updateBoxRedirects","optional":[],"required":[]},"services.box.updateResources":{"kind":"mutation","operationId":"updateBoxResources","optional":[],"required":[]},"services.box.updateRuby":{"kind":"mutation","operationId":"updateBoxRuby","optional":[],"required":[]},"services.box.updateScripts":{"kind":"mutation","operationId":"updateBoxScripts","optional":[],"required":[]},"services.common.getNotes":{"kind":"query","operationId":"getServiceNotes","optional":[],"required":["projectName","serviceName"]},"services.common.getServiceError":{"kind":"query","operationId":"getServiceError","optional":[],"required":["projectName","serviceName"]},"services.common.rename":{"kind":"mutation","operationId":"renameService","optional":[],"required":["newProjectName","newServiceName","oldProjectName","oldServiceName"]},"services.common.setNotes":{"kind":"mutation","operationId":"setServiceNotes","optional":[],"required":[]},"services.compose.createService":{"kind":"mutation","operationId":"createComposeService","optional":[],"required":[]},"services.compose.deployService":{"kind":"mutation","operationId":"deployComposeService","optional":[],"required":[]},"services.compose.destroyService":{"kind":"mutation","operationId":"destroyComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.getDockerServices":{"kind":"query","operationId":"getComposeDockerServices","optional":["projectName","serviceName"],"required":[]},"services.compose.getIssues":{"kind":"query","operationId":"getComposeIssues","optional":[],"required":["projectName","serviceName"]},"services.compose.inspectService":{"kind":"query","operationId":"inspectComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.refreshDeployToken":{"kind":"mutation","operationId":"refreshComposeDeployToken","optional":[],"required":["projectName","serviceName"]},"services.compose.restartService":{"kind":"mutation","operationId":"restartComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.startService":{"kind":"mutation","operationId":"startComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.stopService":{"kind":"mutation","operationId":"stopComposeService","optional":[],"required":["projectName","serviceName"]},"services.compose.updateBasicAuth":{"kind":"mutation","operationId":"updateComposeBasicAuth","optional":[],"required":[]},"services.compose.updateEnv":{"kind":"mutation","operationId":"updateComposeEnv","optional":[],"required":[]},"services.compose.updateMaintenance":{"kind":"mutation","operationId":"updateComposeMaintenance","optional":[],"required":[]},"services.compose.updateRedirects":{"kind":"mutation","operationId":"updateComposeRedirects","optional":[],"required":[]},"services.compose.updateSourceGit":{"kind":"mutation","operationId":"updateComposeSourceGit","optional":[],"required":[]},"services.compose.updateSourceInline":{"kind":"mutation","operationId":"updateComposeSourceInline","optional":[],"required":[]},"services.mariadb.createService":{"kind":"mutation","operationId":"createMariaDBService","optional":[],"required":[]},"services.mariadb.destroyService":{"kind":"mutation","operationId":"destroyMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableDbGate":{"kind":"mutation","operationId":"disableMariaDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mariadb.disablePhpMyAdmin":{"kind":"mutation","operationId":"disableMariaDBPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mariadb.disableService":{"kind":"mutation","operationId":"disableMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableDbGate":{"kind":"mutation","operationId":"enableMariaDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mariadb.enablePhpMyAdmin":{"kind":"mutation","operationId":"enableMariaDBPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mariadb.enableService":{"kind":"mutation","operationId":"enableMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.exposeService":{"kind":"mutation","operationId":"exposeMariaDBService","optional":[],"required":[]},"services.mariadb.inspectService":{"kind":"query","operationId":"inspectMariaDBService","optional":[],"required":["projectName","serviceName"]},"services.mariadb.updateAdvanced":{"kind":"mutation","operationId":"updateMariaDBAdvanced","optional":[],"required":[]},"services.mariadb.updateCredentials":{"kind":"mutation","operationId":"updateMariaDBCredentials","optional":[],"required":[]},"services.mariadb.updateResources":{"kind":"mutation","operationId":"updateMariaDBResources","optional":[],"required":[]},"services.mongo.createService":{"kind":"mutation","operationId":"createMongoDBService","optional":[],"required":[]},"services.mongo.destroyService":{"kind":"mutation","operationId":"destroyMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.disableDbGate":{"kind":"mutation","operationId":"disableMongoDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mongo.disableMongoExpress":{"kind":"mutation","operationId":"disableMongoDBMongoExpress","optional":[],"required":["projectName","serviceName"]},"services.mongo.disableService":{"kind":"mutation","operationId":"disableMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.enableDbGate":{"kind":"mutation","operationId":"enableMongoDBDbGate","optional":[],"required":["projectName","serviceName"]},"services.mongo.enableMongoExpress":{"kind":"mutation","operationId":"enableMongoDBMongoExpress","optional":[],"required":["projectName","serviceName"]},"services.mongo.enableService":{"kind":"mutation","operationId":"enableMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.exposeService":{"kind":"mutation","operationId":"exposeMongoDBService","optional":[],"required":[]},"services.mongo.inspectService":{"kind":"query","operationId":"inspectMongoDBService","optional":[],"required":["projectName","serviceName"]},"services.mongo.updateAdvanced":{"kind":"mutation","operationId":"updateMongoDBAdvanced","optional":[],"required":[]},"services.mongo.updateCredentials":{"kind":"mutation","operationId":"updateMongoDBCredentials","optional":[],"required":[]},"services.mongo.updateResources":{"kind":"mutation","operationId":"updateMongoDBResources","optional":[],"required":[]},"services.mysql.createService":{"kind":"mutation","operationId":"createMySQLService","optional":[],"required":[]},"services.mysql.destroyService":{"kind":"mutation","operationId":"destroyMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.disableDbGate":{"kind":"mutation","operationId":"disableMySQLDbGate","optional":[],"required":["projectName","serviceName"]},"services.mysql.disablePhpMyAdmin":{"kind":"mutation","operationId":"disableMySQLPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mysql.disableService":{"kind":"mutation","operationId":"disableMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.enableDbGate":{"kind":"mutation","operationId":"enableMySQLDbGate","optional":[],"required":["projectName","serviceName"]},"services.mysql.enablePhpMyAdmin":{"kind":"mutation","operationId":"enableMySQLPhpMyAdmin","optional":[],"required":["projectName","serviceName"]},"services.mysql.enableService":{"kind":"mutation","operationId":"enableMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.exposeService":{"kind":"mutation","operationId":"exposeMySQLService","optional":[],"required":[]},"services.mysql.inspectService":{"kind":"query","operationId":"inspectMySQLService","optional":[],"required":["projectName","serviceName"]},"services.mysql.updateAdvanced":{"kind":"mutation","operationId":"updateMySQLAdvanced","optional":[],"required":[]},"services.mysql.updateCredentials":{"kind":"mutation","operationId":"updateMySQLCredentials","optional":[],"required":[]},"services.mysql.updateResources":{"kind":"mutation","operationId":"updateMySQLResources","optional":[],"required":[]},"services.postgres.createService":{"kind":"mutation","operationId":"createPostgresService","optional":[],"required":[]},"services.postgres.destroyService":{"kind":"mutation","operationId":"destroyPostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.disableDbGate":{"kind":"mutation","operationId":"disablePostgresDbGate","optional":[],"required":["projectName","serviceName"]},"services.postgres.disablePgWeb":{"kind":"mutation","operationId":"disablePostgresPgWeb","optional":[],"required":["projectName","serviceName"]},"services.postgres.disableService":{"kind":"mutation","operationId":"disablePostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.enableDbGate":{"kind":"mutation","operationId":"enablePostgresDbGate","optional":[],"required":["projectName","serviceName"]},"services.postgres.enablePgWeb":{"kind":"mutation","operationId":"enablePostgresPgWeb","optional":[],"required":["projectName","serviceName"]},"services.postgres.enableService":{"kind":"mutation","operationId":"enablePostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.exposeService":{"kind":"mutation","operationId":"exposePostgresService","optional":[],"required":[]},"services.postgres.inspectService":{"kind":"query","operationId":"inspectPostgresService","optional":[],"required":["projectName","serviceName"]},"services.postgres.updateAdvanced":{"kind":"mutation","operationId":"updatePostgresAdvanced","optional":[],"required":[]},"services.postgres.updateCredentials":{"kind":"mutation","operationId":"updatePostgresCredentials","optional":[],"required":[]},"services.postgres.updateResources":{"kind":"mutation","operationId":"updatePostgresResources","optional":[],"required":[]},"services.redis.createService":{"kind":"mutation","operationId":"createRedisService","optional":[],"required":[]},"services.redis.destroyService":{"kind":"mutation","operationId":"destroyRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.disableDbGate":{"kind":"mutation","operationId":"disableRedisDbGate","optional":[],"required":["projectName","serviceName"]},"services.redis.disableRedisCommander":{"kind":"mutation","operationId":"disableRedisCommander","optional":[],"required":["projectName","serviceName"]},"services.redis.disableService":{"kind":"mutation","operationId":"disableRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.enableDbGate":{"kind":"mutation","operationId":"enableRedisDbGate","optional":[],"required":["projectName","serviceName"]},"services.redis.enableRedisCommander":{"kind":"mutation","operationId":"enableRedisCommander","optional":[],"required":["projectName","serviceName"]},"services.redis.enableService":{"kind":"mutation","operationId":"enableRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.exposeService":{"kind":"mutation","operationId":"exposeRedisService","optional":[],"required":[]},"services.redis.inspectService":{"kind":"query","operationId":"inspectRedisService","optional":[],"required":["projectName","serviceName"]},"services.redis.updateAdvanced":{"kind":"mutation","operationId":"updateRedisAdvanced","optional":[],"required":[]},"services.redis.updateCredentials":{"kind":"mutation","operationId":"updateRedisCredentials","optional":[],"required":[]},"services.redis.updateResources":{"kind":"mutation","operationId":"updateRedisResources","optional":[],"required":[]},"services.wordpress.activatePlugin":{"kind":"mutation","operationId":"activateWordPressPlugin","optional":[],"required":[]},"services.wordpress.activateTheme":{"kind":"mutation","operationId":"activateWordPressTheme","optional":[],"required":[]},"services.wordpress.createOption":{"kind":"mutation","operationId":"createWordPressOption","optional":[],"required":["name","value"]},"services.wordpress.createRole":{"kind":"mutation","operationId":"createWordPressRole","optional":[],"required":["display_name","name"]},"services.wordpress.createService":{"kind":"mutation","operationId":"createWordPressService","optional":[],"required":[]},"services.wordpress.createUser":{"kind":"mutation","operationId":"createWordPressUser","optional":[],"required":["display_name","password","roles","user_email"]},"services.wordpress.dbOptimize":{"kind":"mutation","operationId":"dbWordPressOptimize","optional":[],"required":["projectName","serviceName"]},"services.wordpress.deactivatePlugin":{"kind":"mutation","operationId":"deactivateWordPressPlugin","optional":[],"required":[]},"services.wordpress.deleteOption":{"kind":"mutation","operationId":"deleteWordPressOption","optional":[],"required":[]},"services.wordpress.deleteRole":{"kind":"mutation","operationId":"deleteWordPressRole","optional":[],"required":[]},"services.wordpress.deleteTransient":{"kind":"mutation","operationId":"deleteWordPressTransient","optional":[],"required":["projectName","serviceName"]},"services.wordpress.deleteUser":{"kind":"mutation","operationId":"deleteWordPressUser","optional":[],"required":[]},"services.wordpress.destroyService":{"kind":"mutation","operationId":"destroyWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.flushCache":{"kind":"mutation","operationId":"flushWordPressCache","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getDatabaseServices":{"kind":"query","operationId":"getWordPressDatabaseServices","optional":[],"required":["projectName"]},"services.wordpress.getMaintenanceMode":{"kind":"query","operationId":"getWordPressMaintenanceMode","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getOptions":{"kind":"query","operationId":"getWordPressOptions","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getPlugins":{"kind":"query","operationId":"getWordPressPlugins","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getProfile":{"kind":"query","operationId":"getWordPressProfile","optional":[],"required":[]},"services.wordpress.getRoles":{"kind":"query","operationId":"getWordPressRoles","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getThemes":{"kind":"query","operationId":"getWordPressThemes","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getUsers":{"kind":"query","operationId":"getWordPressUsers","optional":[],"required":["projectName","serviceName"]},"services.wordpress.getWpConfig":{"kind":"query","operationId":"getWordPressWpConfig","optional":[],"required":["projectName","serviceName"]},"services.wordpress.gitClone":{"kind":"mutation","operationId":"gitWordPressClone","optional":[],"required":[]},"services.wordpress.initService":{"kind":"mutation","operationId":"initWordPressService","optional":[],"required":[]},"services.wordpress.inspectService":{"kind":"query","operationId":"inspectWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.installPlugin":{"kind":"mutation","operationId":"installWordPressPlugin","optional":[],"required":[]},"services.wordpress.installTheme":{"kind":"mutation","operationId":"installWordPressTheme","optional":[],"required":[]},"services.wordpress.mediaRegenerate":{"kind":"mutation","operationId":"mediaWordPressRegenerate","optional":[],"required":["projectName","serviceName"]},"services.wordpress.rebuildDockerImage":{"kind":"mutation","operationId":"rebuildWordPressDockerImage","optional":[],"required":["projectName","serviceName"]},"services.wordpress.restartService":{"kind":"mutation","operationId":"restartWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.runScript":{"kind":"mutation","operationId":"runWordPressScript","optional":[],"required":[]},"services.wordpress.searchPlugin":{"kind":"query","operationId":"searchWordPressPlugin","optional":[],"required":[]},"services.wordpress.searchReplace":{"kind":"mutation","operationId":"searchWordPressReplace","optional":[],"required":[]},"services.wordpress.searchReplaceDryRun":{"kind":"query","operationId":"searchWordPressReplaceDryRun","optional":[],"required":[]},"services.wordpress.searchTheme":{"kind":"query","operationId":"searchWordPressTheme","optional":[],"required":[]},"services.wordpress.startService":{"kind":"mutation","operationId":"startWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.stopService":{"kind":"mutation","operationId":"stopWordPressService","optional":[],"required":["projectName","serviceName"]},"services.wordpress.updateBasicAuth":{"kind":"mutation","operationId":"updateWordPressBasicAuth","optional":[],"required":[]},"services.wordpress.updateEnv":{"kind":"mutation","operationId":"updateWordPressEnv","optional":[],"required":[]},"services.wordpress.updateGitConfig":{"kind":"mutation","operationId":"updateWordPressGitConfig","optional":[],"required":[]},"services.wordpress.updateIde":{"kind":"mutation","operationId":"updateWordPressIde","optional":[],"required":[]},"services.wordpress.updateMaintenanceMode":{"kind":"mutation","operationId":"updateWordPressMaintenanceMode","optional":[],"required":[]},"services.wordpress.updateNginx":{"kind":"mutation","operationId":"updateWordPressNginx","optional":[],"required":[]},"services.wordpress.updateOption":{"kind":"mutation","operationId":"updateWordPressOption","optional":[],"required":["name","value"]},"services.wordpress.updatePhp":{"kind":"mutation","operationId":"updateWordPressPhp","optional":[],"required":[]},"services.wordpress.updateRedirects":{"kind":"mutation","operationId":"updateWordPressRedirects","optional":[],"required":[]},"services.wordpress.updateResources":{"kind":"mutation","operationId":"updateWordPressResources","optional":[],"required":[]},"services.wordpress.updateScripts":{"kind":"mutation","operationId":"updateWordPressScripts","optional":[],"required":[]},"services.wordpress.updateUser":{"kind":"mutation","operationId":"updateWordPressUser","optional":["password"],"required":["ID","display_name","roles","user_email"]},"services.wordpress.updateWpConfig":{"kind":"mutation","operationId":"updateWordPressWpConfig","optional":[],"required":[]},"services.wordpress.updateWpCore":{"kind":"mutation","operationId":"updateWordPressWpCore","optional":[],"required":["projectName","serviceName"]},"settings.changeCredentials":{"kind":"mutation","operationId":"changeCredentials","optional":[],"required":["email","newPassword","oldPassword"]},"settings.checkDockerUpdate":{"kind":"query","operationId":"checkDockerUpdate","optional":[],"required":[]},"settings.checkForUpdates":{"kind":"query","operationId":"checkForUpdates","optional":[],"required":[]},"settings.cleanupDockerBuilder":{"kind":"mutation","operationId":"cleanupDockerBuilder","optional":[],"required":[]},"settings.cleanupDockerImages":{"kind":"mutation","operationId":"cleanupDockerImages","optional":[],"required":[]},"settings.getDailyDockerCleanup":{"kind":"query","operationId":"getDailyDockerCleanup","optional":[],"required":[]},"settings.getDemoMode":{"kind":"query","operationId":"getDemoMode","optional":[],"required":[]},"settings.getDockerVersion":{"kind":"query","operationId":"getDockerVersion","optional":[],"required":[]},"settings.getGithubToken":{"kind":"query","operationId":"getGithubToken","optional":[],"required":[]},"settings.getGoogleAnalyticsMeasurementId":{"kind":"query","operationId":"getGoogleAnalyticsMeasurementId","optional":[],"required":[]},"settings.getLetsEncryptEmail":{"kind":"query","operationId":"getLetsEncryptEmail","optional":[],"required":[]},"settings.getPanelDomain":{"kind":"query","operationId":"getPanelDomain","optional":[],"required":[]},"settings.getServerIp":{"kind":"query","operationId":"getServerIp","optional":[],"required":[]},"settings.getServiceDomain":{"kind":"query","operationId":"getServiceDomain","optional":[],"required":[]},"settings.getTelemetryDisabled":{"kind":"query","operationId":"getTelemetryDisabled","optional":[],"required":[]},"settings.refreshServerIp":{"kind":"mutation","operationId":"refreshServerIp","optional":[],"required":[]},"settings.restartEasypanel":{"kind":"mutation","operationId":"restartEasypanel","optional":[],"required":[]},"settings.setDailyDockerCleanup":{"kind":"mutation","operationId":"setDailyDockerCleanup","optional":[],"required":["dailyDockerCleanup"]},"settings.setGithubToken":{"kind":"mutation","operationId":"setGithubToken","optional":["githubToken"],"required":[]},"settings.setGoogleAnalyticsMeasurementId":{"kind":"mutation","operationId":"setGoogleAnalyticsMeasurementId","optional":["measurementId"],"required":[]},"settings.setLetsEncryptEmail":{"kind":"mutation","operationId":"setLetsEncryptEmail","optional":[],"required":["letsEncryptEmail"]},"settings.setPanelDomain":{"kind":"mutation","operationId":"setPanelDomain","optional":[],"required":["customPanelDomain","serveOnIp"]},"settings.setServiceDomain":{"kind":"mutation","operationId":"setServiceDomain","optional":[],"required":["customServiceDomain"]},"settings.setTelemetryDisabled":{"kind":"mutation","operationId":"setTelemetryDisabled","optional":[],"required":["disabled"]},"settings.systemPrune":{"kind":"mutation","operationId":"systemPrune","optional":[],"required":[]},"setup.getStatus":{"kind":"query","operationId":"getSetupStatus","optional":[],"required":[]},"setup.setup":{"kind":"mutation","operationId":"setup","optional":[],"required":["email","password","source","subscribe","terms"]},"storageProviders.common.list":{"kind":"query","operationId":"listStorageProviders","optional":[],"required":[]},"storageProviders.common.listOptions":{"kind":"query","operationId":"listStorageProviderOptions","optional":[],"required":[]},"storageProviders.dropbox.createProvider":{"kind":"mutation","operationId":"createDropboxProvider","optional":[],"required":["name"]},"storageProviders.dropbox.deleteProvider":{"kind":"mutation","operationId":"deleteDropboxProvider","optional":[],"required":["id"]},"storageProviders.dropbox.disconnectProvider":{"kind":"mutation","operationId":"disconnectDropboxProvider","optional":[],"required":["id"]},"storageProviders.dropbox.updateProvider":{"kind":"mutation","operationId":"updateDropboxProvider","optional":["name"],"required":["id"]},"storageProviders.ftp.createProvider":{"kind":"mutation","operationId":"createFTProvider","optional":[],"required":["host","name","password","port","username"]},"storageProviders.ftp.deleteProvider":{"kind":"mutation","operationId":"deleteFTProvider","optional":[],"required":["id"]},"storageProviders.ftp.updateProvider":{"kind":"mutation","operationId":"updateFTProvider","optional":["port"],"required":["host","id","name","password","username"]},"storageProviders.google.createProvider":{"kind":"mutation","operationId":"createGoogleProvider","optional":[],"required":["name"]},"storageProviders.google.deleteProvider":{"kind":"mutation","operationId":"deleteGoogleProvider","optional":[],"required":["id"]},"storageProviders.google.disconnectProvider":{"kind":"mutation","operationId":"disconnectGoogleProvider","optional":[],"required":["id"]},"storageProviders.google.updateProvider":{"kind":"mutation","operationId":"updateGoogleProvider","optional":["name"],"required":["id"]},"storageProviders.local.createProvider":{"kind":"mutation","operationId":"createLocalProvider","optional":[],"required":["name","path"]},"storageProviders.local.deleteProvider":{"kind":"mutation","operationId":"deleteLocalProvider","optional":[],"required":["id"]},"storageProviders.local.updateProvider":{"kind":"mutation","operationId":"updateLocalProvider","optional":[],"required":["id","name","path"]},"storageProviders.s3.createProvider":{"kind":"mutation","operationId":"createS3Provider","optional":["endpoint"],"required":["accessKeyId","bucket","name","region","secretAccessKey","storageClass","subtype"]},"storageProviders.s3.deleteProvider":{"kind":"mutation","operationId":"deleteS3Provider","optional":[],"required":["id"]},"storageProviders.s3.updateProvider":{"kind":"mutation","operationId":"updateS3Provider","optional":["endpoint"],"required":["accessKeyId","bucket","id","name","region","secretAccessKey","storageClass"]},"storageProviders.sftp.createProvider":{"kind":"mutation","operationId":"createSFTProvider","optional":["port"],"required":["host","name","password","username"]},"storageProviders.sftp.deleteProvider":{"kind":"mutation","operationId":"deleteSFTProvider","optional":[],"required":["id"]},"storageProviders.sftp.updateProvider":{"kind":"mutation","operationId":"updateSFTProvider","optional":["port"],"required":["host","id","name","password","username"]},"subscription.onInvalidateActions":{"kind":"query","operationId":"onInvalidateActions","optional":[],"required":[]},"templates.createFromSchema":{"kind":"mutation","operationId":"createFromSchema","optional":["name"],"required":["projectName","schema"]},"traefik.getCustomConfig":{"kind":"query","operationId":"getCustomConfig","optional":[],"required":[]},"traefik.getDashboard":{"kind":"mutation","operationId":"getDashboard","optional":[],"required":[]},"traefik.getEnv":{"kind":"query","operationId":"getEnv","optional":[],"required":[]},"traefik.restart":{"kind":"mutation","operationId":"restart","optional":[],"required":[]},"traefik.setCustomConfig":{"kind":"mutation","operationId":"setCustomConfig","optional":["config"],"required":[]},"traefik.setEnv":{"kind":"mutation","operationId":"setEnv","optional":["env"],"required":[]},"twoFactor.configure":{"kind":"mutation","operationId":"configure","optional":[],"required":[]},"twoFactor.disable":{"kind":"mutation","operationId":"disable","optional":[],"required":[]},"twoFactor.enable":{"kind":"mutation","operationId":"enable","optional":[],"required":["code"]},"update.getStatus":{"kind":"query","operationId":"getUpdateStatus","optional":[],"required":[]},"update.update":{"kind":"mutation","operationId":"update","optional":[],"required":[]},"users.createUser":{"kind":"mutation","operationId":"createUser","optional":[],"required":["admin","email","password"]},"users.destroyUser":{"kind":"mutation","operationId":"destroyUser","optional":[],"required":["id"]},"users.generateApiToken":{"kind":"mutation","operationId":"generateApiToken","optional":[],"required":["id"]},"users.listUsers":{"kind":"query","operationId":"listUsers","optional":[],"required":[]},"users.revokeApiToken":{"kind":"mutation","operationId":"revokeApiToken","optional":[],"required":["id"]},"users.updateUser":{"kind":"mutation","operationId":"updateUser","optional":["password"],"required":["admin","id"]},"volumeBackups.createVolumeBackup":{"kind":"mutation","operationId":"createVolumeBackup","optional":[],"required":[]},"volumeBackups.destroyVolumeBackup":{"kind":"mutation","operationId":"destroyVolumeBackup","optional":[],"required":["id"]},"volumeBackups.listVolumeBackups":{"kind":"query","operationId":"listVolumeBackups","optional":[],"required":["projectName","serviceName"]},"volumeBackups.listVolumeMounts":{"kind":"query","operationId":"listVolumeMounts","optional":[],"required":["projectName","serviceName"]},"volumeBackups.runVolumeBackup":{"kind":"mutation","operationId":"runVolumeBackup","optional":[],"required":["id"]},"volumeBackups.updateVolumeBackup":{"kind":"mutation","operationId":"updateVolumeBackup","optional":[],"required":[]}}} diff --git a/internal/easypanel/version.go b/internal/easypanel/version.go new file mode 100644 index 0000000..95be2b3 --- /dev/null +++ b/internal/easypanel/version.go @@ -0,0 +1,186 @@ +package easypanel + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" +) + +// Supported panel range. The route surface this project depends on +// (services.app.*, services.compose.*, domains.*, logs.*, projects.*) is +// identical across 2.32.0 through 2.33.1. Both bounds are backed by pinned +// route surfaces in testdata, and MinSupportedVersion must match the oldest one. +const ( + MinSupportedVersion = "2.32.0" + MaxTestedVersion = "2.33.1" +) + +// versionCacheTTL keeps the version probe cheap without pinning a stale value +// across a panel upgrade. +const versionCacheTTL = 10 * time.Minute + +// Version is a parsed panel version. +type Version struct { + Raw string + Major int + Minor int + Patch int +} + +func (v Version) String() string { + if v.Raw != "" { + return v.Raw + } + return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +// Compare returns -1, 0 or 1 comparing v against o by major, minor, patch. +func (v Version) Compare(o Version) int { + for _, pair := range [][2]int{{v.Major, o.Major}, {v.Minor, o.Minor}, {v.Patch, o.Patch}} { + if pair[0] < pair[1] { + return -1 + } + if pair[0] > pair[1] { + return 1 + } + } + return 0 +} + +// AtLeast reports whether v >= other. +func (v Version) AtLeast(other Version) bool { return v.Compare(other) >= 0 } + +// ParseVersion accepts "2.33.0", "v2.33.0" and "2.33.0-canary". +func ParseVersion(raw string) (Version, error) { + v := Version{Raw: raw} + s := strings.TrimPrefix(strings.TrimSpace(raw), "v") + if i := strings.IndexAny(s, "-+"); i >= 0 { + s = s[:i] + } + parts := strings.Split(s, ".") + if len(parts) == 0 || parts[0] == "" { + return Version{}, fmt.Errorf("parse version %q", raw) + } + nums := make([]int, 3) + for i := 0; i < len(parts) && i < 3; i++ { + n, err := strconv.Atoi(parts[i]) + if err != nil { + return Version{}, fmt.Errorf("parse version %q: %w", raw, err) + } + nums[i] = n + } + v.Major, v.Minor, v.Patch = nums[0], nums[1], nums[2] + return v, nil +} + +// SupportRange reports whether ver is inside the verified support window. +// tooOld services get rejected by callers; newer-than-tested only warns. +func SupportRange(ver Version) (tooOld, untested bool) { + minV, _ := ParseVersion(MinSupportedVersion) + maxV, _ := ParseVersion(MaxTestedVersion) + return ver.Compare(minV) < 0, ver.Compare(maxV) > 0 +} + +// PanelVersion reads the panel version from its OpenAPI document. That route +// exists in every release this project supports, unlike /api/cli.json which +// was only added in 2.33. +func (c *Client) PanelVersion(ctx context.Context) (Version, error) { + c.versionMu.Lock() + defer c.versionMu.Unlock() + if c.version.Raw != "" && time.Now().Before(c.versionExpiry) { + return c.version, nil + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/openapi.json", nil) + if err != nil { + return Version{}, fmt.Errorf("panel version request: %w", err) + } + req.Header.Set("Authorization", c.token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return Version{}, fmt.Errorf("panel version: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return Version{}, fmt.Errorf("panel version: HTTP %d", resp.StatusCode) + } + + // The document is large; only info.version is needed. + var doc struct { + Info struct { + Version string `json:"version"` + } `json:"info"` + } + if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { + return Version{}, fmt.Errorf("panel version decode: %w", err) + } + if doc.Info.Version == "" { + return Version{}, fmt.Errorf("panel version missing from OpenAPI document") + } + + ver, err := ParseVersion(doc.Info.Version) + if err != nil { + return Version{}, err + } + c.version = ver + c.versionExpiry = time.Now().Add(versionCacheTTL) + return ver, nil +} + +// LogSettings mirrors the panel's `settings.logs` record. A nil result means log +// aggregation was never configured, so Loki is not deployed. +type LogSettings struct { + Enabled bool `json:"enabled"` + RetentionType string `json:"retentionType"` + RetentionValue int `json:"retentionValue"` +} + +// logSettingsTTL is short: operators can toggle log aggregation at any time. +const logSettingsTTL = 30 * time.Second + +// LogAggregation reports the panel's log aggregation settings. It returns nil +// when the panel has no log settings at all, which is how an unconfigured +// panel presents itself. +func (c *Client) LogAggregation(ctx context.Context) (*LogSettings, error) { + c.logsMu.Lock() + defer c.logsMu.Unlock() + if time.Now().Before(c.logsExpiry) { + return c.logs, nil + } + + var resp Response[*LogSettings] + if err := c.Call(ctx, RouteLogsGetSettings, nil, &resp); err != nil { + return nil, err + } + c.logs = resp.JSON + c.logsExpiry = time.Now().Add(logSettingsTTL) + return c.logs, nil +} + +// InvalidateLogAggregation drops the cached log settings. Callers use this after +// acting on a "disabled" result so that enabling aggregation in the panel takes +// effect on the next call instead of after the cache TTL. +func (c *Client) InvalidateLogAggregation() { + c.logsMu.Lock() + defer c.logsMu.Unlock() + c.logs = nil + c.logsExpiry = time.Time{} +} + +// versionState is embedded in Client; kept here so the cache fields live next +// to the code that uses them. +type versionState struct { + versionMu sync.Mutex + version Version + versionExpiry time.Time + + logsMu sync.Mutex + logs *LogSettings + logsExpiry time.Time +} diff --git a/internal/easypanel/version_test.go b/internal/easypanel/version_test.go new file mode 100644 index 0000000..bc8d190 --- /dev/null +++ b/internal/easypanel/version_test.go @@ -0,0 +1,111 @@ +package easypanel + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" +) + +func TestPanelVersionReadsOpenAPIDocument(t *testing.T) { + var calls int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/openapi.json" { + t.Errorf("unexpected path %s", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "token" { + t.Errorf("want auth header, got %q", got) + } + atomic.AddInt32(&calls, 1) + // Trimmed shape of the real document. + _, _ = w.Write([]byte(`{"openapi":"3.1.1","info":{"title":"Easypanel API","version":"2.33.0"},"paths":{}}`)) + })) + defer srv.Close() + + c := NewClient(srv.URL, "token") + ver, err := c.PanelVersion(context.Background()) + if err != nil { + t.Fatalf("PanelVersion: %v", err) + } + if ver.Major != 2 || ver.Minor != 33 || ver.Patch != 0 { + t.Fatalf("want 2.33.0, got %s", ver) + } + + // Second call is served from cache. + if _, err := c.PanelVersion(context.Background()); err != nil { + t.Fatalf("cached PanelVersion: %v", err) + } + if got := atomic.LoadInt32(&calls); got != 1 { + t.Errorf("want 1 HTTP call, got %d", got) + } +} + +func TestPanelVersionErrorsWithoutVersionField(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"info":{}}`)) + })) + defer srv.Close() + + if _, err := NewClient(srv.URL, "token").PanelVersion(context.Background()); err == nil { + t.Fatal("want error when info.version is missing") + } +} + +func TestLogAggregationNilWhenUnconfigured(t *testing.T) { + // An unconfigured panel answers logs.getSettings with a null payload. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/trpc/"+RouteLogsGetSettings { + t.Errorf("unexpected path %s", r.URL.Path) + } + _, _ = w.Write([]byte(`{"json":null}`)) + })) + defer srv.Close() + + settings, err := NewClient(srv.URL, "token").LogAggregation(context.Background()) + if err != nil { + t.Fatalf("LogAggregation: %v", err) + } + if settings != nil { + t.Fatalf("want nil settings, got %+v", settings) + } +} + +func TestLogAggregationReportsEnabledSettings(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"json":{"enabled":true,"retentionType":"days","retentionValue":7}}`)) + })) + defer srv.Close() + + settings, err := NewClient(srv.URL, "token").LogAggregation(context.Background()) + if err != nil { + t.Fatalf("LogAggregation: %v", err) + } + if settings == nil || !settings.Enabled || settings.RetentionType != "days" || settings.RetentionValue != 7 { + t.Fatalf("unexpected settings: %+v", settings) + } +} + +func TestIsLogStoreUnreachable(t *testing.T) { + // The panel reports its own failed fetch to Loki as BAD_REQUEST. + unreachable := []string{"fetch failed", "Loki HTTP 503", "Loki query failed", "connect ECONNREFUSED 10.0.1.4:3100"} + for _, msg := range unreachable { + err := error(&APIError{Code: "BAD_REQUEST", Status: 400, Message: msg}) + if !IsLogStoreUnreachable(err) { + t.Errorf("want %q classified as log store failure", msg) + } + } + + other := []string{"Input validation failed", "Service not found."} + for _, msg := range other { + err := error(&APIError{Code: "BAD_REQUEST", Status: 400, Message: msg}) + if IsLogStoreUnreachable(err) { + t.Errorf("did not expect %q classified as log store failure", msg) + } + } + + if IsLogStoreUnreachable(errors.New("fetch failed")) { + t.Error("plain errors must not be classified as panel log store failures") + } +} diff --git a/internal/server/env.go b/internal/server/env.go new file mode 100644 index 0000000..c2eba13 --- /dev/null +++ b/internal/server/env.go @@ -0,0 +1,33 @@ +package server + +import "strings" + +// redactedValue replaces env values when a caller does not ask for them. +const redactedValue = "" + +// redactEnvValues keeps env keys but hides their values. The panel stores +// secrets (API keys, tokens, database passwords) in the same env blob it returns +// from its inspect routes, and a gRPC token is panel-wide: any caller can read +// any project. Keys are enough to see how a service is configured, so values are +// only returned when the request explicitly asks for them. +// +// Comments, blank lines and lines without `=` are preserved as-is: they carry no +// values, and dropping them would change the env content a caller round-trips. +func redactEnvValues(env string) string { + if env == "" { + return "" + } + lines := strings.Split(env, "\n") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + key, _, found := strings.Cut(line, "=") + if !found { + continue + } + lines[i] = key + "=" + redactedValue + } + return strings.Join(lines, "\n") +} diff --git a/internal/server/env_test.go b/internal/server/env_test.go new file mode 100644 index 0000000..b4938da --- /dev/null +++ b/internal/server/env_test.go @@ -0,0 +1,66 @@ +package server + +import ( + "context" + "strings" + "testing" + + "github.com/igun997/deploy-everything/internal/easypanel" + pb "github.com/igun997/deploy-everything/proto" +) + +func TestRedactEnvValuesKeepsKeysAndStructure(t *testing.T) { + in := "# panel config\nAPI_KEY=super-secret\nEMPTY=\nPORT=8080\n\nNOT_AN_ASSIGNMENT\nURL=https://example.com/a=b" + want := "# panel config\nAPI_KEY=\nEMPTY=\nPORT=\n\nNOT_AN_ASSIGNMENT\nURL=" + + if got := redactEnvValues(in); got != want { + t.Errorf("redactEnvValues:\n got %q\nwant %q", got, want) + } + if got := redactEnvValues(""); got != "" { + t.Errorf("empty env should stay empty, got %q", got) + } +} + +const envInspectResponse = `{"json":{"name":"api","type":"app","enabled":true,` + + `"env":"API_KEY=super-secret\nPORT=8080"}}` + +func TestGetServiceStatusRedactsEnvByDefault(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.AppRoute(easypanel.ProcInspectService): envInspectResponse, + easypanel.RouteListDomains: `{"json":[]}`, + easypanel.RouteGetDockerContainers: `{"json":[]}`, + }) + + resp, err := panel.newServer().GetServiceStatus(context.Background(), &pb.GetServiceStatusRequest{ + Project: "pods", Service: "api", + }) + if err != nil { + t.Fatalf("GetServiceStatus: %v", err) + } + if strings.Contains(resp.Env, "super-secret") { + t.Errorf("env values must not leak by default, got %q", resp.Env) + } + for _, want := range []string{"API_KEY=", "PORT="} { + if !strings.Contains(resp.Env, want) { + t.Errorf("want %q in redacted env, got %q", want, resp.Env) + } + } +} + +func TestGetServiceStatusReturnsEnvOnRequest(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.AppRoute(easypanel.ProcInspectService): envInspectResponse, + easypanel.RouteListDomains: `{"json":[]}`, + easypanel.RouteGetDockerContainers: `{"json":[]}`, + }) + + resp, err := panel.newServer().GetServiceStatus(context.Background(), &pb.GetServiceStatusRequest{ + Project: "pods", Service: "api", IncludeEnv: true, + }) + if err != nil { + t.Fatalf("GetServiceStatus: %v", err) + } + if resp.Env != "API_KEY=super-secret\nPORT=8080" { + t.Errorf("want clear-text env when include_env is set, got %q", resp.Env) + } +} diff --git a/internal/server/getlogs_test.go b/internal/server/getlogs_test.go new file mode 100644 index 0000000..9419943 --- /dev/null +++ b/internal/server/getlogs_test.go @@ -0,0 +1,195 @@ +package server + +import ( + "context" + "strings" + "testing" + + "github.com/igun997/deploy-everything/internal/easypanel" + pb "github.com/igun997/deploy-everything/proto" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const logsEnabled = `{"json":{"enabled":true,"retentionType":"days","retentionValue":7}}` + +func TestGetLogsRejectsDisabledLogAggregation(t *testing.T) { + // An unconfigured panel answers logs.getSettings with null and its query + // route with an opaque "fetch failed". The precheck turns that into a + // FailedPrecondition the caller can act on. + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: `{"json":null}`, + }) + + _, err := panel.newServer().GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "hermes", ServiceType: "compose", + }) + if err == nil { + t.Fatal("want error when log aggregation is disabled") + } + if got := status.Code(err); got != codes.FailedPrecondition { + t.Errorf("want FailedPrecondition, got %s: %v", got, err) + } + if panel.callCount(easypanel.RouteQueryComposeLogs) != 0 { + t.Error("query route must not be called when log aggregation is disabled") + } +} + +func TestGetLogsMapsUnreachableLogStore(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: logsEnabled, + easypanel.RouteQueryServiceLogs: `!400 {"json":{"code":"BAD_REQUEST","status":400,"message":"fetch failed"}}`, + }) + + _, err := panel.newServer().GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", + }) + if err == nil { + t.Fatal("want error when the panel cannot reach Loki") + } + if got := status.Code(err); got != codes.FailedPrecondition { + t.Errorf("want FailedPrecondition, got %s: %v", got, err) + } + if !strings.Contains(err.Error(), "easypanel-loki") { + t.Errorf("want the hint to name the log store service, got %q", err.Error()) + } +} + +func TestGetLogsClampsLimitToPanelMaximum(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: logsEnabled, + easypanel.RouteQueryServiceLogs: `{"json":{"entries":[]}}`, + }) + srv := panel.newServer() + + if _, err := srv.GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", Limit: 5000, + }); err != nil { + t.Fatalf("GetLogs: %v", err) + } + if got := panel.input(easypanel.RouteQueryServiceLogs, 0)["limit"]; got != float64(maxLogLimit) { + t.Errorf("want limit clamped to %d, got %v", maxLogLimit, got) + } + + if _, err := srv.GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", + }); err != nil { + t.Fatalf("GetLogs: %v", err) + } + if got := panel.input(easypanel.RouteQueryServiceLogs, 1)["limit"]; got != float64(defaultLogLimit) { + t.Errorf("want default limit %d, got %v", defaultLogLimit, got) + } +} + +func TestGetLogsForwardsFiltersAndComposeService(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: logsEnabled, + easypanel.RouteQueryComposeLogs: `{"json":{"entries":[{"stream":{"stream":"stdout","detected_level":"info"},` + + `"values":[["1700000000000000100","hello"]]}]}}`, + }) + + resp, err := panel.newServer().GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", + Service: "hermes", + ServiceType: "compose", + ComposeService: "agent", + Stream: "stdout", + Levels: []string{"info", "error"}, + Search: "boot", + Start: "1700000000000000000", + End: "1700000000000000999", + }) + if err != nil { + t.Fatalf("GetLogs: %v", err) + } + if resp.Logs != "hello" || len(resp.Entries) != 1 { + t.Errorf("unexpected response: %+v", resp) + } + + in := panel.input(easypanel.RouteQueryComposeLogs, 0) + if in["composeInternalService"] != "agent" { + t.Errorf("want composeInternalService agent, got %v", in["composeInternalService"]) + } + for key, want := range map[string]any{ + "stream": "stdout", + "search": "boot", + "start": "1700000000000000000", + "end": "1700000000000000999", + } { + if in[key] != want { + t.Errorf("want %s=%v, got %v", key, want, in[key]) + } + } + levels, ok := in["levels"].([]any) + if !ok || len(levels) != 2 || levels[0] != "info" { + t.Errorf("want levels [info error], got %v", in["levels"]) + } +} + +func TestGetLogsOmitsUnsetFilters(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: logsEnabled, + easypanel.RouteQueryServiceLogs: `{"json":{"entries":[]}}`, + }) + + if _, err := panel.newServer().GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", + }); err != nil { + t.Fatalf("GetLogs: %v", err) + } + + in := panel.input(easypanel.RouteQueryServiceLogs, 0) + for _, key := range []string{"stream", "levels", "search", "start", "end", "composeInternalService"} { + if _, present := in[key]; present { + t.Errorf("did not expect %s in the panel input: %v", key, in) + } + } +} + +func TestGetLogsRejectsInvalidStream(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: logsEnabled, + easypanel.RouteQueryServiceLogs: `{"json":{"entries":[]}}`, + }) + + _, err := panel.newServer().GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", Stream: "stdio", + }) + if got := status.Code(err); got != codes.InvalidArgument { + t.Errorf("want InvalidArgument, got %s: %v", got, err) + } + if panel.callCount(easypanel.RouteQueryServiceLogs) != 0 { + t.Error("invalid stream must be rejected before calling the panel") + } +} + +// Enabling log aggregation must take effect on the next call: the disabled +// precheck drops the cached settings instead of waiting out the cache TTL. +func TestGetLogsRechecksSettingsAfterDisabledResult(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.RouteLogsGetSettings: `{"json":null}`, + easypanel.RouteQueryServiceLogs: `{"json":{"entries":[{"stream":{},"values":[["1","up"]]}]}}`, + }) + srv := panel.newServer() + + if _, err := srv.GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", + }); status.Code(err) != codes.FailedPrecondition { + t.Fatalf("want FailedPrecondition on the first call, got %v", err) + } + + panel.set(easypanel.RouteLogsGetSettings, logsEnabled) + + resp, err := srv.GetLogs(context.Background(), &pb.GetLogsRequest{ + Project: "pods", Service: "api", + }) + if err != nil { + t.Fatalf("GetLogs after enabling aggregation: %v", err) + } + if resp.Logs != "up" { + t.Errorf("want logs after enabling aggregation, got %q", resp.Logs) + } + if got := panel.callCount(easypanel.RouteLogsGetSettings); got != 2 { + t.Errorf("want the settings route re-queried, got %d calls", got) + } +} diff --git a/internal/server/logs.go b/internal/server/logs.go index a9e95bf..9535f9c 100644 --- a/internal/server/logs.go +++ b/internal/server/logs.go @@ -11,6 +11,13 @@ import ( // defaultLogLimit matches what the panel UI requests for an initial log view. const defaultLogLimit = 200 +// maxLogLimit is the panel's zod ceiling on logs.query*ServiceLogs limit. +// Sending more is rejected as a validation error, so clamp instead. +const maxLogLimit = 1000 + +// validLogStreams are the only stream values the panel accepts. +var validLogStreams = map[string]bool{"stdout": true, "stderr": true} + // lokiStream is one label-set group in a panel log response. The panel proxies // its log store (Loki-shaped), so each group carries a label map plus // [timestamp, line] pairs. diff --git a/internal/server/paas.go b/internal/server/paas.go index 108799f..a10dfdf 100644 --- a/internal/server/paas.go +++ b/internal/server/paas.go @@ -42,7 +42,7 @@ func NewPaaSServer(ep *easypanel.Client, defaultDomain, customDomain string) *Pa func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContainerRequest) (*pb.DeployContainerResponse, error) { // 1. Create app service - err := s.ep.Call(ctx, "services.app.createService", map[string]string{ + err := s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcCreateService), map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, nil) @@ -51,7 +51,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine } // 2. Set Docker image - err = s.ep.Call(ctx, "services.app.updateSourceImage", map[string]string{ + err = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcUpdateSourceImage), map[string]string{ "projectName": req.Project, "serviceName": req.Service, "image": req.Image, @@ -66,7 +66,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine for k, v := range req.Env { envLines = append(envLines, fmt.Sprintf("%s=%s", k, v)) } - err = s.ep.Call(ctx, "services.app.updateEnv", map[string]any{ + err = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcUpdateEnv), map[string]any{ "projectName": req.Project, "serviceName": req.Service, "env": strings.Join(envLines, "\n"), @@ -78,7 +78,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine // 4. Set resources if provided if req.Resources != nil { - err = s.ep.Call(ctx, "services.app.updateResources", map[string]any{ + err = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcUpdateResources), map[string]any{ "projectName": req.Project, "serviceName": req.Service, "resources": map[string]any{ @@ -94,7 +94,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine } // 5. Deploy - err = s.ep.Call(ctx, "services.app.deployService", map[string]string{ + err = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcDeployService), map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, nil) @@ -108,7 +108,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine port = req.Port } defaultHost := fmt.Sprintf("%s-%s.%s", req.Project, req.Service, s.defaultDomain) - err = s.ep.Call(ctx, "domains.createDomain", map[string]any{ + err = s.ep.Call(ctx, easypanel.RouteCreateDomain, map[string]any{ "id": "default", "https": true, "host": defaultHost, @@ -133,7 +133,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine customDomain := "" if req.CustomDomain != "" { customDomain = req.CustomDomain - err = s.ep.Call(ctx, "domains.createDomain", map[string]any{ + err = s.ep.Call(ctx, easypanel.RouteCreateDomain, map[string]any{ "id": "custom", "https": true, "host": customDomain, @@ -163,7 +163,7 @@ func (s *PaaSServer) DeployContainer(ctx context.Context, req *pb.DeployContaine // Get deploy URL from inspect var inspectResp easypanel.Response[map[string]any] - _ = s.ep.Call(ctx, "services.app.inspectService", map[string]string{ + _ = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcInspectService), map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, &inspectResp) @@ -188,7 +188,7 @@ func (s *PaaSServer) DeployCompose(ctx context.Context, req *pb.DeployComposeReq } // 1. Create compose service - err = s.ep.Call(ctx, "services.compose.createService", map[string]string{ + err = s.ep.Call(ctx, easypanel.ComposeRoute(easypanel.ProcCreateService), map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, nil) @@ -202,7 +202,7 @@ func (s *PaaSServer) DeployCompose(ctx context.Context, req *pb.DeployComposeReq for k, v := range req.Env { envLines = append(envLines, fmt.Sprintf("%s=%s", k, v)) } - err = s.ep.Call(ctx, "services.compose.updateEnv", map[string]any{ + err = s.ep.Call(ctx, easypanel.ComposeRoute(easypanel.ProcUpdateEnv), map[string]any{ "projectName": req.Project, "serviceName": req.Service, "env": strings.Join(envLines, "\n"), @@ -213,7 +213,7 @@ func (s *PaaSServer) DeployCompose(ctx context.Context, req *pb.DeployComposeReq } // 3. Set validated compose content (host ports removed, expose kept) - err = s.ep.Call(ctx, "services.compose.updateSourceInline", map[string]any{ + err = s.ep.Call(ctx, easypanel.ComposeRoute(easypanel.ProcUpdateSourceInline), map[string]any{ "projectName": req.Project, "serviceName": req.Service, "composeFile": "docker-compose.yml", @@ -224,7 +224,7 @@ func (s *PaaSServer) DeployCompose(ctx context.Context, req *pb.DeployComposeReq } // 4. Deploy - err = s.ep.Call(ctx, "services.compose.deployService", map[string]string{ + err = s.ep.Call(ctx, easypanel.ComposeRoute(easypanel.ProcDeployService), map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, nil) @@ -245,7 +245,7 @@ func (s *PaaSServer) DeployCompose(ctx context.Context, req *pb.DeployComposeReq host = fmt.Sprintf("%s-%s-%s.%s", req.Project, req.Service, d.ComposeService, s.defaultDomain) } - err = s.ep.Call(ctx, "domains.createDomain", map[string]any{ + err = s.ep.Call(ctx, easypanel.RouteCreateDomain, map[string]any{ "id": fmt.Sprintf("compose-%s", d.ComposeService), "https": true, "host": host, @@ -298,19 +298,19 @@ func (s *PaaSServer) DestroyService(ctx context.Context, req *pb.DestroyServiceR // Delete all domains first var domainsResp easypanel.Response[[]map[string]any] - _ = s.ep.Call(ctx, "domains.listDomains", map[string]string{ + _ = s.ep.Call(ctx, easypanel.RouteListDomains, map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, &domainsResp) for _, d := range domainsResp.JSON { if id, ok := d["id"].(string); ok { - _ = s.ep.Call(ctx, "domains.deleteDomain", map[string]string{"id": id}, nil) + _ = s.ep.Call(ctx, easypanel.RouteDeleteDomain, map[string]string{"id": id}, nil) } } // Destroy service - route := fmt.Sprintf("services.%s.destroyService", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcDestroyService) err := s.ep.Call(ctx, route, map[string]string{ "projectName": req.Project, "serviceName": req.Service, @@ -328,7 +328,7 @@ func (s *PaaSServer) GetServiceStatus(ctx context.Context, req *pb.GetServiceSta svcType = "app" } - route := fmt.Sprintf("services.%s.inspectService", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcInspectService) var resp easypanel.Response[map[string]any] err := s.ep.Call(ctx, route, map[string]string{ "projectName": req.Project, @@ -355,7 +355,7 @@ func (s *PaaSServer) GetServiceStatus(ctx context.Context, req *pb.GetServiceSta // Get domains var domainsResp easypanel.Response[[]map[string]any] - _ = s.ep.Call(ctx, "domains.listDomains", map[string]string{ + _ = s.ep.Call(ctx, easypanel.RouteListDomains, map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, &domainsResp) @@ -367,7 +367,7 @@ func (s *PaaSServer) GetServiceStatus(ctx context.Context, req *pb.GetServiceSta } } - return &pb.GetServiceStatusResponse{ + out := &pb.GetServiceStatusResponse{ Name: name, Type: sType, Enabled: enabled, @@ -375,7 +375,87 @@ func (s *PaaSServer) GetServiceStatus(ctx context.Context, req *pb.GetServiceSta Env: env, Domains: domains, DeployUrl: deployURL, - }, nil + } + if !req.IncludeEnv { + out.Env = redactEnvValues(env) + } + + s.attachRuntimeStatus(ctx, req.Project, req.Service, svcType, out) + return out, nil +} + +// attachRuntimeStatus fills the runtime half of GetServiceStatus. The panel's +// inspect routes only return stored configuration, so container state comes +// from projects.getDockerContainers, which already filters to running +// containers for both swarm (app) and compose services. +func (s *PaaSServer) attachRuntimeStatus(ctx context.Context, project, service, svcType string, out *pb.GetServiceStatusResponse) { + if svcType == "compose" { + var composeResp easypanel.Response[[]string] + if err := s.ep.Call(ctx, easypanel.RouteComposeDockerServices, map[string]string{ + "projectName": project, + "serviceName": service, + }, &composeResp); err == nil { + out.ComposeServices = composeResp.JSON + } + } + + var containersResp easypanel.Response[[]dockerContainer] + if err := s.ep.Call(ctx, easypanel.RouteGetDockerContainers, map[string]string{ + "service": project + "_" + service, + }, &containersResp); err != nil { + // Status stays unknown rather than failing the whole call: configuration + // is still useful when the container query is refused. + out.Status = serviceStatusUnknown + return + } + + for _, c := range containersResp.JSON { + out.Containers = append(out.Containers, c.toProto()) + if out.Image == "" { + out.Image = c.Image + } + } + out.RunningContainers = int32(len(out.Containers)) + if out.RunningContainers > 0 { + out.Status = serviceStatusRunning + } else { + out.Status = serviceStatusStopped + } +} + +// Service runtime states reported by GetServiceStatus. +const ( + serviceStatusRunning = "running" + serviceStatusStopped = "stopped" + serviceStatusUnknown = "unknown" +) + +// dockerContainer is the subset of the panel's Docker container payload that +// GetServiceStatus reports. Field names follow the Docker API casing the panel +// passes through verbatim. +type dockerContainer struct { + ID string `json:"Id"` + Names []string `json:"Names"` + Image string `json:"Image"` + State string `json:"State"` + Status string `json:"Status"` + Created int64 `json:"Created"` +} + +func (c dockerContainer) toProto() *pb.ContainerStatus { + name := "" + if len(c.Names) > 0 { + // Docker prefixes container names with a slash. + name = strings.TrimPrefix(c.Names[0], "/") + } + return &pb.ContainerStatus{ + Id: c.ID, + Name: name, + Image: c.Image, + State: c.State, + Status: c.Status, + Created: c.Created, + } } func (s *PaaSServer) ListServices(ctx context.Context, req *pb.ListServicesRequest) (*pb.ListServicesResponse, error) { @@ -383,7 +463,7 @@ func (s *PaaSServer) ListServices(ctx context.Context, req *pb.ListServicesReque Project map[string]any `json:"project"` Services []map[string]any `json:"services"` }] - err := s.ep.Call(ctx, "projects.inspectProject", map[string]string{ + err := s.ep.Call(ctx, easypanel.RouteInspectProject, map[string]string{ "projectName": req.Project, }, &resp) if err != nil { @@ -417,7 +497,7 @@ func (s *PaaSServer) redeployService(ctx context.Context, svcType, project, serv if svcType != "app" && svcType != "compose" { return nil // These service types do not expose deployService. } - return s.ep.Call(ctx, fmt.Sprintf("services.%s.deployService", svcType), map[string]string{ + return s.ep.Call(ctx, easypanel.ServiceRoute(svcType, easypanel.ProcDeployService), map[string]string{ "projectName": project, "serviceName": service, }, nil) @@ -444,7 +524,7 @@ func (s *PaaSServer) AddDomain(ctx context.Context, req *pb.AddDomainRequest) (* dest["composeService"] = req.ComposeService } - err := s.ep.Call(ctx, "domains.createDomain", map[string]any{ + err := s.ep.Call(ctx, easypanel.RouteCreateDomain, map[string]any{ "id": req.Host, "https": req.Https, "host": req.Host, @@ -469,7 +549,7 @@ func (s *PaaSServer) AddDomain(ctx context.Context, req *pb.AddDomainRequest) (* // Get actual ID from list var domainsResp easypanel.Response[[]map[string]any] - _ = s.ep.Call(ctx, "domains.listDomains", map[string]string{ + _ = s.ep.Call(ctx, easypanel.RouteListDomains, map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, &domainsResp) @@ -493,7 +573,7 @@ func (s *PaaSServer) RemoveDomain(ctx context.Context, req *pb.RemoveDomainReque // legacy domain_id-only clients working and prevents stale caller-supplied // type/project data from redeploying the wrong service. var all easypanel.Response[[]map[string]any] - if err := s.ep.Call(ctx, "domains.listDomains", map[string]string{}, &all); err != nil { + if err := s.ep.Call(ctx, easypanel.RouteListDomains, map[string]string{}, &all); err != nil { return nil, fmt.Errorf("resolve domain target: %w", err) } project, service, svcType, found, err := resolveDomainTarget( @@ -507,7 +587,7 @@ func (s *PaaSServer) RemoveDomain(ctx context.Context, req *pb.RemoveDomainReque // success. Skip deletion and retry only the deployment, making the RPC // idempotent when caller supplies target context. if found { - if err := s.ep.Call(ctx, "domains.deleteDomain", map[string]string{"id": req.DomainId}, nil); err != nil { + if err := s.ep.Call(ctx, easypanel.RouteDeleteDomain, map[string]string{"id": req.DomainId}, nil); err != nil { return nil, err } } @@ -584,7 +664,7 @@ func domainTarget(domains []map[string]any, domainID string) (project, service, func (s *PaaSServer) ListDomains(ctx context.Context, req *pb.ListDomainsRequest) (*pb.ListDomainsResponse, error) { var resp easypanel.Response[[]map[string]any] - err := s.ep.Call(ctx, "domains.listDomains", map[string]string{ + err := s.ep.Call(ctx, easypanel.RouteListDomains, map[string]string{ "projectName": req.Project, "serviceName": req.Service, }, &resp) @@ -632,7 +712,7 @@ func (s *PaaSServer) UpdateEnv(ctx context.Context, req *pb.UpdateEnvRequest) (* envLines = append(envLines, fmt.Sprintf("%s=%s", k, v)) } - route := fmt.Sprintf("services.%s.updateEnv", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcUpdateEnv) err := s.ep.Call(ctx, route, map[string]any{ "projectName": req.Project, "serviceName": req.Service, @@ -653,7 +733,7 @@ func (s *PaaSServer) RestartService(ctx context.Context, req *pb.RestartServiceR if svcType == "" { svcType = "app" } - route := fmt.Sprintf("services.%s.restartService", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcRestartService) err := s.ep.Call(ctx, route, map[string]string{ "projectName": req.Project, "serviceName": req.Service, @@ -669,7 +749,7 @@ func (s *PaaSServer) StopService(ctx context.Context, req *pb.StopServiceRequest if svcType == "" { svcType = "app" } - route := fmt.Sprintf("services.%s.stopService", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcStopService) err := s.ep.Call(ctx, route, map[string]string{ "projectName": req.Project, "serviceName": req.Service, @@ -685,7 +765,7 @@ func (s *PaaSServer) StartService(ctx context.Context, req *pb.StartServiceReque if svcType == "" { svcType = "app" } - route := fmt.Sprintf("services.%s.startService", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcStartService) err := s.ep.Call(ctx, route, map[string]string{ "projectName": req.Project, "serviceName": req.Service, @@ -706,18 +786,51 @@ func (s *PaaSServer) GetLogs(ctx context.Context, req *pb.GetLogsRequest) (*pb.G if limit <= 0 { limit = defaultLogLimit } + if limit > maxLogLimit { + limit = maxLogLimit + } + + if req.Stream != "" && !validLogStreams[req.Stream] { + return nil, status.Errorf(codes.InvalidArgument, + "stream must be stdout or stderr, got %q", req.Stream) + } + + // The panel serves logs only from its Loki deployment, which exists solely + // when log aggregation is enabled. Checking first turns the panel's opaque + // "fetch failed" into an actionable error. + if settings, err := s.ep.LogAggregation(ctx); err == nil && (settings == nil || !settings.Enabled) { + // Drop the cached result so enabling aggregation in the panel takes effect + // on the next call rather than after the cache TTL. + s.ep.InvalidateLogAggregation() + return nil, status.Error(codes.FailedPrecondition, logAggregationDisabledMsg) + } input := map[string]any{ "projectName": req.Project, "serviceName": req.Service, "limit": limit, } + if req.Stream != "" { + input["stream"] = req.Stream + } + if len(req.Levels) > 0 { + input["levels"] = req.Levels + } + if req.Search != "" { + input["search"] = req.Search + } + if req.Start != "" { + input["start"] = req.Start + } + if req.End != "" { + input["end"] = req.End + } // Compose stacks are served by a dedicated route; the app route only knows // about single-container services. - route := "logs.queryServiceLogs" + route := easypanel.RouteQueryServiceLogs if svcType == "compose" { - route = "logs.queryComposeServiceLogs" + route = easypanel.RouteQueryComposeLogs if req.ComposeService != "" { input["composeInternalService"] = req.ComposeService } @@ -725,6 +838,10 @@ func (s *PaaSServer) GetLogs(ctx context.Context, req *pb.GetLogsRequest) (*pb.G var resp easypanel.Response[lokiLogs] if err := s.ep.Call(ctx, route, input, &resp); err != nil { + if easypanel.IsLogStoreUnreachable(err) { + return nil, status.Errorf(codes.FailedPrecondition, + "panel could not reach its log store: %v. %s", err, logStoreUnreachableHint) + } return nil, fmt.Errorf("query logs: %w", err) } @@ -735,6 +852,15 @@ func (s *PaaSServer) GetLogs(ctx context.Context, req *pb.GetLogsRequest) (*pb.G }, nil } +// logAggregationDisabledMsg explains the only supported way to make GetLogs +// work: the panel has no log source other than its own Loki service. +const logAggregationDisabledMsg = "panel log aggregation is disabled, so Loki is not deployed and the panel has no log source. " + + "Enable it in the Easypanel UI under Settings -> Logs (requires a license with advanced monitoring), " + + "then retry GetLogs." + +const logStoreUnreachableHint = "Log aggregation is enabled but easypanel-loki is not answering; " + + "check the easypanel-loki and easypanel-promtail services on the host." + func (s *PaaSServer) ScaleService(ctx context.Context, req *pb.ScaleServiceRequest) (*pb.ScaleServiceResponse, error) { svcType := req.ServiceType if svcType == "" { @@ -748,7 +874,7 @@ func (s *PaaSServer) ScaleService(ctx context.Context, req *pb.ScaleServiceReque return nil, fmt.Errorf("inspect deploy config: %w", err) } deploy["replicas"] = req.Replicas - err = s.ep.Call(ctx, "services.app.updateDeploy", map[string]any{ + err = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcUpdateDeploy), map[string]any{ "projectName": req.Project, "serviceName": req.Service, "deploy": deploy, @@ -774,7 +900,7 @@ func (s *PaaSServer) UpdateResources(ctx context.Context, req *pb.UpdateResource if req.Resources == nil { return nil, status.Error(codes.InvalidArgument, "resources are required") } - route := fmt.Sprintf("services.%s.updateResources", svcType) + route := easypanel.ServiceRoute(svcType, easypanel.ProcUpdateResources) err := s.ep.Call(ctx, route, map[string]any{ "projectName": req.Project, "serviceName": req.Service, @@ -816,7 +942,7 @@ func (s *PaaSServer) UpdateDeploy(ctx context.Context, req *pb.UpdateDeployReque deploy["command"] = nil } - err = s.ep.Call(ctx, "services.app.updateDeploy", map[string]any{ + err = s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcUpdateDeploy), map[string]any{ "projectName": req.Project, "serviceName": req.Service, "deploy": deploy, @@ -835,7 +961,7 @@ func (s *PaaSServer) UpdateDeploy(ctx context.Context, req *pb.UpdateDeployReque // command, capabilities, sysctls and other settings. func (s *PaaSServer) currentAppDeploy(ctx context.Context, project, service string) (map[string]any, error) { var inspect easypanel.Response[map[string]any] - if err := s.ep.Call(ctx, "services.app.inspectService", map[string]string{ + if err := s.ep.Call(ctx, easypanel.AppRoute(easypanel.ProcInspectService), map[string]string{ "projectName": project, "serviceName": service, }, &inspect); err != nil { diff --git a/internal/server/panel_fake_test.go b/internal/server/panel_fake_test.go new file mode 100644 index 0000000..1611168 --- /dev/null +++ b/internal/server/panel_fake_test.go @@ -0,0 +1,97 @@ +package server + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "github.com/igun997/deploy-everything/internal/easypanel" +) + +// fakePanel is a stand-in Easypanel that answers /api/trpc/ from a +// canned response table and records the decoded input of every call. +type fakePanel struct { + t *testing.T + responses map[string]string + mu sync.Mutex + calls map[string][]map[string]any + server *httptest.Server +} + +func newFakePanel(t *testing.T, responses map[string]string) *fakePanel { + t.Helper() + p := &fakePanel{ + t: t, + responses: responses, + calls: map[string][]map[string]any{}, + } + p.server = httptest.NewServer(http.HandlerFunc(p.handle)) + t.Cleanup(p.server.Close) + return p +} + +func (p *fakePanel) handle(w http.ResponseWriter, r *http.Request) { + route := strings.TrimPrefix(r.URL.Path, "/api/trpc/") + + body, _ := io.ReadAll(r.Body) + var envelope struct { + JSON map[string]any `json:"json"` + } + _ = json.Unmarshal(body, &envelope) + + p.mu.Lock() + p.calls[route] = append(p.calls[route], envelope.JSON) + p.mu.Unlock() + + p.mu.Lock() + resp, ok := p.responses[route] + p.mu.Unlock() + if !ok { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"json":{"code":"NOT_FOUND","status":404,"message":"route ` + route + ` not stubbed"}}`)) + return + } + if strings.HasPrefix(resp, "!") { + // "! " returns an error response. + parts := strings.SplitN(strings.TrimPrefix(resp, "!"), " ", 2) + w.WriteHeader(http.StatusBadRequest) + if len(parts) == 2 { + _, _ = w.Write([]byte(parts[1])) + } + return + } + _, _ = w.Write([]byte(resp)) +} + +// input returns the decoded input of the nth call to route. +func (p *fakePanel) input(route string, n int) map[string]any { + p.t.Helper() + p.mu.Lock() + defer p.mu.Unlock() + calls := p.calls[route] + if len(calls) <= n { + p.t.Fatalf("route %s was called %d times, wanted call %d", route, len(calls), n+1) + } + return calls[n] +} + +// set replaces the canned response for a route mid-test. +func (p *fakePanel) set(route, response string) { + p.mu.Lock() + defer p.mu.Unlock() + p.responses[route] = response +} + +func (p *fakePanel) callCount(route string) int { + p.mu.Lock() + defer p.mu.Unlock() + return len(p.calls[route]) +} + +func (p *fakePanel) newServer() *PaaSServer { + return NewPaaSServer(easypanel.NewClient(p.server.URL, "token"), "example.host", "custom.host") +} diff --git a/internal/server/status_test.go b/internal/server/status_test.go new file mode 100644 index 0000000..78632c5 --- /dev/null +++ b/internal/server/status_test.go @@ -0,0 +1,110 @@ +package server + +import ( + "context" + "testing" + + "github.com/igun997/deploy-everything/internal/easypanel" + pb "github.com/igun997/deploy-everything/proto" +) + +// A compose service: the panel's inspect route carries no image, so the running +// state and image have to come from the Docker container view. +func TestGetServiceStatusReportsRunningComposeContainers(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.ComposeRoute(easypanel.ProcInspectService): `{"json":{"name":"hermes","type":"compose","enabled":true,"source":{"content":"services:\n agent:\n image: demo\n"}}}`, + easypanel.RouteListDomains: `{"json":[{"host":"hermes.example.host"}]}`, + easypanel.RouteComposeDockerServices: `{"json":["agent"]}`, + easypanel.RouteGetDockerContainers: `{"json":[{"Id":"abc123","Names":["/pods_hermes-agent-1"],` + + `"Image":"demo:1","State":"running","Status":"Up 2 hours","Created":1785849830}]}`, + }) + + resp, err := panel.newServer().GetServiceStatus(context.Background(), &pb.GetServiceStatusRequest{ + Project: "pods", Service: "hermes", ServiceType: "compose", + }) + if err != nil { + t.Fatalf("GetServiceStatus: %v", err) + } + + if resp.Status != serviceStatusRunning { + t.Errorf("want status running, got %q", resp.Status) + } + if resp.RunningContainers != 1 { + t.Errorf("want 1 running container, got %d", resp.RunningContainers) + } + if len(resp.Containers) != 1 { + t.Fatalf("want 1 container entry, got %d", len(resp.Containers)) + } + c := resp.Containers[0] + if c.Name != "pods_hermes-agent-1" { + t.Errorf("want the Docker name slash stripped, got %q", c.Name) + } + if c.Id != "abc123" || c.State != "running" || c.Status != "Up 2 hours" || c.Created != 1785849830 { + t.Errorf("unexpected container payload: %+v", c) + } + // Compose config has no top-level image, so it falls back to the container. + if resp.Image != "demo:1" { + t.Errorf("want image from container, got %q", resp.Image) + } + if len(resp.ComposeServices) != 1 || resp.ComposeServices[0] != "agent" { + t.Errorf("want compose services [agent], got %v", resp.ComposeServices) + } + + // The container query is keyed by _. + if got := panel.input(easypanel.RouteGetDockerContainers, 0)["service"]; got != "pods_hermes" { + t.Errorf("want service pods_hermes, got %v", got) + } +} + +// The panel returns only running containers, so an empty list means stopped. +func TestGetServiceStatusReportsStoppedWhenNoContainers(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.AppRoute(easypanel.ProcInspectService): `{"json":{"name":"api","type":"app","enabled":true,"source":{"image":"nginx:alpine"}}}`, + easypanel.RouteListDomains: `{"json":[]}`, + easypanel.RouteGetDockerContainers: `{"json":[]}`, + }) + + resp, err := panel.newServer().GetServiceStatus(context.Background(), &pb.GetServiceStatusRequest{ + Project: "pods", Service: "api", + }) + if err != nil { + t.Fatalf("GetServiceStatus: %v", err) + } + if resp.Status != serviceStatusStopped { + t.Errorf("want status stopped, got %q", resp.Status) + } + if resp.RunningContainers != 0 || len(resp.Containers) != 0 { + t.Errorf("want no containers, got %d", resp.RunningContainers) + } + // Stored config still wins for the image. + if resp.Image != "nginx:alpine" { + t.Errorf("want configured image, got %q", resp.Image) + } + // App services have no compose service list. + if len(resp.ComposeServices) != 0 { + t.Errorf("want no compose services, got %v", resp.ComposeServices) + } +} + +// A refused container query must not fail the whole call: stored configuration +// is still worth returning. +func TestGetServiceStatusFallsBackToUnknownStatus(t *testing.T) { + panel := newFakePanel(t, map[string]string{ + easypanel.AppRoute(easypanel.ProcInspectService): `{"json":{"name":"api","type":"app","enabled":true}}`, + easypanel.RouteListDomains: `{"json":[]}`, + easypanel.RouteGetDockerContainers: `!400 {"json":{"code":"FORBIDDEN","status":403,"message":"Not authorized"}}`, + }) + + resp, err := panel.newServer().GetServiceStatus(context.Background(), &pb.GetServiceStatusRequest{ + Project: "pods", Service: "api", + }) + if err != nil { + t.Fatalf("GetServiceStatus should tolerate a failed container query: %v", err) + } + if resp.Status != serviceStatusUnknown { + t.Errorf("want status unknown, got %q", resp.Status) + } + if resp.Name != "api" { + t.Errorf("want configuration preserved, got %+v", resp) + } +} diff --git a/proto/paas.pb.go b/proto/paas.pb.go index 44bb99c..2b7bc58 100644 --- a/proto/paas.pb.go +++ b/proto/paas.pb.go @@ -550,10 +550,14 @@ func (x *DestroyServiceResponse) GetSuccess() bool { } type GetServiceStatusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` - Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` - ServiceType string `protobuf:"bytes,3,opt,name=service_type,json=serviceType,proto3" json:"service_type,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + ServiceType string `protobuf:"bytes,3,opt,name=service_type,json=serviceType,proto3" json:"service_type,omitempty"` + // Return environment variable values in clear text. Off by default: the + // panel stores secrets in env, and every gRPC token can read every project. + // With this unset, values are replaced by and only keys are shown. + IncludeEnv bool `protobuf:"varint,4,opt,name=include_env,json=includeEnv,proto3" json:"include_env,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -609,17 +613,33 @@ func (x *GetServiceStatusRequest) GetServiceType() string { return "" } +func (x *GetServiceStatusRequest) GetIncludeEnv() bool { + if x != nil { + return x.IncludeEnv + } + return false +} + type GetServiceStatusResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` - Image string `protobuf:"bytes,4,opt,name=image,proto3" json:"image,omitempty"` - Env string `protobuf:"bytes,5,opt,name=env,proto3" json:"env,omitempty"` - Domains []string `protobuf:"bytes,6,rep,name=domains,proto3" json:"domains,omitempty"` - DeployUrl string `protobuf:"bytes,7,opt,name=deploy_url,json=deployUrl,proto3" json:"deploy_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` + Image string `protobuf:"bytes,4,opt,name=image,proto3" json:"image,omitempty"` + // Saved env content. Values are redacted unless the request sets include_env. + Env string `protobuf:"bytes,5,opt,name=env,proto3" json:"env,omitempty"` + Domains []string `protobuf:"bytes,6,rep,name=domains,proto3" json:"domains,omitempty"` + DeployUrl string `protobuf:"bytes,7,opt,name=deploy_url,json=deployUrl,proto3" json:"deploy_url,omitempty"` + // Runtime state derived from the panel's Docker view: + // running, stopped, or unknown when the panel refuses the container query. + Status string `protobuf:"bytes,8,opt,name=status,proto3" json:"status,omitempty"` + // Running containers backing this service. + Containers []*ContainerStatus `protobuf:"bytes,9,rep,name=containers,proto3" json:"containers,omitempty"` + RunningContainers int32 `protobuf:"varint,10,opt,name=running_containers,json=runningContainers,proto3" json:"running_containers,omitempty"` + // Internal compose service names. Empty for non-compose services. + ComposeServices []string `protobuf:"bytes,11,rep,name=compose_services,json=composeServices,proto3" json:"compose_services,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetServiceStatusResponse) Reset() { @@ -701,6 +721,121 @@ func (x *GetServiceStatusResponse) GetDeployUrl() string { return "" } +func (x *GetServiceStatusResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *GetServiceStatusResponse) GetContainers() []*ContainerStatus { + if x != nil { + return x.Containers + } + return nil +} + +func (x *GetServiceStatusResponse) GetRunningContainers() int32 { + if x != nil { + return x.RunningContainers + } + return 0 +} + +func (x *GetServiceStatusResponse) GetComposeServices() []string { + if x != nil { + return x.ComposeServices + } + return nil +} + +type ContainerStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"` + // Docker state, e.g. running. + State string `protobuf:"bytes,4,opt,name=state,proto3" json:"state,omitempty"` + // Human readable status, e.g. "Up 2 hours". + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + // Unix seconds. + Created int64 `protobuf:"varint,6,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContainerStatus) Reset() { + *x = ContainerStatus{} + mi := &file_proto_paas_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContainerStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerStatus) ProtoMessage() {} + +func (x *ContainerStatus) ProtoReflect() protoreflect.Message { + mi := &file_proto_paas_proto_msgTypes[10] + 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 ContainerStatus.ProtoReflect.Descriptor instead. +func (*ContainerStatus) Descriptor() ([]byte, []int) { + return file_proto_paas_proto_rawDescGZIP(), []int{10} +} + +func (x *ContainerStatus) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ContainerStatus) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ContainerStatus) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *ContainerStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *ContainerStatus) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ContainerStatus) GetCreated() int64 { + if x != nil { + return x.Created + } + return 0 +} + type ListServicesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` @@ -710,7 +845,7 @@ type ListServicesRequest struct { func (x *ListServicesRequest) Reset() { *x = ListServicesRequest{} - mi := &file_proto_paas_proto_msgTypes[10] + mi := &file_proto_paas_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -722,7 +857,7 @@ func (x *ListServicesRequest) String() string { func (*ListServicesRequest) ProtoMessage() {} func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[10] + mi := &file_proto_paas_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -735,7 +870,7 @@ func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. func (*ListServicesRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{10} + return file_proto_paas_proto_rawDescGZIP(), []int{11} } func (x *ListServicesRequest) GetProject() string { @@ -754,7 +889,7 @@ type ListServicesResponse struct { func (x *ListServicesResponse) Reset() { *x = ListServicesResponse{} - mi := &file_proto_paas_proto_msgTypes[11] + mi := &file_proto_paas_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -766,7 +901,7 @@ func (x *ListServicesResponse) String() string { func (*ListServicesResponse) ProtoMessage() {} func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[11] + mi := &file_proto_paas_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -779,7 +914,7 @@ func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. func (*ListServicesResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{11} + return file_proto_paas_proto_rawDescGZIP(), []int{12} } func (x *ListServicesResponse) GetServices() []*ServiceInfo { @@ -801,7 +936,7 @@ type ServiceInfo struct { func (x *ServiceInfo) Reset() { *x = ServiceInfo{} - mi := &file_proto_paas_proto_msgTypes[12] + mi := &file_proto_paas_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -813,7 +948,7 @@ func (x *ServiceInfo) String() string { func (*ServiceInfo) ProtoMessage() {} func (x *ServiceInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[12] + mi := &file_proto_paas_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -826,7 +961,7 @@ func (x *ServiceInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceInfo.ProtoReflect.Descriptor instead. func (*ServiceInfo) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{12} + return file_proto_paas_proto_rawDescGZIP(), []int{13} } func (x *ServiceInfo) GetName() string { @@ -873,7 +1008,7 @@ type AddDomainRequest struct { func (x *AddDomainRequest) Reset() { *x = AddDomainRequest{} - mi := &file_proto_paas_proto_msgTypes[13] + mi := &file_proto_paas_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -885,7 +1020,7 @@ func (x *AddDomainRequest) String() string { func (*AddDomainRequest) ProtoMessage() {} func (x *AddDomainRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[13] + mi := &file_proto_paas_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -898,7 +1033,7 @@ func (x *AddDomainRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDomainRequest.ProtoReflect.Descriptor instead. func (*AddDomainRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{13} + return file_proto_paas_proto_rawDescGZIP(), []int{14} } func (x *AddDomainRequest) GetProject() string { @@ -960,7 +1095,7 @@ type AddDomainResponse struct { func (x *AddDomainResponse) Reset() { *x = AddDomainResponse{} - mi := &file_proto_paas_proto_msgTypes[14] + mi := &file_proto_paas_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -972,7 +1107,7 @@ func (x *AddDomainResponse) String() string { func (*AddDomainResponse) ProtoMessage() {} func (x *AddDomainResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[14] + mi := &file_proto_paas_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -985,7 +1120,7 @@ func (x *AddDomainResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDomainResponse.ProtoReflect.Descriptor instead. func (*AddDomainResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{14} + return file_proto_paas_proto_rawDescGZIP(), []int{15} } func (x *AddDomainResponse) GetDomainId() string { @@ -1015,7 +1150,7 @@ type RemoveDomainRequest struct { func (x *RemoveDomainRequest) Reset() { *x = RemoveDomainRequest{} - mi := &file_proto_paas_proto_msgTypes[15] + mi := &file_proto_paas_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1027,7 +1162,7 @@ func (x *RemoveDomainRequest) String() string { func (*RemoveDomainRequest) ProtoMessage() {} func (x *RemoveDomainRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[15] + mi := &file_proto_paas_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1040,7 +1175,7 @@ func (x *RemoveDomainRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveDomainRequest.ProtoReflect.Descriptor instead. func (*RemoveDomainRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{15} + return file_proto_paas_proto_rawDescGZIP(), []int{16} } func (x *RemoveDomainRequest) GetDomainId() string { @@ -1080,7 +1215,7 @@ type RemoveDomainResponse struct { func (x *RemoveDomainResponse) Reset() { *x = RemoveDomainResponse{} - mi := &file_proto_paas_proto_msgTypes[16] + mi := &file_proto_paas_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1092,7 +1227,7 @@ func (x *RemoveDomainResponse) String() string { func (*RemoveDomainResponse) ProtoMessage() {} func (x *RemoveDomainResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[16] + mi := &file_proto_paas_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1105,7 +1240,7 @@ func (x *RemoveDomainResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveDomainResponse.ProtoReflect.Descriptor instead. func (*RemoveDomainResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{16} + return file_proto_paas_proto_rawDescGZIP(), []int{17} } func (x *RemoveDomainResponse) GetSuccess() bool { @@ -1125,7 +1260,7 @@ type ListDomainsRequest struct { func (x *ListDomainsRequest) Reset() { *x = ListDomainsRequest{} - mi := &file_proto_paas_proto_msgTypes[17] + mi := &file_proto_paas_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1137,7 +1272,7 @@ func (x *ListDomainsRequest) String() string { func (*ListDomainsRequest) ProtoMessage() {} func (x *ListDomainsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[17] + mi := &file_proto_paas_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1150,7 +1285,7 @@ func (x *ListDomainsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListDomainsRequest.ProtoReflect.Descriptor instead. func (*ListDomainsRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{17} + return file_proto_paas_proto_rawDescGZIP(), []int{18} } func (x *ListDomainsRequest) GetProject() string { @@ -1176,7 +1311,7 @@ type ListDomainsResponse struct { func (x *ListDomainsResponse) Reset() { *x = ListDomainsResponse{} - mi := &file_proto_paas_proto_msgTypes[18] + mi := &file_proto_paas_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1188,7 +1323,7 @@ func (x *ListDomainsResponse) String() string { func (*ListDomainsResponse) ProtoMessage() {} func (x *ListDomainsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[18] + mi := &file_proto_paas_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1201,7 +1336,7 @@ func (x *ListDomainsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListDomainsResponse.ProtoReflect.Descriptor instead. func (*ListDomainsResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{18} + return file_proto_paas_proto_rawDescGZIP(), []int{19} } func (x *ListDomainsResponse) GetDomains() []*DomainInfo { @@ -1225,7 +1360,7 @@ type DomainInfo struct { func (x *DomainInfo) Reset() { *x = DomainInfo{} - mi := &file_proto_paas_proto_msgTypes[19] + mi := &file_proto_paas_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1237,7 +1372,7 @@ func (x *DomainInfo) String() string { func (*DomainInfo) ProtoMessage() {} func (x *DomainInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[19] + mi := &file_proto_paas_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1250,7 +1385,7 @@ func (x *DomainInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use DomainInfo.ProtoReflect.Descriptor instead. func (*DomainInfo) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{19} + return file_proto_paas_proto_rawDescGZIP(), []int{20} } func (x *DomainInfo) GetId() string { @@ -1307,7 +1442,7 @@ type UpdateEnvRequest struct { func (x *UpdateEnvRequest) Reset() { *x = UpdateEnvRequest{} - mi := &file_proto_paas_proto_msgTypes[20] + mi := &file_proto_paas_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1319,7 +1454,7 @@ func (x *UpdateEnvRequest) String() string { func (*UpdateEnvRequest) ProtoMessage() {} func (x *UpdateEnvRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[20] + mi := &file_proto_paas_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1332,7 +1467,7 @@ func (x *UpdateEnvRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateEnvRequest.ProtoReflect.Descriptor instead. func (*UpdateEnvRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{20} + return file_proto_paas_proto_rawDescGZIP(), []int{21} } func (x *UpdateEnvRequest) GetProject() string { @@ -1372,7 +1507,7 @@ type UpdateEnvResponse struct { func (x *UpdateEnvResponse) Reset() { *x = UpdateEnvResponse{} - mi := &file_proto_paas_proto_msgTypes[21] + mi := &file_proto_paas_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1384,7 +1519,7 @@ func (x *UpdateEnvResponse) String() string { func (*UpdateEnvResponse) ProtoMessage() {} func (x *UpdateEnvResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[21] + mi := &file_proto_paas_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1397,7 +1532,7 @@ func (x *UpdateEnvResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateEnvResponse.ProtoReflect.Descriptor instead. func (*UpdateEnvResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{21} + return file_proto_paas_proto_rawDescGZIP(), []int{22} } func (x *UpdateEnvResponse) GetSuccess() bool { @@ -1418,7 +1553,7 @@ type RestartServiceRequest struct { func (x *RestartServiceRequest) Reset() { *x = RestartServiceRequest{} - mi := &file_proto_paas_proto_msgTypes[22] + mi := &file_proto_paas_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1430,7 +1565,7 @@ func (x *RestartServiceRequest) String() string { func (*RestartServiceRequest) ProtoMessage() {} func (x *RestartServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[22] + mi := &file_proto_paas_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1443,7 +1578,7 @@ func (x *RestartServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartServiceRequest.ProtoReflect.Descriptor instead. func (*RestartServiceRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{22} + return file_proto_paas_proto_rawDescGZIP(), []int{23} } func (x *RestartServiceRequest) GetProject() string { @@ -1476,7 +1611,7 @@ type RestartServiceResponse struct { func (x *RestartServiceResponse) Reset() { *x = RestartServiceResponse{} - mi := &file_proto_paas_proto_msgTypes[23] + mi := &file_proto_paas_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1488,7 +1623,7 @@ func (x *RestartServiceResponse) String() string { func (*RestartServiceResponse) ProtoMessage() {} func (x *RestartServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[23] + mi := &file_proto_paas_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1501,7 +1636,7 @@ func (x *RestartServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartServiceResponse.ProtoReflect.Descriptor instead. func (*RestartServiceResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{23} + return file_proto_paas_proto_rawDescGZIP(), []int{24} } func (x *RestartServiceResponse) GetSuccess() bool { @@ -1522,7 +1657,7 @@ type StopServiceRequest struct { func (x *StopServiceRequest) Reset() { *x = StopServiceRequest{} - mi := &file_proto_paas_proto_msgTypes[24] + mi := &file_proto_paas_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1534,7 +1669,7 @@ func (x *StopServiceRequest) String() string { func (*StopServiceRequest) ProtoMessage() {} func (x *StopServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[24] + mi := &file_proto_paas_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1547,7 +1682,7 @@ func (x *StopServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopServiceRequest.ProtoReflect.Descriptor instead. func (*StopServiceRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{24} + return file_proto_paas_proto_rawDescGZIP(), []int{25} } func (x *StopServiceRequest) GetProject() string { @@ -1580,7 +1715,7 @@ type StopServiceResponse struct { func (x *StopServiceResponse) Reset() { *x = StopServiceResponse{} - mi := &file_proto_paas_proto_msgTypes[25] + mi := &file_proto_paas_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1592,7 +1727,7 @@ func (x *StopServiceResponse) String() string { func (*StopServiceResponse) ProtoMessage() {} func (x *StopServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[25] + mi := &file_proto_paas_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1605,7 +1740,7 @@ func (x *StopServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopServiceResponse.ProtoReflect.Descriptor instead. func (*StopServiceResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{25} + return file_proto_paas_proto_rawDescGZIP(), []int{26} } func (x *StopServiceResponse) GetSuccess() bool { @@ -1626,7 +1761,7 @@ type StartServiceRequest struct { func (x *StartServiceRequest) Reset() { *x = StartServiceRequest{} - mi := &file_proto_paas_proto_msgTypes[26] + mi := &file_proto_paas_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1638,7 +1773,7 @@ func (x *StartServiceRequest) String() string { func (*StartServiceRequest) ProtoMessage() {} func (x *StartServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[26] + mi := &file_proto_paas_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1651,7 +1786,7 @@ func (x *StartServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartServiceRequest.ProtoReflect.Descriptor instead. func (*StartServiceRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{26} + return file_proto_paas_proto_rawDescGZIP(), []int{27} } func (x *StartServiceRequest) GetProject() string { @@ -1684,7 +1819,7 @@ type StartServiceResponse struct { func (x *StartServiceResponse) Reset() { *x = StartServiceResponse{} - mi := &file_proto_paas_proto_msgTypes[27] + mi := &file_proto_paas_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1696,7 +1831,7 @@ func (x *StartServiceResponse) String() string { func (*StartServiceResponse) ProtoMessage() {} func (x *StartServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[27] + mi := &file_proto_paas_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1709,7 +1844,7 @@ func (x *StartServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartServiceResponse.ProtoReflect.Descriptor instead. func (*StartServiceResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{27} + return file_proto_paas_proto_rawDescGZIP(), []int{28} } func (x *StartServiceResponse) GetSuccess() bool { @@ -1728,15 +1863,25 @@ type GetLogsRequest struct { ServiceType string `protobuf:"bytes,3,opt,name=service_type,json=serviceType,proto3" json:"service_type,omitempty"` // Optional: restrict compose logs to one internal compose service. ComposeService string `protobuf:"bytes,4,opt,name=compose_service,json=composeService,proto3" json:"compose_service,omitempty"` - // Optional: max log lines to return. Defaults to 200. - Limit int32 `protobuf:"varint,5,opt,name=limit,proto3" json:"limit,omitempty"` + // Optional: max log lines to return. Defaults to 200, panel maximum is 1000. + Limit int32 `protobuf:"varint,5,opt,name=limit,proto3" json:"limit,omitempty"` + // Optional: stdout or stderr. + Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"` + // Optional: keep only these detected levels (info, warn, error, ...). + Levels []string `protobuf:"bytes,7,rep,name=levels,proto3" json:"levels,omitempty"` + // Optional: case-insensitive substring filter applied by the log store. + Search string `protobuf:"bytes,8,opt,name=search,proto3" json:"search,omitempty"` + // Optional window bounds. Unix nanoseconds or RFC3339, as accepted by Loki. + // Supplying start switches the query to forward (oldest first) direction. + Start string `protobuf:"bytes,9,opt,name=start,proto3" json:"start,omitempty"` + End string `protobuf:"bytes,10,opt,name=end,proto3" json:"end,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetLogsRequest) Reset() { *x = GetLogsRequest{} - mi := &file_proto_paas_proto_msgTypes[28] + mi := &file_proto_paas_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1748,7 +1893,7 @@ func (x *GetLogsRequest) String() string { func (*GetLogsRequest) ProtoMessage() {} func (x *GetLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[28] + mi := &file_proto_paas_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1761,7 +1906,7 @@ func (x *GetLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogsRequest.ProtoReflect.Descriptor instead. func (*GetLogsRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{28} + return file_proto_paas_proto_rawDescGZIP(), []int{29} } func (x *GetLogsRequest) GetProject() string { @@ -1799,6 +1944,41 @@ func (x *GetLogsRequest) GetLimit() int32 { return 0 } +func (x *GetLogsRequest) GetStream() string { + if x != nil { + return x.Stream + } + return "" +} + +func (x *GetLogsRequest) GetLevels() []string { + if x != nil { + return x.Levels + } + return nil +} + +func (x *GetLogsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +func (x *GetLogsRequest) GetStart() string { + if x != nil { + return x.Start + } + return "" +} + +func (x *GetLogsRequest) GetEnd() string { + if x != nil { + return x.End + } + return "" +} + type GetLogsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Logs string `protobuf:"bytes,1,opt,name=logs,proto3" json:"logs,omitempty"` @@ -1809,7 +1989,7 @@ type GetLogsResponse struct { func (x *GetLogsResponse) Reset() { *x = GetLogsResponse{} - mi := &file_proto_paas_proto_msgTypes[29] + mi := &file_proto_paas_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1821,7 +2001,7 @@ func (x *GetLogsResponse) String() string { func (*GetLogsResponse) ProtoMessage() {} func (x *GetLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[29] + mi := &file_proto_paas_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1834,7 +2014,7 @@ func (x *GetLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogsResponse.ProtoReflect.Descriptor instead. func (*GetLogsResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{29} + return file_proto_paas_proto_rawDescGZIP(), []int{30} } func (x *GetLogsResponse) GetLogs() string { @@ -1866,7 +2046,7 @@ type LogEntry struct { func (x *LogEntry) Reset() { *x = LogEntry{} - mi := &file_proto_paas_proto_msgTypes[30] + mi := &file_proto_paas_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1878,7 +2058,7 @@ func (x *LogEntry) String() string { func (*LogEntry) ProtoMessage() {} func (x *LogEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[30] + mi := &file_proto_paas_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1891,7 +2071,7 @@ func (x *LogEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use LogEntry.ProtoReflect.Descriptor instead. func (*LogEntry) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{30} + return file_proto_paas_proto_rawDescGZIP(), []int{31} } func (x *LogEntry) GetTimestamp() string { @@ -1934,7 +2114,7 @@ type ScaleServiceRequest struct { func (x *ScaleServiceRequest) Reset() { *x = ScaleServiceRequest{} - mi := &file_proto_paas_proto_msgTypes[31] + mi := &file_proto_paas_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1946,7 +2126,7 @@ func (x *ScaleServiceRequest) String() string { func (*ScaleServiceRequest) ProtoMessage() {} func (x *ScaleServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[31] + mi := &file_proto_paas_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1959,7 +2139,7 @@ func (x *ScaleServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ScaleServiceRequest.ProtoReflect.Descriptor instead. func (*ScaleServiceRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{31} + return file_proto_paas_proto_rawDescGZIP(), []int{32} } func (x *ScaleServiceRequest) GetProject() string { @@ -1999,7 +2179,7 @@ type ScaleServiceResponse struct { func (x *ScaleServiceResponse) Reset() { *x = ScaleServiceResponse{} - mi := &file_proto_paas_proto_msgTypes[32] + mi := &file_proto_paas_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2011,7 +2191,7 @@ func (x *ScaleServiceResponse) String() string { func (*ScaleServiceResponse) ProtoMessage() {} func (x *ScaleServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[32] + mi := &file_proto_paas_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2024,7 +2204,7 @@ func (x *ScaleServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ScaleServiceResponse.ProtoReflect.Descriptor instead. func (*ScaleServiceResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{32} + return file_proto_paas_proto_rawDescGZIP(), []int{33} } func (x *ScaleServiceResponse) GetSuccess() bool { @@ -2046,7 +2226,7 @@ type ResourceLimits struct { func (x *ResourceLimits) Reset() { *x = ResourceLimits{} - mi := &file_proto_paas_proto_msgTypes[33] + mi := &file_proto_paas_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2058,7 +2238,7 @@ func (x *ResourceLimits) String() string { func (*ResourceLimits) ProtoMessage() {} func (x *ResourceLimits) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[33] + mi := &file_proto_paas_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2071,7 +2251,7 @@ func (x *ResourceLimits) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead. func (*ResourceLimits) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{33} + return file_proto_paas_proto_rawDescGZIP(), []int{34} } func (x *ResourceLimits) GetCpuLimit() float64 { @@ -2114,7 +2294,7 @@ type UpdateResourcesRequest struct { func (x *UpdateResourcesRequest) Reset() { *x = UpdateResourcesRequest{} - mi := &file_proto_paas_proto_msgTypes[34] + mi := &file_proto_paas_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2126,7 +2306,7 @@ func (x *UpdateResourcesRequest) String() string { func (*UpdateResourcesRequest) ProtoMessage() {} func (x *UpdateResourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[34] + mi := &file_proto_paas_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2139,7 +2319,7 @@ func (x *UpdateResourcesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateResourcesRequest.ProtoReflect.Descriptor instead. func (*UpdateResourcesRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{34} + return file_proto_paas_proto_rawDescGZIP(), []int{35} } func (x *UpdateResourcesRequest) GetProject() string { @@ -2179,7 +2359,7 @@ type UpdateResourcesResponse struct { func (x *UpdateResourcesResponse) Reset() { *x = UpdateResourcesResponse{} - mi := &file_proto_paas_proto_msgTypes[35] + mi := &file_proto_paas_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2191,7 +2371,7 @@ func (x *UpdateResourcesResponse) String() string { func (*UpdateResourcesResponse) ProtoMessage() {} func (x *UpdateResourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[35] + mi := &file_proto_paas_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2204,7 +2384,7 @@ func (x *UpdateResourcesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateResourcesResponse.ProtoReflect.Descriptor instead. func (*UpdateResourcesResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{35} + return file_proto_paas_proto_rawDescGZIP(), []int{36} } func (x *UpdateResourcesResponse) GetSuccess() bool { @@ -2228,7 +2408,7 @@ type UpdateDeployRequest struct { func (x *UpdateDeployRequest) Reset() { *x = UpdateDeployRequest{} - mi := &file_proto_paas_proto_msgTypes[36] + mi := &file_proto_paas_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2240,7 +2420,7 @@ func (x *UpdateDeployRequest) String() string { func (*UpdateDeployRequest) ProtoMessage() {} func (x *UpdateDeployRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[36] + mi := &file_proto_paas_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2253,7 +2433,7 @@ func (x *UpdateDeployRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateDeployRequest.ProtoReflect.Descriptor instead. func (*UpdateDeployRequest) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{36} + return file_proto_paas_proto_rawDescGZIP(), []int{37} } func (x *UpdateDeployRequest) GetProject() string { @@ -2307,7 +2487,7 @@ type UpdateDeployResponse struct { func (x *UpdateDeployResponse) Reset() { *x = UpdateDeployResponse{} - mi := &file_proto_paas_proto_msgTypes[37] + mi := &file_proto_paas_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2319,7 +2499,7 @@ func (x *UpdateDeployResponse) String() string { func (*UpdateDeployResponse) ProtoMessage() {} func (x *UpdateDeployResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_paas_proto_msgTypes[37] + mi := &file_proto_paas_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2332,7 +2512,7 @@ func (x *UpdateDeployResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateDeployResponse.ProtoReflect.Descriptor instead. func (*UpdateDeployResponse) Descriptor() ([]byte, []int) { - return file_proto_paas_proto_rawDescGZIP(), []int{37} + return file_proto_paas_proto_rawDescGZIP(), []int{38} } func (x *UpdateDeployResponse) GetSuccess() bool { @@ -2393,11 +2573,13 @@ const file_proto_paas_proto_rawDesc = "" + "\aservice\x18\x02 \x01(\tR\aservice\x12!\n" + "\fservice_type\x18\x03 \x01(\tR\vserviceType\"2\n" + "\x16DestroyServiceResponse\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\"p\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\"\x91\x01\n" + "\x17GetServiceStatusRequest\x12\x18\n" + "\aproject\x18\x01 \x01(\tR\aproject\x12\x18\n" + "\aservice\x18\x02 \x01(\tR\aservice\x12!\n" + - "\fservice_type\x18\x03 \x01(\tR\vserviceType\"\xbd\x01\n" + + "\fservice_type\x18\x03 \x01(\tR\vserviceType\x12\x1f\n" + + "\vinclude_env\x18\x04 \x01(\bR\n" + + "includeEnv\"\xe6\x02\n" + "\x18GetServiceStatusResponse\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12\x18\n" + @@ -2406,7 +2588,21 @@ const file_proto_paas_proto_rawDesc = "" + "\x03env\x18\x05 \x01(\tR\x03env\x12\x18\n" + "\adomains\x18\x06 \x03(\tR\adomains\x12\x1d\n" + "\n" + - "deploy_url\x18\a \x01(\tR\tdeployUrl\"/\n" + + "deploy_url\x18\a \x01(\tR\tdeployUrl\x12\x16\n" + + "\x06status\x18\b \x01(\tR\x06status\x125\n" + + "\n" + + "containers\x18\t \x03(\v2\x15.paas.ContainerStatusR\n" + + "containers\x12-\n" + + "\x12running_containers\x18\n" + + " \x01(\x05R\x11runningContainers\x12)\n" + + "\x10compose_services\x18\v \x03(\tR\x0fcomposeServices\"\x93\x01\n" + + "\x0fContainerStatus\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + + "\x05image\x18\x03 \x01(\tR\x05image\x12\x14\n" + + "\x05state\x18\x04 \x01(\tR\x05state\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\x12\x18\n" + + "\acreated\x18\x06 \x01(\x03R\acreated\"/\n" + "\x13ListServicesRequest\x12\x18\n" + "\aproject\x18\x01 \x01(\tR\aproject\"E\n" + "\x14ListServicesResponse\x12-\n" + @@ -2474,13 +2670,19 @@ const file_proto_paas_proto_rawDesc = "" + "\aservice\x18\x02 \x01(\tR\aservice\x12!\n" + "\fservice_type\x18\x03 \x01(\tR\vserviceType\"0\n" + "\x14StartServiceResponse\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\"\xa6\x01\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\"\x96\x02\n" + "\x0eGetLogsRequest\x12\x18\n" + "\aproject\x18\x01 \x01(\tR\aproject\x12\x18\n" + "\aservice\x18\x02 \x01(\tR\aservice\x12!\n" + "\fservice_type\x18\x03 \x01(\tR\vserviceType\x12'\n" + "\x0fcompose_service\x18\x04 \x01(\tR\x0ecomposeService\x12\x14\n" + - "\x05limit\x18\x05 \x01(\x05R\x05limit\"O\n" + + "\x05limit\x18\x05 \x01(\x05R\x05limit\x12\x16\n" + + "\x06stream\x18\x06 \x01(\tR\x06stream\x12\x16\n" + + "\x06levels\x18\a \x03(\tR\x06levels\x12\x16\n" + + "\x06search\x18\b \x01(\tR\x06search\x12\x14\n" + + "\x05start\x18\t \x01(\tR\x05start\x12\x10\n" + + "\x03end\x18\n" + + " \x01(\tR\x03end\"O\n" + "\x0fGetLogsResponse\x12\x12\n" + "\x04logs\x18\x01 \x01(\tR\x04logs\x12(\n" + "\aentries\x18\x02 \x03(\v2\x0e.paas.LogEntryR\aentries\"j\n" + @@ -2547,7 +2749,7 @@ func file_proto_paas_proto_rawDescGZIP() []byte { return file_proto_paas_proto_rawDescData } -var file_proto_paas_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_proto_paas_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_proto_paas_proto_goTypes = []any{ (*DeployContainerRequest)(nil), // 0: paas.DeployContainerRequest (*DeployContainerResponse)(nil), // 1: paas.DeployContainerResponse @@ -2559,86 +2761,88 @@ var file_proto_paas_proto_goTypes = []any{ (*DestroyServiceResponse)(nil), // 7: paas.DestroyServiceResponse (*GetServiceStatusRequest)(nil), // 8: paas.GetServiceStatusRequest (*GetServiceStatusResponse)(nil), // 9: paas.GetServiceStatusResponse - (*ListServicesRequest)(nil), // 10: paas.ListServicesRequest - (*ListServicesResponse)(nil), // 11: paas.ListServicesResponse - (*ServiceInfo)(nil), // 12: paas.ServiceInfo - (*AddDomainRequest)(nil), // 13: paas.AddDomainRequest - (*AddDomainResponse)(nil), // 14: paas.AddDomainResponse - (*RemoveDomainRequest)(nil), // 15: paas.RemoveDomainRequest - (*RemoveDomainResponse)(nil), // 16: paas.RemoveDomainResponse - (*ListDomainsRequest)(nil), // 17: paas.ListDomainsRequest - (*ListDomainsResponse)(nil), // 18: paas.ListDomainsResponse - (*DomainInfo)(nil), // 19: paas.DomainInfo - (*UpdateEnvRequest)(nil), // 20: paas.UpdateEnvRequest - (*UpdateEnvResponse)(nil), // 21: paas.UpdateEnvResponse - (*RestartServiceRequest)(nil), // 22: paas.RestartServiceRequest - (*RestartServiceResponse)(nil), // 23: paas.RestartServiceResponse - (*StopServiceRequest)(nil), // 24: paas.StopServiceRequest - (*StopServiceResponse)(nil), // 25: paas.StopServiceResponse - (*StartServiceRequest)(nil), // 26: paas.StartServiceRequest - (*StartServiceResponse)(nil), // 27: paas.StartServiceResponse - (*GetLogsRequest)(nil), // 28: paas.GetLogsRequest - (*GetLogsResponse)(nil), // 29: paas.GetLogsResponse - (*LogEntry)(nil), // 30: paas.LogEntry - (*ScaleServiceRequest)(nil), // 31: paas.ScaleServiceRequest - (*ScaleServiceResponse)(nil), // 32: paas.ScaleServiceResponse - (*ResourceLimits)(nil), // 33: paas.ResourceLimits - (*UpdateResourcesRequest)(nil), // 34: paas.UpdateResourcesRequest - (*UpdateResourcesResponse)(nil), // 35: paas.UpdateResourcesResponse - (*UpdateDeployRequest)(nil), // 36: paas.UpdateDeployRequest - (*UpdateDeployResponse)(nil), // 37: paas.UpdateDeployResponse - nil, // 38: paas.DeployContainerRequest.EnvEntry - nil, // 39: paas.DeployComposeRequest.EnvEntry - nil, // 40: paas.UpdateEnvRequest.EnvEntry + (*ContainerStatus)(nil), // 10: paas.ContainerStatus + (*ListServicesRequest)(nil), // 11: paas.ListServicesRequest + (*ListServicesResponse)(nil), // 12: paas.ListServicesResponse + (*ServiceInfo)(nil), // 13: paas.ServiceInfo + (*AddDomainRequest)(nil), // 14: paas.AddDomainRequest + (*AddDomainResponse)(nil), // 15: paas.AddDomainResponse + (*RemoveDomainRequest)(nil), // 16: paas.RemoveDomainRequest + (*RemoveDomainResponse)(nil), // 17: paas.RemoveDomainResponse + (*ListDomainsRequest)(nil), // 18: paas.ListDomainsRequest + (*ListDomainsResponse)(nil), // 19: paas.ListDomainsResponse + (*DomainInfo)(nil), // 20: paas.DomainInfo + (*UpdateEnvRequest)(nil), // 21: paas.UpdateEnvRequest + (*UpdateEnvResponse)(nil), // 22: paas.UpdateEnvResponse + (*RestartServiceRequest)(nil), // 23: paas.RestartServiceRequest + (*RestartServiceResponse)(nil), // 24: paas.RestartServiceResponse + (*StopServiceRequest)(nil), // 25: paas.StopServiceRequest + (*StopServiceResponse)(nil), // 26: paas.StopServiceResponse + (*StartServiceRequest)(nil), // 27: paas.StartServiceRequest + (*StartServiceResponse)(nil), // 28: paas.StartServiceResponse + (*GetLogsRequest)(nil), // 29: paas.GetLogsRequest + (*GetLogsResponse)(nil), // 30: paas.GetLogsResponse + (*LogEntry)(nil), // 31: paas.LogEntry + (*ScaleServiceRequest)(nil), // 32: paas.ScaleServiceRequest + (*ScaleServiceResponse)(nil), // 33: paas.ScaleServiceResponse + (*ResourceLimits)(nil), // 34: paas.ResourceLimits + (*UpdateResourcesRequest)(nil), // 35: paas.UpdateResourcesRequest + (*UpdateResourcesResponse)(nil), // 36: paas.UpdateResourcesResponse + (*UpdateDeployRequest)(nil), // 37: paas.UpdateDeployRequest + (*UpdateDeployResponse)(nil), // 38: paas.UpdateDeployResponse + nil, // 39: paas.DeployContainerRequest.EnvEntry + nil, // 40: paas.DeployComposeRequest.EnvEntry + nil, // 41: paas.UpdateEnvRequest.EnvEntry } var file_proto_paas_proto_depIdxs = []int32{ - 38, // 0: paas.DeployContainerRequest.env:type_name -> paas.DeployContainerRequest.EnvEntry - 33, // 1: paas.DeployContainerRequest.resources:type_name -> paas.ResourceLimits - 39, // 2: paas.DeployComposeRequest.env:type_name -> paas.DeployComposeRequest.EnvEntry + 39, // 0: paas.DeployContainerRequest.env:type_name -> paas.DeployContainerRequest.EnvEntry + 34, // 1: paas.DeployContainerRequest.resources:type_name -> paas.ResourceLimits + 40, // 2: paas.DeployComposeRequest.env:type_name -> paas.DeployComposeRequest.EnvEntry 3, // 3: paas.DeployComposeRequest.domains:type_name -> paas.ComposeDomain 5, // 4: paas.DeployComposeResponse.domains:type_name -> paas.DomainMapping - 12, // 5: paas.ListServicesResponse.services:type_name -> paas.ServiceInfo - 19, // 6: paas.ListDomainsResponse.domains:type_name -> paas.DomainInfo - 40, // 7: paas.UpdateEnvRequest.env:type_name -> paas.UpdateEnvRequest.EnvEntry - 30, // 8: paas.GetLogsResponse.entries:type_name -> paas.LogEntry - 33, // 9: paas.UpdateResourcesRequest.resources:type_name -> paas.ResourceLimits - 0, // 10: paas.PaaS.DeployContainer:input_type -> paas.DeployContainerRequest - 2, // 11: paas.PaaS.DeployCompose:input_type -> paas.DeployComposeRequest - 6, // 12: paas.PaaS.DestroyService:input_type -> paas.DestroyServiceRequest - 8, // 13: paas.PaaS.GetServiceStatus:input_type -> paas.GetServiceStatusRequest - 10, // 14: paas.PaaS.ListServices:input_type -> paas.ListServicesRequest - 13, // 15: paas.PaaS.AddDomain:input_type -> paas.AddDomainRequest - 15, // 16: paas.PaaS.RemoveDomain:input_type -> paas.RemoveDomainRequest - 17, // 17: paas.PaaS.ListDomains:input_type -> paas.ListDomainsRequest - 20, // 18: paas.PaaS.UpdateEnv:input_type -> paas.UpdateEnvRequest - 22, // 19: paas.PaaS.RestartService:input_type -> paas.RestartServiceRequest - 24, // 20: paas.PaaS.StopService:input_type -> paas.StopServiceRequest - 26, // 21: paas.PaaS.StartService:input_type -> paas.StartServiceRequest - 28, // 22: paas.PaaS.GetLogs:input_type -> paas.GetLogsRequest - 31, // 23: paas.PaaS.ScaleService:input_type -> paas.ScaleServiceRequest - 34, // 24: paas.PaaS.UpdateResources:input_type -> paas.UpdateResourcesRequest - 36, // 25: paas.PaaS.UpdateDeploy:input_type -> paas.UpdateDeployRequest - 1, // 26: paas.PaaS.DeployContainer:output_type -> paas.DeployContainerResponse - 4, // 27: paas.PaaS.DeployCompose:output_type -> paas.DeployComposeResponse - 7, // 28: paas.PaaS.DestroyService:output_type -> paas.DestroyServiceResponse - 9, // 29: paas.PaaS.GetServiceStatus:output_type -> paas.GetServiceStatusResponse - 11, // 30: paas.PaaS.ListServices:output_type -> paas.ListServicesResponse - 14, // 31: paas.PaaS.AddDomain:output_type -> paas.AddDomainResponse - 16, // 32: paas.PaaS.RemoveDomain:output_type -> paas.RemoveDomainResponse - 18, // 33: paas.PaaS.ListDomains:output_type -> paas.ListDomainsResponse - 21, // 34: paas.PaaS.UpdateEnv:output_type -> paas.UpdateEnvResponse - 23, // 35: paas.PaaS.RestartService:output_type -> paas.RestartServiceResponse - 25, // 36: paas.PaaS.StopService:output_type -> paas.StopServiceResponse - 27, // 37: paas.PaaS.StartService:output_type -> paas.StartServiceResponse - 29, // 38: paas.PaaS.GetLogs:output_type -> paas.GetLogsResponse - 32, // 39: paas.PaaS.ScaleService:output_type -> paas.ScaleServiceResponse - 35, // 40: paas.PaaS.UpdateResources:output_type -> paas.UpdateResourcesResponse - 37, // 41: paas.PaaS.UpdateDeploy:output_type -> paas.UpdateDeployResponse - 26, // [26:42] is the sub-list for method output_type - 10, // [10:26] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 10, // 5: paas.GetServiceStatusResponse.containers:type_name -> paas.ContainerStatus + 13, // 6: paas.ListServicesResponse.services:type_name -> paas.ServiceInfo + 20, // 7: paas.ListDomainsResponse.domains:type_name -> paas.DomainInfo + 41, // 8: paas.UpdateEnvRequest.env:type_name -> paas.UpdateEnvRequest.EnvEntry + 31, // 9: paas.GetLogsResponse.entries:type_name -> paas.LogEntry + 34, // 10: paas.UpdateResourcesRequest.resources:type_name -> paas.ResourceLimits + 0, // 11: paas.PaaS.DeployContainer:input_type -> paas.DeployContainerRequest + 2, // 12: paas.PaaS.DeployCompose:input_type -> paas.DeployComposeRequest + 6, // 13: paas.PaaS.DestroyService:input_type -> paas.DestroyServiceRequest + 8, // 14: paas.PaaS.GetServiceStatus:input_type -> paas.GetServiceStatusRequest + 11, // 15: paas.PaaS.ListServices:input_type -> paas.ListServicesRequest + 14, // 16: paas.PaaS.AddDomain:input_type -> paas.AddDomainRequest + 16, // 17: paas.PaaS.RemoveDomain:input_type -> paas.RemoveDomainRequest + 18, // 18: paas.PaaS.ListDomains:input_type -> paas.ListDomainsRequest + 21, // 19: paas.PaaS.UpdateEnv:input_type -> paas.UpdateEnvRequest + 23, // 20: paas.PaaS.RestartService:input_type -> paas.RestartServiceRequest + 25, // 21: paas.PaaS.StopService:input_type -> paas.StopServiceRequest + 27, // 22: paas.PaaS.StartService:input_type -> paas.StartServiceRequest + 29, // 23: paas.PaaS.GetLogs:input_type -> paas.GetLogsRequest + 32, // 24: paas.PaaS.ScaleService:input_type -> paas.ScaleServiceRequest + 35, // 25: paas.PaaS.UpdateResources:input_type -> paas.UpdateResourcesRequest + 37, // 26: paas.PaaS.UpdateDeploy:input_type -> paas.UpdateDeployRequest + 1, // 27: paas.PaaS.DeployContainer:output_type -> paas.DeployContainerResponse + 4, // 28: paas.PaaS.DeployCompose:output_type -> paas.DeployComposeResponse + 7, // 29: paas.PaaS.DestroyService:output_type -> paas.DestroyServiceResponse + 9, // 30: paas.PaaS.GetServiceStatus:output_type -> paas.GetServiceStatusResponse + 12, // 31: paas.PaaS.ListServices:output_type -> paas.ListServicesResponse + 15, // 32: paas.PaaS.AddDomain:output_type -> paas.AddDomainResponse + 17, // 33: paas.PaaS.RemoveDomain:output_type -> paas.RemoveDomainResponse + 19, // 34: paas.PaaS.ListDomains:output_type -> paas.ListDomainsResponse + 22, // 35: paas.PaaS.UpdateEnv:output_type -> paas.UpdateEnvResponse + 24, // 36: paas.PaaS.RestartService:output_type -> paas.RestartServiceResponse + 26, // 37: paas.PaaS.StopService:output_type -> paas.StopServiceResponse + 28, // 38: paas.PaaS.StartService:output_type -> paas.StartServiceResponse + 30, // 39: paas.PaaS.GetLogs:output_type -> paas.GetLogsResponse + 33, // 40: paas.PaaS.ScaleService:output_type -> paas.ScaleServiceResponse + 36, // 41: paas.PaaS.UpdateResources:output_type -> paas.UpdateResourcesResponse + 38, // 42: paas.PaaS.UpdateDeploy:output_type -> paas.UpdateDeployResponse + 27, // [27:43] is the sub-list for method output_type + 11, // [11:27] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_proto_paas_proto_init() } @@ -2652,7 +2856,7 @@ func file_proto_paas_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_paas_proto_rawDesc), len(file_proto_paas_proto_rawDesc)), NumEnums: 0, - NumMessages: 41, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/paas.proto b/proto/paas.proto index 6bc7c13..5b34788 100644 --- a/proto/paas.proto +++ b/proto/paas.proto @@ -121,6 +121,10 @@ message GetServiceStatusRequest { string project = 1; string service = 2; string service_type = 3; + // Return environment variable values in clear text. Off by default: the + // panel stores secrets in env, and every gRPC token can read every project. + // With this unset, values are replaced by and only keys are shown. + bool include_env = 4; } message GetServiceStatusResponse { @@ -128,9 +132,30 @@ message GetServiceStatusResponse { string type = 2; bool enabled = 3; string image = 4; + // Saved env content. Values are redacted unless the request sets include_env. string env = 5; repeated string domains = 6; string deploy_url = 7; + // Runtime state derived from the panel's Docker view: + // running, stopped, or unknown when the panel refuses the container query. + string status = 8; + // Running containers backing this service. + repeated ContainerStatus containers = 9; + int32 running_containers = 10; + // Internal compose service names. Empty for non-compose services. + repeated string compose_services = 11; +} + +message ContainerStatus { + string id = 1; + string name = 2; + string image = 3; + // Docker state, e.g. running. + string state = 4; + // Human readable status, e.g. "Up 2 hours". + string status = 5; + // Unix seconds. + int64 created = 6; } // --- List --- @@ -253,8 +278,18 @@ message GetLogsRequest { string service_type = 3; // Optional: restrict compose logs to one internal compose service. string compose_service = 4; - // Optional: max log lines to return. Defaults to 200. + // Optional: max log lines to return. Defaults to 200, panel maximum is 1000. int32 limit = 5; + // Optional: stdout or stderr. + string stream = 6; + // Optional: keep only these detected levels (info, warn, error, ...). + repeated string levels = 7; + // Optional: case-insensitive substring filter applied by the log store. + string search = 8; + // Optional window bounds. Unix nanoseconds or RFC3339, as accepted by Loki. + // Supplying start switches the query to forward (oldest first) direction. + string start = 9; + string end = 10; } message GetLogsResponse { diff --git a/scripts/extract-panel-surface.py b/scripts/extract-panel-surface.py new file mode 100644 index 0000000..f88e6ca --- /dev/null +++ b/scripts/extract-panel-surface.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""Extract the Easypanel backend route surface from a bundled backend.js. + +Usage: epsurface.py +""" +import json +import re +import sys + + +def load(path): + return open(path, encoding="utf8", errors="replace").read() + + +def match_block(src, open_idx): + """Brace-match starting at src[open_idx] which must be one of {[(""" + pairs = {"{": "}", "[": "]", "(": ")"} + assert src[open_idx] in pairs, src[open_idx] + depth = 0 + instr = None + esc = False + i = open_idx + while i < len(src): + c = src[i] + if instr: + if esc: + esc = False + elif c == "\\": + esc = True + elif c == instr: + instr = None + else: + if c in "\"'`": + instr = c + elif c in "{[(": + depth += 1 + elif c in "}])": + depth -= 1 + if depth == 0: + return src[open_idx : i + 1], i + 1 + i += 1 + raise ValueError("unbalanced") + + +def find_var_object(src, name): + m = re.search(r"(? 3: + return expr[:200] + m = re.match(r"^([A-Za-z_$][\w$]*)$", expr) + if m: + name = m.group(1) + mm = re.search(r"(? var; walk it + for key, val in top_level_entries(root): + val = val.strip() + if val.startswith("{"): + walk([key], val) + else: + m = re.match(r"^([A-Za-z_$][\w$]*)$", val) + sub = find_var_object(src, m.group(1)) if m else None + if sub: + walk([key], sub) + else: + out["routers"].setdefault(key, []) + + # For every router path, re-resolve to grab per-procedure metadata + def procs_of(path): + # walk down from root following path + cur = root + for seg in path.split("."): + ent = dict(top_level_entries(cur)) + v = ent[seg].strip() + if v.startswith("{"): + cur = v + else: + cur = find_var_object(src, re.match(r"^([A-Za-z_$][\w$]*)$", v).group(1)) + return top_level_entries(cur) + + for rpath in list(out["routers"].keys()): + try: + entries = procs_of(rpath) + except Exception as e: # noqa: BLE001 + print(f"skip {rpath}: {e}", file=sys.stderr) + continue + for name, body in entries: + full = f"{rpath}.{name}" + meta = {} + om = re.search(r'operationId:"([^"]+)"', body) + km = re.search(r'kind:"([^"]+)"', body) + meta["operationId"] = om.group(1) if om else None + meta["kind"] = km.group(1) if km else None + meta["destructive"] = "destructive:!0" in body + im = re.search(r"\.input\(", body) + if im: + inner, _ = match_block(body, body.index("(", im.start() + 6)) + expr = inner[1:-1] + resolved = resolve_schema(src, expr) + meta["inputExpr"] = re.sub(r"\s+", "", expr)[:120] + meta["fields"] = schema_fields(resolved) or schema_fields(expr) + else: + meta["inputExpr"] = None + meta["fields"] = None + out["procedures"][full] = meta + + json.dump(out, open(sys.argv[2], "w"), indent=1, sort_keys=True) + print( + f"{sys.argv[1]}: routers={len(out['routers'])} procedures={len(out['procedures'])}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/panel-surface.sh b/scripts/panel-surface.sh new file mode 100755 index 0000000..024ca38 --- /dev/null +++ b/scripts/panel-surface.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Regenerate the pinned Easypanel route surfaces in +# internal/easypanel/testdata/ from the official panel images. +# +# The panel ships a single bundled backend.js; this script extracts the oRPC +# router tree (router.procedure paths plus input field names) so the Go tests +# can assert the routes this project calls exist in every supported release. +# +# Usage: scripts/panel-surface.sh [version ...] +# scripts/panel-surface.sh 2.32.2 2.33.0 2.33.1 +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TESTDATA="$REPO_ROOT/internal/easypanel/testdata" +EXTRACT="$REPO_ROOT/scripts/extract-panel-surface.py" +IMAGE="${EASYPANEL_IMAGE:-easypanel/easypanel}" + +# Default set must match MinSupportedVersion..MaxTestedVersion in version.go. +VERSIONS=("$@") +if [ ${#VERSIONS[@]} -eq 0 ]; then + VERSIONS=(2.32.2 2.33.0 2.33.1) +fi + +mkdir -p "$TESTDATA" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +for version in "${VERSIONS[@]}"; do + echo "==> $IMAGE:$version" + docker pull "$IMAGE:$version" >/dev/null + + container="ep-surface-${version//./-}-$$" + docker create --name "$container" "$IMAGE:$version" >/dev/null + # Only app/backend.js is needed; the rest of the image is ~900MB. + docker export "$container" | tar -x -C "$WORK" app/backend.js app/package.json + docker rm "$container" >/dev/null + + reported="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$WORK/app/package.json")" + if [ "$reported" != "$version" ]; then + echo " note: image tag $version reports package version $reported" >&2 + fi + + python3 "$EXTRACT" "$WORK/app/backend.js" "$WORK/full-$version.json" + python3 - "$WORK/full-$version.json" "$TESTDATA/panel-surface-$version.json" "$version" <<'PY' +import json +import sys + +full = json.load(open(sys.argv[1])) +procedures = {} +for path, meta in full["procedures"].items(): + fields = meta.get("fields") or {} + procedures[path] = { + "kind": meta.get("kind"), + "operationId": meta.get("operationId"), + "required": sorted(n for n, f in fields.items() if not f["optional"]), + "optional": sorted(n for n, f in fields.items() if f["optional"]), + } +with open(sys.argv[2], "w") as fh: + json.dump({"panelVersion": sys.argv[3], "procedures": procedures}, fh, + sort_keys=True, separators=(",", ":")) + fh.write("\n") +print(f" wrote {sys.argv[2]} ({len(procedures)} procedures)") +PY + rm -f "$WORK/app/backend.js" +done + +echo +echo "Now run: go test ./internal/easypanel/ -run TestRequiredRoutes -v"