Skip to content

Commit

Permalink
Add custom domain DB configuration logic
Browse files Browse the repository at this point in the history
  • Loading branch information
kiootic committed Nov 29, 2023
1 parent 89fe61c commit 330f852
Show file tree
Hide file tree
Showing 18 changed files with 661 additions and 2 deletions.
12 changes: 12 additions & 0 deletions cmd/pageship/app/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,23 @@ var appsConfigureCmd = &cobra.Command{
return fmt.Errorf("failed to get app: %w", err)
}

oldConfig := app.Config

app, err = API().ConfigureApp(cmd.Context(), app.ID, &conf.App)
if err != nil {
return fmt.Errorf("failed to configure app: %w", err)
}

for _, dconf := range conf.App.Domains {
if _, exists := oldConfig.ResolveDomain(dconf.Domain); !exists {
Info("Activating custom domain %q...", dconf.Domain)
_, err = API().CreateDomain(cmd.Context(), app.ID, dconf.Domain, "")
if err != nil {
Warn("Activation of custom domain %q failed: %s", dconf.Domain, err)
}
}
}

Info("Configured app %q.", app.ID)
return nil
},
Expand Down
194 changes: 194 additions & 0 deletions cmd/pageship/app/domains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package app

import (
"context"
"fmt"
"net/http"
"os"
"text/tabwriter"
"time"

"github.com/manifoldco/promptui"
"github.com/oursky/pageship/internal/api"
"github.com/oursky/pageship/internal/models"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func init() {
rootCmd.AddCommand(domainsCmd)
domainsCmd.PersistentFlags().String("app", "", "app ID")

domainsCmd.AddCommand(domainsActivateCmd)
domainsCmd.AddCommand(domainsDeactivateCmd)
}

var domainsCmd = &cobra.Command{
Use: "domains",
Short: "Manage custom domains",
RunE: func(cmd *cobra.Command, args []string) error {
appID := viper.GetString("app")
if appID == "" {
appID = tryLoadAppID()
}
if appID == "" {
return fmt.Errorf("app ID is not set")
}

app, err := API().GetApp(cmd.Context(), appID)
if err != nil {
return fmt.Errorf("failed to get app: %w", err)
}

type domainEntry struct {
name string
site string
model *api.APIDomain
}
domains := map[string]domainEntry{}
for _, dconf := range app.Config.Domains {
domains[dconf.Domain] = domainEntry{
name: dconf.Domain,
site: dconf.Site,
model: nil,
}
}

apiDomains, err := API().ListDomains(cmd.Context(), appID)
if err != nil {
return fmt.Errorf("failed to list domains: %w", err)
}

for _, d := range apiDomains {
dd := d
domains[d.Domain.Domain] = domainEntry{
name: d.Domain.Domain,
site: d.Domain.SiteName,
model: &dd,
}
}

w := tabwriter.NewWriter(os.Stdout, 1, 4, 4, ' ', 0)
fmt.Fprintln(w, "NAME\tSITE\tCREATED AT\tSTATUS")
for _, domain := range domains {
createdAt := "-"
site := "-"
if domain.model != nil {
createdAt = domain.model.CreatedAt.Local().Format(time.DateTime)
site = fmt.Sprintf("%s/%s", domain.model.AppID, domain.model.SiteName)
} else {
site = fmt.Sprintf("%s/%s", app.ID, domain.site)
}

var status string
switch {
case domain.model != nil && domain.model.AppID != app.ID:
status = "IN_USE"
case domain.model != nil && domain.model.AppID == app.ID:
status = "ACTIVE"
default:
status = "INACTIVE"
}

fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", domain.name, site, createdAt, status)
}
w.Flush()
return nil
},
}

func promptDomainReplaceApp(ctx context.Context, appID string, domainName string) (replaceApp string, err error) {
domains, err := API().ListDomains(ctx, appID)
if err != nil {
return "", fmt.Errorf("failed list domain: %w", err)
}

appID = ""
for _, d := range domains {
if d.Domain.Domain == domainName {
appID = d.AppID
}
}

if appID == "" {
return "", models.ErrDomainUsedName
}

label := fmt.Sprintf("Domain %q is in use by app %q; activates the domain anyways", domainName, appID)

prompt := promptui.Prompt{Label: label, IsConfirm: true}
_, err = prompt.Run()
if err != nil {
Info("Cancelled.")
return "", ErrCancelled
}

return appID, nil
}

var domainsActivateCmd = &cobra.Command{
Use: "activate",
Short: "Activate domain for the app",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
domainName := args[0]

appID := viper.GetString("app")
if appID == "" {
appID = tryLoadAppID()
}
if appID == "" {
return fmt.Errorf("app ID is not set")
}

app, err := API().GetApp(cmd.Context(), appID)
if err != nil {
return fmt.Errorf("failed to get app: %w", err)
}
if _, ok := app.Config.ResolveDomain(domainName); !ok {
return fmt.Errorf("undefined domain")
}

_, err = API().CreateDomain(cmd.Context(), appID, domainName, "")
if code, ok := api.ErrorStatusCode(err); ok && code == http.StatusConflict {
var replaceApp string
replaceApp, err = promptDomainReplaceApp(cmd.Context(), appID, domainName)
if err != nil {
return err
}
_, err = API().CreateDomain(cmd.Context(), appID, domainName, replaceApp)
}

if err != nil {
return fmt.Errorf("failed to create domain: %w", err)
}

Info("Domain %q activated.", domainName)
return nil
},
}

