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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ when it is left off.
| `url [--all] [--open]` | The browsable address, alone on stdout. `--all` lists every name the edge routes |
| `deploy` · `rollback` · `redeploy` · `release list` | Shipping |
| `activity list` · `activity get` · `activity log` · `activity cancel` | What the platform has been doing, the log of one task, and calling off one that has not started |
| `logs` | Where the site's own request and error logs go — the project's destination and the machine's copy. Not a tail: Vallic Cloud does not keep them |
| `logs [--follow] [--since] [--source] [--output]` | The site's own logs, read off its machines over your SSH key: containers, cron, the application's log files and the proxy's requests. `--follow` is a live tail, `--output` saves a window to a file, `--where` says where the project sends them besides |
| `ssh` · `sql` · `db dump\|export\|import\|size` · `mount` | Getting inside. `db dump` writes a file named for the project and environment, all of it or `--table`s; `db size` shows the largest tables; `sql -- <query>` runs one query |
| `drush` | `drush` in the container, over the same forced command `ssh --` uses |
| `var list\|get\|set\|delete\|import\|apply` | Variables, at either scope; `import` adds a `.env`, `apply` restarts the site with them now |
Expand Down
11 changes: 10 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,14 @@ vallic activity log 1234 --follow
# Call off a deploy still waiting in the queue; one already running finishes
vallic activity cancel 1235

# Where the site's logs go
# The site's own logs: the last 100 lines, a live tail, a window, one kind
vallic logs production
vallic logs production --follow
vallic logs production --since 1h --source proxy
vallic logs production --since 24h --output production.log.gz

# Where the project sends them besides
vallic logs production --where

# CPU, memory and disk: now, on average and at peak
vallic metrics production
Expand Down Expand Up @@ -308,6 +314,9 @@ vallic domain list production
vallic domain primary shop.example.com production
vallic domain canonical shop.example.com production

# Where the repository declares a frontend: which application a hostname reaches
vallic domain application shop.example.com frontend production

# Give one up
vallic domain delete old.example.com production
```
Expand Down
31 changes: 31 additions & 0 deletions internal/api/domains_promote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)

Expand Down Expand Up @@ -55,3 +56,33 @@ func TestPromoteDomainSaysWhetherTheOthersRedirect(t *testing.T) {
})
}
}

// The application is sent on its own, so nothing else about the hostname moves.
func TestSetDomainApplicationSendsOnlyThat(t *testing.T) {
var sent map[string]any

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&sent); err != nil {
t.Errorf("decoding: %v", err)
}
if r.Method != http.MethodPatch || !strings.HasSuffix(r.URL.Path, "/domains/5") {
t.Errorf("sent to %s %s", r.Method, r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"domain":{"id":5,"application":"frontend"},"message":"ok"}`))
}))
defer server.Close()

client := New(server.URL, anonymous{}, "test")

updated, err := client.SetDomainApplication(context.Background(), 5, "frontend")
if err != nil {
t.Fatalf("SetDomainApplication: %v", err)
}
if updated.Domain.Application != "frontend" {
t.Errorf("application = %q", updated.Domain.Application)
}
if len(sent) != 1 || sent["application"] != "frontend" {
t.Errorf("sent %v", sent)
}
}
23 changes: 23 additions & 0 deletions internal/api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ type SSHTarget struct {
DBSize string `json:"db_size"`
DBExportOptions []string `json:"db_export_options"`

// Logs is the verb that reads the environment's own logs off the
// machine, for `vallic logs`. Empty where a machine's agent is older
// than the shell that answers it, and against a control plane that
// predates it.
Logs string `json:"logs"`

// FilesPath is the one writable directory that is the same for every
// project type and survives every deploy. What rsync should be aimed at.
FilesPath string `json:"files_path"`
Expand Down Expand Up @@ -692,6 +698,11 @@ type Domain struct {
// environment and cannot be removed on its own.
Kind string `json:"kind"`

// Application is which of the environment's applications it asks to reach:
// "backend" or "frontend". With no frontend deployed every hostname reaches
// the backend, whatever this says.
Application string `json:"application"`

// IsPrimary is the site's address, which the platform hostname redirects
// to; IsCanonical, only ever on a primary custom domain, redirects every
// other hostname to it as well.
Expand Down Expand Up @@ -1075,6 +1086,18 @@ type Manifest struct {
// BackupExclude is what the files backup leaves out beside the
// framework's own caches, as the file wrote it.
BackupExclude []string `json:"backup_exclude"`

// Frontend is a Node.js or Bun application beside this one, in a directory
// of its own. Nil where the repository declares none.
Frontend *ManifestFrontend `json:"frontend"`
}

// ManifestFrontend is the application a repository runs beside its backend.
type ManifestFrontend struct {
Root string `json:"root"`
Runtime map[string]string `json:"runtime"`
Start string `json:"start"`
Port int `json:"port"`
}

// ManifestService is one service a repository asked to run beside it.
Expand Down
17 changes: 17 additions & 0 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,23 @@ func (c *Client) PromoteDomain(ctx context.Context, domainID int, canonical bool
return &out, nil
}

// SetDomainApplication says which of the environment's applications a hostname
// reaches: "backend" or "frontend".
func (c *Client) SetDomainApplication(ctx context.Context, domainID int, application string) (*DomainUpdated, error) {
body, err := json.Marshal(map[string]any{"application": application})
if err != nil {
return nil, err
}

var out DomainUpdated

if err := c.do(ctx, http.MethodPatch, fmt.Sprintf("/domains/%d", domainID), body, &out); err != nil {
return nil, err
}

return &out, nil
}

// DeleteDomain removes a hostname.
func (c *Client) DeleteDomain(ctx context.Context, domainID int) (*DomainRemoved, error) {
var out DomainRemoved
Expand Down
52 changes: 52 additions & 0 deletions internal/cli/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ actually being routed.`,
domainVerifyCommand(),
domainPrimaryCommand(),
domainCanonicalCommand(),
domainApplicationCommand(),
domainDeleteCommand(),
},
}
Expand Down Expand Up @@ -271,6 +272,57 @@ hostname cannot, because your own domains would redirect to it.`,
}
}

func domainApplicationCommand() *Command {
return &Command{
Name: "application",
Summary: "send a hostname to the backend or the frontend",
Usage: "domain application <hostname> <backend|frontend> [<env>]",
Long: `Says which of the environment's applications one of your own hostnames
reaches, where the repository declares a frontend beside its backend. Your
hostnames reach the backend until you send them to the frontend.

It stops being primary or canonical as it moves: those are of the application
it leaves. Choose again among the hostnames of the one it joins.`,
Run: func(ctx context.Context, env *Env, args []string) error {
if len(args) < 2 {
return Usagef(nil, "name a hostname, and backend or frontend")
}

if args[1] != "backend" && args[1] != "frontend" {
return Usagef(nil, "%q is neither backend nor frontend", args[1])
}

target, err := env.ResolveEnvironment(ctx, firstOf(args[2:]))
if err != nil {
return err
}

client, err := env.Client()
if err != nil {
return err
}

domain, err := findDomain(ctx, client, target.Environment.ID, args[0])
if err != nil {
return err
}

updated, err := client.SetDomainApplication(ctx, domain.ID, args[1])
if err != nil {
return err
}

if env.Printer.Structured() {
return env.Printer.Value(updated)
}

env.Printer.Good("%s", updated.Message)

return nil
},
}
}

// promoteDomain is domain primary and domain canonical: the same request,
// with or without the other hostnames redirecting.
func promoteDomain(ctx context.Context, env *Env, args []string, canonical bool) error {
Expand Down
Loading
Loading