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
3 changes: 3 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,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)
}
}
17 changes: 17 additions & 0 deletions internal/api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,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 +1080,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
10 changes: 10 additions & 0 deletions internal/cli/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ func manifestEntries(manifest *api.Manifest) []manifestEntry {
add("mounts", strings.Join(manifest.Mounts, ", "))
add("extras", manifestExtras(manifest.Extras))
add("backup exclude", strings.Join(manifest.BackupExclude, ", "))
add("frontend", manifestFrontend(manifest.Frontend))

return entries
}
Expand Down Expand Up @@ -412,6 +413,15 @@ func manifestWorkers(workers []api.Worker) string {
return strings.Join(parts, ", ")
}

// manifestFrontend renders the application beside the backend, or nothing.
func manifestFrontend(frontend *api.ManifestFrontend) string {
if frontend == nil {
return ""
}

return frontend.Root + " (" + settingsLine(frontend.Runtime) + "), " + frontend.Start
}

// manifestExtras renders the customer's own machines and how to reach them.
//
// The ports are the point. An extra is a machine running software this platform
Expand Down
Loading