Skip to content

Commit 4c16f23

Browse files
committed
erservercli: embeddable loadbalancer's CLI
1 parent 3ea9f24 commit 4c16f23

5 files changed

Lines changed: 274 additions & 22 deletions

File tree

cmd/edgerouter/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func serveEntry() *cobra.Command {
4646

4747
osutil.ExitIfError(erserver.Serve(
4848
osutil.CancelOnInterruptOrTerminate(rootLogger),
49+
erserver.DefaultConfigDir,
4950
rootLogger))
5051
},
5152
}

pkg/erserver/ipfilter.go

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,12 @@ import (
99
"io"
1010
"os"
1111

12-
"github.com/function61/gokit/fileexists"
1312
"github.com/function61/gokit/hcl2json"
1413
"github.com/function61/gokit/jsonfile"
1514
"github.com/function61/gokit/sliceutil"
1615
"inet.af/netaddr"
1716
)
1817

19-
const (
20-
ipRulesFile = "/etc/edgerouter/ip-rules.hcl" // temporary solution - these will have to live in EventHorizon
21-
)
22-
2318
type ipRule struct {
2419
ipPrefix netaddr.IPPrefix
2520
allowedAppIds []string // if empty, means all apps are allowed
@@ -94,19 +89,14 @@ type ipRulesConfig struct {
9489
} `json:"allow_specified"`
9590
}
9691

