summaryrefslogtreecommitdiff
path: root/internal/nginx/nginx.go
blob: 4a85435b6e28be9fa889173a63d5ce3e515fc017 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Package nginx regenerates one vhost file per app and reloads. Skiff never
// touches nginx.conf itself - that just needs one `include skiff.d/*.conf;`
// line added once, by hand, when nginx is first set up.
package nginx

import (
	"context"
	"fmt"
	"io"
	"os"

	"skiff/internal/appmeta"
	"skiff/internal/layout"
	"skiff/internal/sh"
	"skiff/internal/tmpl"
)

// EnsureRoute (re)writes app's vhost file from meta and reloads nginx, but
// only if the rendered config changed. A hostname gets HTTPS only once it's
// in both meta.Domains and meta.TLSDomains. No domains yet is a no-op.
func EnsureRoute(ctx context.Context, out io.Writer, cfg layout.Config, app string, meta appmeta.Meta) error {
	if len(meta.Domains) == 0 {
		fmt.Fprintf(out, "==> no domains configured for %s, skipping nginx route\n", app)
		return nil
	}
	primary, ok := meta.PrimaryDomain()
	if !ok {
		return fmt.Errorf("%s has domains but none marked primary", app)
	}

	live := make(map[string]bool, len(meta.TLSDomains))
	for _, h := range meta.TLSDomains {
		live[h] = true
	}

	doms := make([]tmpl.NginxDomain, len(meta.Domains))
	for i, d := range meta.Domains {
		doms[i] = tmpl.NginxDomain{Hostname: d.Hostname, Primary: d.Primary, CertLive: live[d.Hostname]}
	}

	rendered, err := tmpl.NginxApp(tmpl.NginxAppData{
		Name:            app,
		IP:              meta.IP,
		Port:            meta.Port,
		Webroot:         cfg.AcmeWebrootDir,
		PrimaryHostname: primary.Hostname,
		PrimaryCertLive: live[primary.Hostname],
		PrimaryLiveDir:  cfg.LetsEncryptLiveDir(primary.Hostname),
		Domains:         doms,
	})
	if err != nil {
		return err
	}

	path := cfg.NginxConfPath(app)
	existing, _ := os.ReadFile(path)
	if string(existing) == rendered {
		return nil
	}

	if err := os.WriteFile(path, []byte(rendered), 0644); err != nil {
		return fmt.Errorf("write nginx conf for %s: %w", app, err)
	}
	fmt.Fprintf(out, "==> nginx route changed for %s, reloading\n", app)
	return Reload(ctx, out)
}

// Remove deletes app's vhost file entirely and reloads - used once its last
// domain is removed, since there's nothing left to route.
func Remove(ctx context.Context, out io.Writer, cfg layout.Config, app string) error {
	path := cfg.NginxConfPath(app)
	if err := os.Remove(path); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("remove nginx conf for %s: %w", app, err)
	}
	fmt.Fprintf(out, "==> nginx route removed for %s, reloading\n", app)
	return Reload(ctx, out)
}

func Reload(ctx context.Context, out io.Writer) error {
	return sh.Run(ctx, out, "", "nginx", "-s", "reload")
}