Skip to content

Commit 3cf599b

Browse files
authored
Merge pull request #22 from juanfont/json-output
Added JSON-formatted output to CLI
2 parents 32da9c0 + 8ad366f commit 3cf599b

File tree

10 files changed

+165
-45
lines changed

10 files changed

+165
-45
lines changed

README.md

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ An open source implementation of the Tailscale coordination server.
66

77
## Overview
88

9-
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
9+
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
1010

11-
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
11+
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
1212

1313
The control server works as an exchange point of cryptographic public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes.
1414

@@ -20,9 +20,10 @@ Headscale implements this coordination server.
2020
- [x] Node registration through the web flow
2121
- [x] Network changes are relied to the nodes
2222
- [x] ~~Multiuser~~ Namespace support
23-
- [x] Basic routing (advertise & accept)
23+
- [x] Basic routing (advertise & accept)
2424
- [ ] Share nodes between ~~users~~ namespaces
2525
- [x] Node registration via pre-auth keys
26+
- [X] JSON-formatted output
2627
- [ ] ACLs
2728
- [ ] DNS
2829

@@ -42,18 +43,18 @@ Suggestions/PRs welcomed!
4243
```shell
4344
make
4445
```
45-
46+
4647
2. Get yourself a PostgreSQL DB running (yes, [I know](https://tailscale.com/blog/an-unlikely-database-migration/))
4748

48-
```shell
49+
```shell
4950
docker run --name headscale -e POSTGRES_DB=headscale -e \
5051
POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -p 5432:5432 -d postgres
5152
```
5253

5354
3. Set some stuff up (headscale Wireguard keys & the config.json file)
5455
```shell
5556
wg genkey > private.key
56-
wg pubkey < private.key > public.key # not needed
57+
wg pubkey < private.key > public.key # not needed
5758
cp config.json.example config.json
5859
```
5960

@@ -66,7 +67,7 @@ Suggestions/PRs welcomed!
6667
```shell
6768
./headscale serve
6869
```
69-
70+
7071
6. Add your first machine
7172
```shell
7273
tailscale up -login-server YOUR_HEADSCALE_URL
@@ -79,6 +80,22 @@ Suggestions/PRs welcomed!
7980
./headscale -n myfirstnamespace node register YOURMACHINEKEY
8081
```
8182

83+
Alternatively, you can use Auth Keys to register your machines:
84+
85+
1. Create an authkey
86+
```shell
87+
./headscale -n myfirstnamespace preauthkey create --reusable --expiration 24h
88+
```
89+
90+
2. Use the authkey from your machine to register it
91+
```shell
92+
tailscale up -login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY
93+
```
94+
95+
96+
Please bear in mind that all the commands from headscale support adding `-o json` or `-o json-line` to get a nicely JSON-formatted output.
97+
98+
8299
## Configuration reference
83100

84101
Headscale's configuration file is named `config.json` or `config.yaml`. Headscale will look for it in `/etc/headscale`, `~/.headscale` and finally the directory from where the Headscale binary is executed.
@@ -131,7 +148,15 @@ To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/)
131148

132149
## Disclaimer
133150

134-
1. I have nothing to do with Tailscale, or Tailscale Inc.
151+
1. We have nothing to do with Tailscale, or Tailscale Inc.
135152
2. The purpose of writing this was to learn how Tailscale works.
136-
3. I don't use Headscale myself.
153+
3. ~~I don't use Headscale myself.~~
154+
155+
156+
157+
## More on Tailscale
158+
159+
- https://tailscale.com/blog/how-tailscale-works/
160+
- https://tailscale.com/blog/tailscale-key-management/
161+
- https://tailscale.com/blog/an-unlikely-database-migration/
137162

cli.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,45 @@
11
package headscale
22

33
import (
4-
"fmt"
4+
"errors"
55
"log"
66

77
"tailscale.com/wgengine/wgcfg"
88
)
99

1010
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey
11-
func (h *Headscale) RegisterMachine(key string, namespace string) error {
11+
func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, error) {
1212
ns, err := h.GetNamespace(namespace)
1313
if err != nil {
14-
return err
14+
return nil, err
1515
}
1616
mKey, err := wgcfg.ParseHexKey(key)
1717
if err != nil {
18-
log.Printf("Cannot parse client key: %s", err)
19-
return err
18+
return nil, err
2019
}
2120
db, err := h.db()
2221
if err != nil {
2322
log.Printf("Cannot open DB: %s", err)
24-
return err
23+
return nil, err
2524
}
2625
defer db.Close()
2726
m := Machine{}
2827
if db.First(&m, "machine_key = ?", mKey.HexString()).RecordNotFound() {
29-
log.Printf("Cannot find machine with machine key: %s", mKey.Base64())
30-
return err
28+
return nil, errors.New("Machine not found")
3129
}
3230

3331
if m.isAlreadyRegistered() {
34-
fmt.Println("This machine already registered")
35-
return nil
32+
return nil, errors.New("Machine already registered")
3633
}
3734

3835
ip, err := h.getAvailableIP()
3936
if err != nil {
40-
log.Println(err)
41-
return err
37+
return nil, err
4238
}
4339
m.IPAddress = ip.String()
4440
m.NamespaceID = ns.ID
4541
m.Registered = true
4642
m.RegisterMethod = "cli"
4743
db.Save(&m)
48-
fmt.Println("Machine registered 🎉")
49-
return nil
44+
return &m, nil
5045
}

cmd/headscale/cli/namespaces.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55
"log"
6+
"strings"
67

78
"github.com/spf13/cobra"
89
)
@@ -22,34 +23,44 @@ var CreateNamespaceCmd = &cobra.Command{
2223
return nil
2324
},
2425
Run: func(cmd *cobra.Command, args []string) {
26+
o, _ := cmd.Flags().GetString("output")
2527
h, err := getHeadscaleApp()
2628
if err != nil {
2729
log.Fatalf("Error initializing: %s", err)
2830
}
29-
_, err = h.CreateNamespace(args[0])
31+
namespace, err := h.CreateNamespace(args[0])
32+
if strings.HasPrefix(o, "json") {
33+
JsonOutput(namespace, err, o)
34+
return
35+
}
3036
if err != nil {
31-
fmt.Println(err)
37+
fmt.Printf("Error creating namespace: %s\n", err)
3238
return
3339
}
34-
fmt.Printf("Ook.\n")
40+
fmt.Printf("Namespace created\n")
3541
},
3642
}
3743

3844
var ListNamespacesCmd = &cobra.Command{
3945
Use: "list",
4046
Short: "List all the namespaces",
4147
Run: func(cmd *cobra.Command, args []string) {
48+
o, _ := cmd.Flags().GetString("output")
4249
h, err := getHeadscaleApp()
4350
if err != nil {
4451
log.Fatalf("Error initializing: %s", err)
4552
}
46-
ns, err := h.ListNamespaces()
53+
namespaces, err := h.ListNamespaces()
54+
if strings.HasPrefix(o, "json") {
55+
JsonOutput(namespaces, err, o)
56+
return
57+
}
4758
if err != nil {
4859
fmt.Println(err)
4960
return
5061
}
5162
fmt.Printf("ID\tName\n")
52-
for _, n := range *ns {
63+
for _, n := range *namespaces {
5364
fmt.Printf("%d\t%s\n", n.ID, n.Name)
5465
}
5566
},

cmd/headscale/cli/nodes.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55
"log"
6+
"strings"
67

78
"github.com/spf13/cobra"
89
)
@@ -21,17 +22,22 @@ var RegisterCmd = &cobra.Command{
2122
if err != nil {
2223
log.Fatalf("Error getting namespace: %s", err)
2324
}
25+
o, _ := cmd.Flags().GetString("output")
2426

2527
h, err := getHeadscaleApp()
2628
if err != nil {
2729
log.Fatalf("Error initializing: %s", err)
2830
}
29-
err = h.RegisterMachine(args[0], n)
31+
m, err := h.RegisterMachine(args[0], n)
32+
if strings.HasPrefix(o, "json") {
33+
JsonOutput(m, err, o)
34+
return
35+
}
3036
if err != nil {
31-
fmt.Printf("Error: %s", err)
37+
fmt.Printf("Cannot register machine: %s\n", err)
3238
return
3339
}
34-
fmt.Println("Ook.")
40+
fmt.Printf("Machine registered\n")
3541
},
3642
}
3743

@@ -43,12 +49,18 @@ var ListNodesCmd = &cobra.Command{
4349
if err != nil {
4450
log.Fatalf("Error getting namespace: %s", err)
4551
}
52+
o, _ := cmd.Flags().GetString("output")
4653

4754
h, err := getHeadscaleApp()
4855
if err != nil {
4956
log.Fatalf("Error initializing: %s", err)
5057
}
5158
machines, err := h.ListMachinesInNamespace(n)
59+
if strings.HasPrefix(o, "json") {
60+
JsonOutput(machines, err, o)
61+
return
62+
}
63+
5264
if err != nil {
5365
log.Fatalf("Error getting nodes: %s", err)
5466
}

cmd/headscale/cli/preauthkeys.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55
"log"
6+
"strings"
67
"time"
78

89
"github.com/hako/durafmt"
@@ -22,14 +23,20 @@ var ListPreAuthKeys = &cobra.Command{
2223
if err != nil {
2324
log.Fatalf("Error getting namespace: %s", err)
2425
}
26+
o, _ := cmd.Flags().GetString("output")
2527

2628
h, err := getHeadscaleApp()
2729
if err != nil {
2830
log.Fatalf("Error initializing: %s", err)
2931
}
3032
keys, err := h.GetPreAuthKeys(n)
33+
if strings.HasPrefix(o, "json") {
34+
JsonOutput(keys, err, o)
35+
return
36+
}
37+
3138
if err != nil {
32-
fmt.Println(err)
39+
fmt.Printf("Error getting the list of keys: %s\n", err)
3340
return
3441
}
3542
for _, k := range *keys {
@@ -57,6 +64,7 @@ var CreatePreAuthKeyCmd = &cobra.Command{
5764
if err != nil {
5865
log.Fatalf("Error getting namespace: %s", err)
5966
}
67+
o, _ := cmd.Flags().GetString("output")
6068

6169
h, err := getHeadscaleApp()
6270
if err != nil {
@@ -75,11 +83,15 @@ var CreatePreAuthKeyCmd = &cobra.Command{
7583
expiration = &exp
7684
}
7785

78-
_, err = h.CreatePreAuthKey(n, reusable, expiration)
86+
k, err := h.CreatePreAuthKey(n, reusable, expiration)
87+
if strings.HasPrefix(o, "json") {
88+
JsonOutput(k, err, o)
89+
return
90+
}
7991
if err != nil {
8092
fmt.Println(err)
8193
return
8294
}
83-
fmt.Printf("Ook.\n")
95+
fmt.Printf("Key: %s\n", k.Key)
8496
},
8597
}

cmd/headscale/cli/routes.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55
"log"
6+
"strings"
67

78
"github.com/spf13/cobra"
89
)
@@ -26,16 +27,24 @@ var ListRoutesCmd = &cobra.Command{
2627
if err != nil {
2728
log.Fatalf("Error getting namespace: %s", err)
2829
}
30+
o, _ := cmd.Flags().GetString("output")
2931

3032
h, err := getHeadscaleApp()
3133
if err != nil {
3234
log.Fatalf("Error initializing: %s", err)
3335
}
3436
routes, err := h.GetNodeRoutes(n, args[0])
37+
38+
if strings.HasPrefix(o, "json") {
39+
JsonOutput(routes, err, o)
40+
return
41+
}
42+
3543
if err != nil {
3644
fmt.Println(err)
3745
return
3846
}
47+
3948
fmt.Println(routes)
4049
},
4150
}
@@ -54,15 +63,22 @@ var EnableRouteCmd = &cobra.Command{
5463
if err != nil {
5564
log.Fatalf("Error getting namespace: %s", err)
5665
}
66+
o, _ := cmd.Flags().GetString("output")
5767

5868
h, err := getHeadscaleApp()
5969
if err != nil {
6070
log.Fatalf("Error initializing: %s", err)
6171
}
62-
err = h.EnableNodeRoute(n, args[0], args[1])
72+
route, err := h.EnableNodeRoute(n, args[0], args[1])
73+
if strings.HasPrefix(o, "json") {
74+
JsonOutput(route, err, o)
75+
return
76+
}
77+
6378
if err != nil {
6479
fmt.Println(err)
6580
return
6681
}
82+
fmt.Printf("Enabled route %s\n", route)
6783
},
6884
}

0 commit comments

Comments
 (0)