97-
func loadIpRules() ([]ipRule, error) {
98-
exists, err := fileexists.Exists(ipRulesFile)
99-
if err != nil {
100-
return nil, err
101-
}
102-
103-
if !exists { // not an error => we just don't have any rules.. pun intended :)
104-
return nil, nil
105-
}
106-
92+
func loadIpRules(ipRulesFile string) ([]ipRule, error) {
10793
f, err := os.Open(ipRulesFile)
10894
if err != nil {
109-
return nil, err
95+
if os.IsNotExist(err) { // not an error => we just don't have any rules.. pun intended :)
96+
return nil, nil
97+
} else { // actual error
98+
return nil, err
99+
}
110100
}
111101
defer f.Close()
112102

pkg/erserver/serve.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log"
1010
"net/http"
1111
"os"
12+
"path/filepath"
1213
"strconv"
1314
"strings"
1415
"sync/atomic"
@@ -29,13 +30,12 @@ import (
2930
)
3031

3132
const (
32-
ConfigDir = "/etc/edgerouter"
33-
LocalDevCertfilePath = "/etc/edgerouter/dev-cert.pem" // used when CertBus not configured
33+
DefaultConfigDir ConfigDir = "/etc/edgerouter"
3434
)
3535

3636
type GetCertificateFn func(*tls.ClientHelloInfo) (*tls.Certificate, error)
3737

38-
func Serve(ctx context.Context, logger *log.Logger) error {
38+
func Serve(ctx context.Context, configDir ConfigDir, logger *log.Logger) error {
3939
logl := logex.Levels(logger)
4040

4141
metrics := initMetrics()
@@ -70,10 +70,10 @@ func Serve(ctx context.Context, logger *log.Logger) error {
7070

7171
return certBus.GetCertificateAdapter(), nil
7272
} else {
73-
logl.Info.Printf("CertBus not configured - assuming local dev-server & using %s", LocalDevCertfilePath)
73+
logl.Info.Printf("CertBus not configured - assuming local dev-server & using %s", configDir.DevelopmentCertificate())
7474

7575
// this is expected to be configured with mkcert or similar
76-
keyPair, err := tls.LoadX509KeyPair(LocalDevCertfilePath, LocalDevCertfilePath)
76+
keyPair, err := tls.LoadX509KeyPair(configDir.DevelopmentCertificate(), configDir.DevelopmentCertificate())
7777
if err != nil {
7878
if errors.Is(err, os.ErrNotExist) {
7979
return nil, fmt.Errorf(
@@ -111,7 +111,9 @@ func Serve(ctx context.Context, logger *log.Logger) error {
111111

112112
// TODO: if these rules have syntax error, it'd be good if it came up before other async tasks
113113
// are started.
114-
ipRules, err := loadIpRules()
114+
//
115+
// file is a temporary solution - these will have to live in EventHorizon
116+
ipRules, err := loadIpRules("/etc/edgerouter/ip-rules.hcl")
115117
if err != nil {
116118
return err
117119
}
@@ -363,6 +365,22 @@ func alwaysReturnSameCertificate(keyPair *tls.Certificate) GetCertificateFn {
363365
}
364366
}
365367

368+
type ConfigDir string
369+
370+
func (c ConfigDir) DevelopmentCertificate() string {
371+
return c.File("dev-cert.pem")
372+
}
373+
374+
func (c ConfigDir) String() string {
375+
return string(c)
376+
}
377+
378+
// makes path to any file in the configuration directory
379+
// use sparingly
380+
func (c ConfigDir) File(filename string) string {
381+
return filepath.Join(string(c), filename)
382+
}
383+
366384
type atomicConfig struct {
367385
atomic.Value // stores *frontendMatchers
368386
}

pkg/erservercli/servercli.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Embeddable CLI for Edgerouter server library.
2+
// Think calling "yourapp proxy ..." and all subcommands are routed to Edgerouter-provided subcommands.
3+
package erservercli
4+
5+
import (
6+
"fmt"
7+
"io/ioutil"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/function61/edgerouter/pkg/erserver"
12+
"github.com/function61/gokit/dynversion"
13+
"github.com/function61/gokit/fileexists"
14+
"github.com/function61/gokit/logex"
15+
"github.com/function61/gokit/osutil"
16+
"github.com/function61/gokit/systemdinstaller"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
type Options struct {
21+
ServiceName string
22+
SubcommandName string
23+
}
24+
25+
func (o Options) ConfigDir() erserver.ConfigDir {
26+
return erserver.ConfigDir(filepath.Join("/etc", o.ServiceName))
27+
}
28+
29+
// suppose you embed Edgerouter in application called "bob".
30+
// it is assumed that you'll assign top-level subcommand name e.g. "proxy" for Edgerouter inside bob.
31+
// hence commands to edgerouter will start with "bob proxy".
32+
func Entrypoint(opts Options) *cobra.Command {
33+
cmd := &cobra.Command{
34+
Use: opts.SubcommandName,
35+
Short: "Edgerouter embeddable proxy",
36+
Version: dynversion.Version,
37+
}
38+
39+
cmd.AddCommand(&cobra.Command{
40+
Use: "serve",
41+
Short: "Runs the HTTP server",
42+
Args: cobra.NoArgs,
43+
Run: func(cmd *cobra.Command, args []string) {
44+
rootLogger := logex.StandardLogger()
45+
46+
osutil.ExitIfError(erserver.Serve(
47+
osutil.CancelOnInterruptOrTerminate(rootLogger),
48+
opts.ConfigDir(),
49+
rootLogger))
50+
},
51+
})
52+
53+
cmd.AddCommand(setupDevCertsEntry(opts))
54+
55+
cmd.AddCommand(&cobra.Command{
56+
Use: "install-as-service",
57+
Short: "Install systemd service file to start Edgerouter on system startup",
58+
Args: cobra.NoArgs,
59+
Run: func(cmd *cobra.Command, args []string) {
60+
osutil.ExitIfError(installAsService(opts))
61+
},
62+
})
63+
64+
return cmd
65+
}
66+
67+
func installAsService(opts Options) error {
68+
envFile := opts.ConfigDir().File("edgerouter.env")
69+
exists, err := fileexists.Exists(opts.ConfigDir().String())
70+
if err != nil {
71+
return err
72+
}
73+
74+
if exists {
75+
return fmt.Errorf("'%s' exists - Edgerouter already installed? Not continuing for safety", opts.ConfigDir().String())
76+
}
77+
78+
service := systemdinstaller.SystemdServiceFile(
79+
opts.ServiceName,
80+
"Edgerouter",
81+
systemdinstaller.Args(opts.SubcommandName, "serve"),
82+
// TODO: systemdinstaller.EnvFile("/etc/edgerouter.env")
83+
systemdinstaller.Env("DUMMY", "x\nEnvironmentFile="+envFile), // ugly hack
84+
systemdinstaller.Docs(
85+
"https://github.com/function61/edgerouter",
86+
"https://function61.com/"))
87+
88+
osutil.ExitIfError(systemdinstaller.Install(service))
89+
90+
fmt.Println(systemdinstaller.GetHints(service))
91+
92+
if err := os.MkdirAll(opts.ConfigDir().String(), 0700); err != nil {
93+
return err
94+
}
95+
96+
return ioutil.WriteFile(envFile, []byte(`
97+
# --- Docker integration
98+
DOCKER_URL=unix:///var/run/docker.sock
99+
NETWORK_NAME=docker_gwbridge
100+
101+
# --- CertBus / EventHorizon-based discovery / Lambda function routing / S3 hosting
102+
# AWS_SECRET_ACCESS_KEY=...
103+
# AWS_ACCESS_KEY_ID=AKIA...
104+
105+
# --- CertBus / EventHorizon-based discovery
106+
# EVENTHORIZON=prod:1:::eu-central-1
107+
108+
# --- CertBus
109+
# CERTBUS_CLIENT_PRIVKEY=...
110+
`), 0600)
111+
}

pkg/erservercli/setupdevcerts.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package erservercli
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
11+
"github.com/function61/gokit/osutil"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func setupDevCertsEntry(opts Options) *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "setup-devcerts",
18+
Short: "mkcert utility shortcuts for generating CA and server cert for development purposes",
19+
}
20+
21+
cmd.AddCommand(&cobra.Command{
22+
Use: "ca-install",
23+
Short: "Install local-trust-only CA certificate into system trust stores",
24+
Args: cobra.NoArgs,
25+
Run: func(cmd *cobra.Command, args []string) {
26+
mkcert := exec.Command("mkcert", "-install")
27+
mkcert.Stdout = os.Stdout
28+
mkcert.Stderr = os.Stderr
29+
osutil.ExitIfError(translateIfMkcertNotInstalledError(mkcert.Run()))
30+
},
31+
})
32+
33+
cmd.AddCommand(&cobra.Command{
34+
Use: "servercert-generate [hostname]",
35+
Short: "Generate server cert. Example hostname: *.dev.example.com",
36+
Args: cobra.ExactArgs(1),
37+
Run: func(cmd *cobra.Command, args []string) {
38+
osutil.ExitIfError(serverCertGenerate(args[0], opts))
39+
},
40+
})
41+
42+
return cmd
43+
}
44+
45+
func serverCertGenerate(hostname string, opts Options) error {
46+
tempDir, err := ioutil.TempDir("", "edgerouter-mkcert-*")
47+
if err != nil {
48+
return err
49+
}
50+
defer os.RemoveAll(tempDir)
51+
52+
if err := os.MkdirAll(filepath.Dir(opts.ConfigDir().DevelopmentCertificate()), 0750); err != nil {
53+
return translateIfSudoError(err)
54+
}
55+
56+
// unfortunately, you can't ask mkcert where or for which name to store the certs under
57+
58+
mkcert := exec.Command("mkcert", hostname)
59+
mkcert.Dir = tempDir
60+
mkcert.Stdout = os.Stdout
61+
mkcert.Stderr = os.Stderr
62+
if err := mkcert.Run(); err != nil {
63+
return translateIfMkcertNotInstalledError(err)
64+
}
65+
66+
// *.dev.fn61.net would generate these names
67+
// _wildcard.dev.fn61.net.pem
68+
// _wildcard.dev.fn61.net-key.pem
69+
fmt.Println("\nServer cert generated.")
70+
71+
key, err := findReadAndDeleteFile(filepath.Join(tempDir, "*-key.pem"))
72+
if err != nil {
73+
return err
74+
}
75+
76+
cert, err := findReadAndDeleteFile(filepath.Join(tempDir, "*.pem"))
77+
if err != nil {
78+
return err
79+
}
80+
81+
if err := translateIfSudoError(ioutil.WriteFile(
82+
opts.ConfigDir().DevelopmentCertificate(),
83+
append(cert, key...),
84+
0600),
85+
); err != nil {
86+
return err
87+
}
88+
89+
fmt.Printf(
90+
"Server cert written to '%s' - will be picked up on Edgerouter start\n",
91+
opts.ConfigDir().DevelopmentCertificate())
92+
93+
return nil
94+
}
95+
96+
func translateIfMkcertNotInstalledError(err error) error {
97+
if err != nil && errors.Is(err, exec.ErrNotFound) {
98+
return errors.New("mkcert not installed? See https://github.com/FiloSottile/mkcert#installation")
99+
}
100+
101+
return err
102+
}
103+
104+
func translateIfSudoError(err error) error {
105+
if err != nil && errors.Is(err, os.ErrPermission) {
106+
return fmt.Errorf("(probably need '$ sudo ...') %w", err)
107+
}
108+
109+
return err
110+
}
111+
112+
func findReadAndDeleteFile(globPattern string) ([]byte, error) {
113+
globMatches, err := filepath.Glob(globPattern)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
if len(globMatches) != 1 {
119+
return nil, fmt.Errorf("findReadAndDeleteFile: expected 1 match; got %d", len(globMatches))
120+
}
121+
122+
content, err := ioutil.ReadFile(globMatches[0])
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
if err := os.Remove(globMatches[0]); err != nil {
128+
return nil, err
129+
}
130+
131+
return content, nil
132+
}

0 commit comments

Comments
 (0)