diff --git a/USAGE.md b/USAGE.md index 1d6394b..0f51cb0 100644 --- a/USAGE.md +++ b/USAGE.md @@ -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 ``` diff --git a/internal/api/domains_promote_test.go b/internal/api/domains_promote_test.go index 85fbdfb..464a811 100644 --- a/internal/api/domains_promote_test.go +++ b/internal/api/domains_promote_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" ) @@ -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) + } +} diff --git a/internal/api/models.go b/internal/api/models.go index 0acff64..0085a44 100644 --- a/internal/api/models.go +++ b/internal/api/models.go @@ -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. @@ -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. diff --git a/internal/api/routes.go b/internal/api/routes.go index f6cdb1e..882cc86 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -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 diff --git a/internal/cli/domain.go b/internal/cli/domain.go index f781485..a6e8edf 100644 --- a/internal/cli/domain.go +++ b/internal/cli/domain.go @@ -28,6 +28,7 @@ actually being routed.`, domainVerifyCommand(), domainPrimaryCommand(), domainCanonicalCommand(), + domainApplicationCommand(), domainDeleteCommand(), }, } @@ -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 []", + 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 { diff --git a/internal/cli/validate.go b/internal/cli/validate.go index d3af64a..6b1e7f1 100644 --- a/internal/cli/validate.go +++ b/internal/cli/validate.go @@ -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 } @@ -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