Skip to content

Commit

Permalink
cmd/tailsql: add the ability to start up tsnet (#2)
Browse files Browse the repository at this point in the history
- Use --local to run on a local port (no longer the default).
- Otherwise, start HTTP on tailscale (with TS_AUTHKEY).
- If ServeHTTPS is enabled, start HTTPS.
  • Loading branch information
creachadair authored Aug 10, 2023
1 parent 8934d61 commit c87a612
Show file tree
Hide file tree
Showing 5 changed files with 457 additions and 80 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,45 @@
TailSQL is a self-contained SQL playground service that runs on [Tailscale](https://tailscale.com).
It permits users to query SQL databases from a basic web-based UI, with support for any database
that can plug in to the Go [`database/sql`](https://godoc.org/database/sql) package.

## Running Locally

Run the commands below from a checkout of https://github.com/tailscale/tailsql.

To run locally, you will need a SQLite database to serve data from. If you do
not already have one, you can create one using the test data for this package:

```shell
# Creates test.db in the current working directory.
sqlite3 test.db -init ./server/tailsql/testdata/init.sql .quit
```

Now build the `tailsql` tool, and create a HuJSON (JWCC) configuration file for it:

```shell
go build ./cmd/tailsql

# The --init-config flag generates a stub config pointing to "test.db".
./tailsql --init-config demo.conf
```

Feel free to edit this configuration file to suit your tastes, then run:

```shell
# The --local flag starts an HTTP server on localhost.
./tailsql --local 8080 --config demo.conf
```

This starts up the server on localhost. Visit the UI at http://localhost:8080,
or call it from the command-line using `curl`:

```shell
# Fetch output as comma-separated values.
curl -s http://localhost:8080/csv --url-query 'q=select * from users'

# Fetch output as JSON objects.
curl -s http://localhost:8080/json --url-query 'q=select location, count(*) n from users where location is not null group by location order by n desc'

# Check the query log.
curl -s http://localhost:8080/json --url-query 'q=select * from query_log' --url-query src=local
```
169 changes: 135 additions & 34 deletions cmd/tailsql/tailsql.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,92 +5,193 @@ import (
"context"
"encoding/json"
"errors"
"expvar"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/creachadair/ctrl"
"github.com/tailscale/tailsql/server/tailsql"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
"tailscale.com/types/logger"

// If you want to support other source types with this tool, you will need
// to import other database drivers below.

// SQLite driver for database/sql.
_ "modernc.org/sqlite"
)

var (
port = flag.Int("port", 8080, "Service port")
localPort = flag.Int("local", 0, "Local service port")
configPath = flag.String("config", "", "Configuration file (HuJSON, required)")
doDebugLog = flag.Bool("debug", false, "Enable verbose debug logging")
initConfig = flag.String("init-config", "",
"Generate a basic configuration file in the given path and exit")

// TODO(creachadair): Allow starting on tsnet.
)

func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: %[1]s [options] --config config.json
%[1]s --init-config demo.json
Run a TailSQL service with the specified --config file.
If --local > 0, the service is run on localhost at that port.
Otherwise, the server starts a Tailscale node at the configured hostname.
When run with --init-config set, %[1]s generates an example configuration file
with defaults suitable for running a local service and then exits.
Options:
`, filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
}

func main() {
flag.Parse()
ctrl.Run(func() error {
// Case 1: Generate a semple configuration file and exit.
if *initConfig != "" {
generateBasicConfig(*initConfig)
log.Printf("Generated sample config in %s", *initConfig)
return nil
} else if *configPath == "" {
ctrl.Fatalf("You must provide a non-empty --config path")
}

if *initConfig != "" {
generateBasicConfig(*initConfig)
log.Printf("Generated sample config in %s", *initConfig)
return
}
if *port <= 0 {
log.Fatal("You must provide a --port > 0")
} else if *configPath == "" {
log.Fatal("You must provide a --config path")
}
// For all the cases below, we need a valid configuration file.
data, err := os.ReadFile(*configPath)
if err != nil {
ctrl.Fatalf("Reading tailsql config: %v", err)
}
var opts tailsql.Options
if err := tailsql.UnmarshalOptions(data, &opts); err != nil {
ctrl.Fatalf("Parsing tailsql config: %v", err)
}
opts.Metrics = expvar.NewMap("tailsql")

data, err := os.ReadFile(*configPath)
if err != nil {
log.Fatalf("Reading tailsql config: %v", err)
}
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

var opts tailsql.Options
if err := tailsql.UnmarshalOptions(data, &opts); err != nil {
log.Fatalf("Parsing tailsql config: %v", err)
}
// Case 2: Run unencrypted on a local port.
if *localPort > 0 {
return runLocalService(ctx, opts, *localPort)
}

// Case 3: Run on a tailscale node.
if opts.Hostname == "" {
ctrl.Fatalf("You must provide a non-empty Tailscale hostname")
}
logf := logger.Discard
if *doDebugLog {
logf = log.Printf
}
tsNode := &tsnet.Server{
Dir: os.ExpandEnv(opts.StateDir),
Hostname: opts.Hostname,
Logf: logf,
}
defer tsNode.Close()

log.Printf("Starting tailscale (hostname=%q)", opts.Hostname)
lc, err := tsNode.LocalClient()
if err != nil {
ctrl.Fatalf("Connect local client: %v", err)
}
opts.LocalClient = lc

if st, err := tsNode.Up(ctx); err != nil {
ctrl.Fatalf("Starting tailscale: %v", err)
} else {
log.Printf("Tailscale started, node state %q", st.BackendState)
}

tsql, err := tailsql.NewServer(opts)
if err != nil {
ctrl.Fatalf("Creating tailsql server: %v", err)
}

lst, err := tsNode.Listen("tcp", ":80")
if err != nil {
ctrl.Fatalf("Listen port 80: %v", err)
}

if opts.ServeHTTPS {
// When serving TLS, add a redirect from HTTP on port 80 to HTTPS on 443.
certDomains := tsNode.CertDomains()
if len(certDomains) == 0 {
ctrl.Fatalf("No cert domains available for HTTPS")
}
base := "https://" + certDomains[0]
go http.Serve(lst, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
target := base + r.RequestURI
http.Redirect(w, r, target, http.StatusPermanentRedirect)
}))
log.Printf("Redirecting HTTP to HTTPS at %q", base)

// For the real service, start a separate listener.
// Note: Replaces the port 80 listener.
var err error
lst, err = tsNode.ListenTLS("tcp", ":443")
if err != nil {
ctrl.Fatalf("Listen TLS: %v", err)
}
log.Print("Enabled serving via HTTPS")
}

mux := tsql.NewMux()
tsweb.Debugger(mux)
go http.Serve(lst, mux)
log.Printf("TailSQL started")
<-ctx.Done()
log.Print("TailSQL shutting down...")
return tsNode.Close()
})
}

func runLocalService(ctx context.Context, opts tailsql.Options, port int) error {
tsql, err := tailsql.NewServer(opts)
if err != nil {
log.Fatalf("Creating tailsql server: %v", err)
ctrl.Fatalf("Creating tailsql server: %v", err)
}

mux := tsql.NewMux()
tsweb.Debugger(mux)
if opts.Hostname == "" {
opts.Hostname = "localhost"
}
hsrv := &http.Server{
Addr: fmt.Sprintf("%s:%d", opts.Hostname, *port),
Addr: fmt.Sprintf("localhost:%d", port),
Handler: mux,
}

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
go func() {
<-ctx.Done()
log.Print("Signal received, stopping")
hsrv.Shutdown(context.Background()) // ctx is already terminated
tsql.Close()
}()
log.Printf("Starting tailsql at http://%s", hsrv.Addr)
log.Printf("Starting local tailsql at http://%s", hsrv.Addr)
if err := hsrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
ctrl.Fatalf(err.Error())
}
return nil
}

func generateBasicConfig(path string) {
f, err := os.Create(path)
if err != nil {
log.Fatalf("Create config: %v", err)
ctrl.Fatalf("Create config: %v", err)
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
eerr := enc.Encode(tailsql.Options{
Hostname: "localhost",
Hostname: "tailsql-dev",
LocalState: "tailsql-state.db",
LocalSource: "local",
Sources: []tailsql.DBSpec{{
Expand All @@ -104,11 +205,11 @@ func generateBasicConfig(path string) {
}},
UILinks: []tailsql.UILink{{
Anchor: "source",
URL: "https://github.com/tailscale/tailsql/tree/main/tailsql",
URL: "https://github.com/tailscale/tailsql",
}},
})
cerr := f.Close()
if err := errors.Join(eerr, cerr); err != nil {
log.Fatalf("Write config: %v", err)
ctrl.Fatalf("Write config: %v", err)
}
}
61 changes: 61 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/tailscale/tailsql
go 1.21.0

require (
github.com/creachadair/ctrl v0.1.1
github.com/google/go-cmp v0.5.9
github.com/klauspost/compress v1.16.7
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
Expand All @@ -12,26 +13,85 @@ require (
)

require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.22 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.18.10 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-iptables v0.6.0 // indirect
github.com/dblohm7/wingoes v0.0.0-20230803162905-5c6286bb8c6e // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/illarion/gonotify v1.0.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect
github.com/jsimonetti/rtnetlink v1.3.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.15.1 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect
github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect
github.com/tailscale/wireguard-go v0.0.0-20230710185534-bb2c8f22eccf // indirect
github.com/tcnksm/go-httpstat v0.2.0 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.1 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f // indirect
inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
Expand All @@ -41,4 +101,5 @@ require (
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)
Loading

0 comments on commit c87a612

Please sign in to comment.