Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,39 @@
All notable changes to this project are documented here. Versions follow the
`vMAJOR.MINOR.PATCH` git tags that drive the release pipeline.

## v0.2.0

Supported Easypanel versions: **2.32.0 .. 2.33.1**, unchanged from v0.1.0.

### Features

- New RPC `UpdateComposeSource` replaces the compose content of an existing
compose service and redeploys it. `DeployCompose` calls the panel's
`createService` first, so it fails with `Service already exists.` on a service
that is already there, leaving no way to update a live stack over gRPC. The new
RPC runs the same gate as `DeployCompose` (invalid YAML and `network_mode: host`
rejected, host `ports:` stripped, `expose:` kept), maps a missing or
wrong-typed service to `NotFound`, and accepts `skip_deploy` to store content
without deploying so several edits can be batched into one deploy.

### Fixes

- Stop sending `composeFile` to `services.compose.updateSourceInline`. The panel's
schema accepts only `projectName`, `serviceName` and `content`, and silently
dropped the extra field. Affected `DeployCompose` and the CLI.
- `services set-source-inline` now takes 3 arguments
(`[project] [service] [compose-content]`). The old 4-argument form is still
accepted and warns that the `compose-file` argument is ignored.

### Internal

- `easypanel.IsNotFound` classifies the panel's `NOT_FOUND`, which it returns both
for a missing service and for one whose type does not match the route namespace.
- Unit tests cover the new RPC's field payload, sanitizing, `skip_deploy`,
`NotFound` mapping, input validation and deploy failures. New live test
`TestGRPC_UpdateComposeSourceE2E` also asserts that `DeployCompose` still
refuses an existing service, which is the gap this RPC fills.

## v0.1.0