var domainsDeactivateCmd = &cobra.Command{
Use: "deactivate",
Short: "Deactivate domain for the app",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
domainName := args[0]

appID := viper.GetString("app")
if appID == "" {
appID = tryLoadAppID()
}
if appID == "" {
return fmt.Errorf("app ID is not set")
}

_, err := API().DeleteDomain(cmd.Context(), appID, domainName)
if err != nil {
return fmt.Errorf("failed to delete domain: %w", err)
}

Info("Domain %q deactivated.", domainName)
return nil
},
}
74 changes: 74 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,80 @@ func (c *Client) UploadDeploymentTarball(
return decodeJSONResponse[*models.Deployment](resp)
}

func (c *Client) ListDomains(ctx context.Context, appID string) ([]APIDomain, error) {
endpoint, err := url.JoinPath(c.endpoint, "api", "v1", "apps", appID, "domains")
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
if err != nil {
return nil, err
}
if err := c.attachToken(req); err != nil {
return nil, err
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

return decodeJSONResponse[[]APIDomain](resp)
}

func (c *Client) CreateDomain(ctx context.Context, appID string, domainName string, replaceApp string) (*APIDomain, error) {
endpoint, err := url.JoinPath(c.endpoint, "api", "v1", "apps", appID, "domains", domainName)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "POST", endpoint, nil)
if err != nil {
return nil, err
}
if replaceApp != "" {
req.URL.RawQuery = url.Values{
"replaceApp": []string{replaceApp},
}.Encode()
}
if err := c.attachToken(req); err != nil {
return nil, err
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

return decodeJSONResponse[*APIDomain](resp)
}

func (c *Client) DeleteDomain(ctx context.Context, appID string, domainName string) (*APIDomain, error) {
endpoint, err := url.JoinPath(c.endpoint, "api", "v1", "apps", appID, "domains", domainName)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "DELETE", endpoint, nil)
if err != nil {
return nil, err
}
if err := c.attachToken(req); err != nil {
return nil, err
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

return decodeJSONResponse[*APIDomain](resp)
}

func (c *Client) OpenAuthGitHubSSH(ctx context.Context) (*websocket.Conn, error) {
endpoint, err := url.JoinPath(c.endpoint, "api", "v1", "auth", "github-ssh")
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions internal/api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type APIDeployment struct {
URL *string `json:"url"`
}

type APIDomain struct {
*models.Domain
}

type APIUser struct {
ID string `json:"id"`
Name string `json:"name"`
Expand Down
11 changes: 11 additions & 0 deletions internal/config/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ func (c *AppConfig) SetDefaults() {
}
}

func (c *AppConfig) ResolveDomain(domain string) (resolved AppDomainConfig, ok bool) {
for _, d := range c.Domains {
if d.Domain == domain {
resolved = d
ok = true
return
}
}
return
}

func (c *AppConfig) ResolveSite(site string) (resolved AppSiteConfig, ok bool) {
for _, s := range c.Sites {
pattern, err := s.CompilePattern()
Expand Down
8 changes: 8 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type DBQuery interface {
AppsDB
SitesDB
DeploymentsDB
DomainsDB
UserDB
CertificateDB
}
Expand Down Expand Up @@ -74,6 +75,13 @@ type DeploymentsDB interface {
DeleteExpiredDeployments(ctx context.Context, now time.Time, expireBefore time.Time) (int64, error)
}

type DomainsDB interface {
CreateDomain(ctx context.Context, domain *models.Domain) error
GetDomainByName(ctx context.Context, domain string) (*models.Domain, error)
DeleteDomain(ctx context.Context, id string, now time.Time) error
ListDomains(ctx context.Context, appID string) ([]*models.Domain, error)
}

type UserDB interface {
GetUser(ctx context.Context, id string) (*models.User, error)
GetCredential(ctx context.Context, id models.CredentialID) (*models.UserCredential, error)
Expand Down
Loading

0 comments on commit 330f852

Please sign in to comment.