Supported Easypanel versions: **2.32.0 .. 2.33.1**, both bounds backed by pinned
Expand Down
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A Go toolkit for managing [Easypanel](https://easypanel.io) deployments — both
┌──────────────────▼──────────────────────────────────┐
│ gRPC PaaS Server (cmd/grpc-server) │
│ • Auth interceptor (constant-time token compare) │
│ • 16 RPC methods │
│ • 17 RPC methods │
│ • Compose port sanitization │
│ • Auto-domain provisioning │
└──────────────────┬──────────────────────────────────┘
Expand All @@ -31,7 +31,7 @@ A Go toolkit for managing [Easypanel](https://easypanel.io) deployments — both
- **CLI** — Direct Easypanel tRPC control (projects, services, domains, users, certs, settings)
- **gRPC Server** — Multi-tenant PaaS adapter with bearer token authentication
- **Container Deployments** — Deploy Docker images with auto-assigned domains
- **Compose Stacks** — Deploy docker-compose with per-service domain routing (ports auto-stripped for security)
- **Compose Stacks** — Deploy docker-compose with per-service domain routing (ports auto-stripped for security), and replace the content of a live stack in place
- **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
Expand Down Expand Up @@ -118,7 +118,7 @@ export EASYPANEL_ENDPOINT EASYPANEL_API_KEY
./deploy-everything services set-dockerfile <project> <service> '<Dockerfile content>'

# Compose sources (save, then automatically redeploy)
./deploy-everything services set-source-inline <project> <service> docker-compose.yml '<compose content>'
./deploy-everything services set-source-inline <project> <service> '<compose content>'
./deploy-everything services set-source-git-compose <project> <service> <repo> <ref> docker-compose.yml [--root-path /]

# Logs
Expand Down Expand Up @@ -180,7 +180,8 @@ Tokens are configured via `GRPC_AUTH_TOKENS` (comma-separated). Tokens grant pan
| Method | Description |
|--------|-------------|
| `DeployContainer` | Deploy Docker image → create service + set image + env + deploy + auto-domain |
| `DeployCompose` | Deploy docker-compose stack (strips `ports:`, routes via `composeService`) |
| `DeployCompose` | Create and deploy a docker-compose stack (strips `ports:`, routes via `composeService`). Fails if the service already exists |
| `UpdateComposeSource` | Replace the compose content of an existing compose service, then redeploy. Set `skip_deploy` to store without deploying |
| `DestroyService` | Remove service and all associated domains |
| `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 |
Expand Down Expand Up @@ -233,6 +234,33 @@ grpcurl -plaintext -H "authorization: your-token" -d '{
}' localhost:50051 paas.PaaS/DeployCompose
```

#### Update an existing compose stack

`DeployCompose` creates: it calls the panel's `createService` first, so it fails with
`create compose: [BAD_REQUEST] Service already exists.` on a service that is already
there. Use `UpdateComposeSource` to replace the content of a live stack.

```bash
grpcurl -plaintext -H "authorization: your-token" -d '{
"project": "myproject",
"service": "stack",
"composeContent": "services:\n web:\n image: wordpress:6.5\n expose:\n - \"80\"\n"
}' localhost:50051 paas.PaaS/UpdateComposeSource
```

```json
{
"deployed": true,
"status": "deploying"
}
```

The content fully replaces what was stored, and goes through the same gate as
`DeployCompose`: invalid YAML and `network_mode: host` are rejected, host-published
`ports:` are stripped, `expose:` is kept. Set `"skipDeploy": true` to store the
content without redeploying, then deploy once after a batch of edits. A service that
does not exist, or exists under another type, returns `NotFound`.

#### Add a custom domain

```bash
Expand Down Expand Up @@ -506,11 +534,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.1.0
ghcr.io/igun997/deploy-everything:0.2.0

# Run a CLI command
docker run --rm --env-file .env \
ghcr.io/igun997/deploy-everything-cli:0.1.0 projects list
ghcr.io/igun997/deploy-everything-cli:0.2.0 projects list

# Bleeding edge (current master)
docker run --rm --env-file .env \
Expand All @@ -531,13 +559,13 @@ docker run --rm --env-file .env \
| Git event | Image tags |
|-----------|-----------|
| push/merge to `master` | `dev`, `dev-<short-sha>` |
| tag `v0.1.0` | `0.1.0`, `0.1`, `0`, `latest` |
| tag `v0.2.0` | `0.2.0`, `0.2`, `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.1.0` produces image tag `0.1.0`. The binaries still report the full `v0.1.0` from `version`.
The leading `v` is stripped from image tags: git tag `v0.2.0` produces image tag `0.2.0`. The binaries still report the full `v0.2.0` from `version`.

### Cutting a release

Expand Down
28 changes: 20 additions & 8 deletions cmd/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strings"

Expand Down Expand Up @@ -678,26 +679,37 @@ func rawJSONText(raw json.RawMessage) string {
// --- Compose-specific ---

var servicesSetSourceInlineCmd = &cobra.Command{
Use: "set-source-inline [project] [service] [compose-file] [compose-content]",
Use: "set-source-inline [project] [service] [compose-content]",
Short: "Set inline compose content (compose type)",
Args: cobra.ExactArgs(4),
Long: "Set inline compose content (compose type).\n\n" +
"A 4-argument form [project] [service] [compose-file] [compose-content] is still\n" +
"accepted for compatibility. The panel schema has no compose-file field, so that\n" +
"argument is ignored.",
Args: cobra.RangeArgs(3, 4),
RunE: func(cmd *cobra.Command, args []string) error {
project, service := args[0], args[1]
content := args[2]
if len(args) == 4 {
// Legacy positional compose-file, dropped by the panel's zod schema.
content = args[3]
fmt.Fprintln(os.Stderr, "warning: the compose-file argument is ignored; the panel accepts only projectName, serviceName and content")
}

c := newPanelClient()
input := map[string]any{
"projectName": args[0],
"serviceName": args[1],
"composeFile": args[2],
"projectName": project,
"serviceName": service,
// Panel schema calls this field "content", not "composeContent".
"content": args[3],
"content": content,
}
err := c.call(context.Background(), "/api/trpc/services.compose.updateSourceInline", input, nil)
if err != nil {
return err
}
if err := redeployAfterConfig(c, args[0], args[1], "compose"); err != nil {
if err := redeployAfterConfig(c, project, service, "compose"); err != nil {
return fmt.Errorf("apply inline source: %w", err)
}
fmt.Printf("Inline source set and deployed: %s/%s\n", args[0], args[1])
fmt.Printf("Inline source set and deployed: %s/%s\n", project, service)
return nil
},
}
Expand Down
66 changes: 56 additions & 10 deletions docs/deployment-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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.1.0`.
Examples use release `0.2.0`.

Supported Easypanel versions: `2.32.0` .. `2.33.1`. Check what you are pointed at with
`deploy-everything settings panel-version`.
Expand Down Expand Up @@ -61,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.1.0
docker pull ghcr.io/igun997/deploy-everything:0.2.0

docker run -d \
--name deploy-everything \
--restart unless-stopped \
-p 50051:50051 \
--env-file .env \
ghcr.io/igun997/deploy-everything:0.1.0
ghcr.io/igun997/deploy-everything:0.2.0
```

Verify:

```bash
docker logs deploy-everything
docker run --rm ghcr.io/igun997/deploy-everything:0.1.0 version
docker run --rm ghcr.io/igun997/deploy-everything:0.2.0 version
```

### Docker Compose
Expand All @@ -84,7 +84,7 @@ docker run --rm ghcr.io/igun997/deploy-everything:0.1.0 version
# compose.grpc-server.yml
services:
deploy-everything:
image: ghcr.io/igun997/deploy-everything:0.1.0
image: ghcr.io/igun997/deploy-everything:0.2.0
restart: unless-stopped
ports:
- "50051:50051"
Expand Down Expand Up @@ -240,6 +240,48 @@ Without `customDomain`, generated host format is:
{project}-{service}-{composeService}.{DEFAULT_DOMAIN}
```

### Update an existing Compose stack

`DeployCompose` creates. It calls the panel's `createService` first, so a second call
for the same service fails:

```text
create compose: [BAD_REQUEST] Service already exists.
```

Use `UpdateComposeSource` to replace the content of a stack that is already running:

```bash
jq -n --rawfile compose docker-compose.yml '{
project: "demo",
service: "whoami-stack",
composeContent: $compose
}' > /tmp/update-compose.json

paas_call UpdateComposeSource < /tmp/update-compose.json
```

```json
{
"deployed": true,
"status": "deploying"
}
```

| Field | Meaning |
|-------|---------|
| `composeContent` | Full replacement. Not merged with the stored content |
| `skipDeploy` | Store the content without redeploying, to batch several edits |
| `deployed` | `false` when `skipDeploy` was set |

The Compose rules above still apply: invalid YAML and `network_mode: host` are
rejected, host `ports:` are stripped, `expose:` survives. A service that does not
exist, or exists under a different type, returns `NotFound`.

Domains are not touched by this RPC. Use `AddDomain` / `RemoveDomain` for routing
changes, and note that adding a domain to a Compose stack needs a redeploy to take
effect.

### Multi-service WordPress + MySQL + Adminer

```yaml
Expand Down Expand Up @@ -570,14 +612,14 @@ Use the release binary:

```bash
# Download archive from:
# https://github.com/igun997/deploy-everything/releases/tag/v0.1.0
# https://github.com/igun997/deploy-everything/releases/tag/v0.2.0
```

Or run the CLI image:

```bash
docker run --rm --env-file .env \
ghcr.io/igun997/deploy-everything-cli:0.1.0 version
ghcr.io/igun997/deploy-everything-cli:0.2.0 version
```

For local commands:
Expand All @@ -592,7 +634,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.1.0
ghcr.io/igun997/deploy-everything-cli:0.2.0
```

## 9. CLI: normal app deployment cases
Expand Down Expand Up @@ -687,7 +729,7 @@ services:

```bash
$CLI services create demo stack compose
$CLI services set-source-inline demo stack docker-compose.yml "$(cat docker-compose.yml)"
$CLI services set-source-inline demo stack "$(cat docker-compose.yml)"
$CLI domains create demo stack stack.example.com \
--type compose \
--compose-service web \
Expand All @@ -697,6 +739,10 @@ $CLI domains create demo stack stack.example.com \

Both source and domain commands automatically redeploy. Direct CLI Compose source is sent to Easypanel as written, so use `expose` rather than host `ports`.

Re-running `set-source-inline` on the same service replaces the stored content, so
this is also the update path. The gRPC equivalent is
[`UpdateComposeSource`](#update-an-existing-compose-stack).

### Compose from Git

```bash
Expand Down Expand Up @@ -883,7 +929,7 @@ 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.1.0 version
docker run --rm ghcr.io/igun997/deploy-everything-cli:0.2.0 version

# Easypanel version + supported range
./deploy-everything settings panel-version
Expand Down
Loading