AppManager: Clean up VPN node and auth keys upon app removal
Change-Id: Ie76278556247d16806ba81286621adca973e3f6e
diff --git a/core/headscale/client.go b/core/headscale/client.go
index ce82291..18c37df 100644
--- a/core/headscale/client.go
+++ b/core/headscale/client.go
@@ -1,9 +1,12 @@
package main
import (
+ "bytes"
+ "encoding/json"
"errors"
"fmt"
"os/exec"
+ "strconv"
"strings"
)
@@ -33,21 +36,81 @@
// TODO(giolekva): make expiration configurable, and auto-refresh
cmd := exec.Command("headscale", c.config, "--user", user, "preauthkeys", "create", "--reusable", "--expiration", "365d")
out, err := cmd.Output()
+ fmt.Println(string(out))
if err != nil {
return "", err
}
- fmt.Println(string(out))
return extractLastLine(string(out))
}
+func (c *client) expirePreAuthKey(user, authKey string) error {
+ cmd := exec.Command("headscale", c.config, "--user", user, "preauthkeys", "expire", authKey)
+ out, err := cmd.Output()
+ fmt.Println(string(out))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *client) expireUserNode(user, node string) error {
+ id, err := c.getNodeId(user, node)
+ if err != nil {
+ return err
+ }
+ cmd := exec.Command("headscale", c.config, "node", "expire", "--identifier", id)
+ out, err := cmd.Output()
+ fmt.Println(string(out))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *client) removeUserNode(user, node string) error {
+ id, err := c.getNodeId(user, node)
+ if err != nil {
+ return err
+ }
+ cmd := exec.Command("headscale", c.config, "node", "delete", "--identifier", id, "--force")
+ out, err := cmd.Output()
+ fmt.Println(string(out))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func (c *client) enableRoute(id string) error {
- // TODO(giolekva): make expiration configurable, and auto-refresh
cmd := exec.Command("headscale", c.config, "routes", "enable", "-r", id)
out, err := cmd.Output()
fmt.Println(string(out))
return err
}
+type nodeInfo struct {
+ Id int `json:"id"`
+ Name string `json:"name"`
+}
+
+func (c *client) getNodeId(user, node string) (string, error) {
+ cmd := exec.Command("headscale", c.config, "--user", user, "node", "list", "-o", "json")
+ out, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ var nodes []nodeInfo
+ if err := json.NewDecoder(bytes.NewReader(out)).Decode(&nodes); err != nil {
+ return "", err
+ }
+ for _, n := range nodes {
+ if n.Name == node {
+ return strconv.Itoa(n.Id), nil
+ }
+ }
+ return "", fmt.Errorf("not found")
+}
+
func extractLastLine(s string) (string, error) {
items := strings.Split(s, "\n")
for i := len(items) - 1; i >= 0; i-- {
diff --git a/core/headscale/main.go b/core/headscale/main.go
index da0ff6c..698d9d2 100644
--- a/core/headscale/main.go
+++ b/core/headscale/main.go
@@ -88,6 +88,9 @@
r := mux.NewRouter()
r.HandleFunc("/sync-users", s.handleSyncUsers).Methods(http.MethodGet)
r.HandleFunc("/user/{user}/preauthkey", s.createReusablePreAuthKey).Methods(http.MethodPost)
+ r.HandleFunc("/user/{user}/preauthkey", s.expireReusablePreAuthKey).Methods(http.MethodDelete)
+ r.HandleFunc("/user/{user}/node/{node}/expire", s.expireUserNode).Methods(http.MethodPost)
+ r.HandleFunc("/user/{user}/node/{node}", s.removeUserNode).Methods(http.MethodDelete)
r.HandleFunc("/user", s.createUser).Methods(http.MethodPost)
r.HandleFunc("/routes/{id}/enable", s.enableRoute).Methods(http.MethodPost)
go func() {
@@ -132,6 +135,63 @@
}
}
+type expirePreAuthKeyReq struct {
+ AuthKey string `json:"authKey"`
+}
+
+func (s *server) expireReusablePreAuthKey(w http.ResponseWriter, r *http.Request) {
+ user, ok := mux.Vars(r)["user"]
+ if !ok {
+ http.Error(w, "no user", http.StatusBadRequest)
+ return
+ }
+ var req expirePreAuthKeyReq
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if err := s.client.expirePreAuthKey(user, req.AuthKey); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (s *server) expireUserNode(w http.ResponseWriter, r *http.Request) {
+ fmt.Println("expire node")
+ user, ok := mux.Vars(r)["user"]
+ if !ok {
+ http.Error(w, "no user", http.StatusBadRequest)
+ return
+ }
+ node, ok := mux.Vars(r)["node"]
+ if !ok {
+ http.Error(w, "no user", http.StatusBadRequest)
+ return
+ }
+ if err := s.client.expireUserNode(user, node); err != nil {
+ fmt.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (s *server) removeUserNode(w http.ResponseWriter, r *http.Request) {
+ user, ok := mux.Vars(r)["user"]
+ if !ok {
+ http.Error(w, "no user", http.StatusBadRequest)
+ return
+ }
+ node, ok := mux.Vars(r)["node"]
+ if !ok {
+ http.Error(w, "no user", http.StatusBadRequest)
+ return
+ }
+ if err := s.client.removeUserNode(user, node); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
func (s *server) handleSyncUsers(_ http.ResponseWriter, _ *http.Request) {
go s.syncUsers()
}
diff --git a/core/installer/app.go b/core/installer/app.go
index 3a16abe..91f290f 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -202,7 +202,7 @@
networks []Network,
values map[string]any,
charts map[string]helmv2.HelmChartTemplateSpec,
- vpnKeyGen VPNAuthKeyGenerator,
+ vpnKeyGen VPNAPIClient,
) (EnvAppRendered, error)
}
@@ -459,7 +459,7 @@
networks []Network,
values map[string]any,
charts map[string]helmv2.HelmChartTemplateSpec,
- vpnKeyGen VPNAuthKeyGenerator,
+ vpnKeyGen VPNAPIClient,
) (EnvAppRendered, error) {
derived, err := deriveValues(values, values, a.Schema(), networks, vpnKeyGen)
if err != nil {
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 013c05d..d31d9ff 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -30,13 +30,13 @@
var ErrorNotFound = errors.New("not found")
type AppManager struct {
- l sync.Locker
- repoIO soft.RepoIO
- nsc NamespaceCreator
- jc JobCreator
- hf HelmFetcher
- vpnKeyGen VPNAuthKeyGenerator
- appDirRoot string
+ l sync.Locker
+ repoIO soft.RepoIO
+ nsc NamespaceCreator
+ jc JobCreator
+ hf HelmFetcher
+ vpnAPIClient VPNAPIClient
+ appDirRoot string
}
func NewAppManager(
@@ -44,7 +44,7 @@
nsc NamespaceCreator,
jc JobCreator,
hf HelmFetcher,
- vpnKeyGen VPNAuthKeyGenerator,
+ vpnKeyGen VPNAPIClient,
appDirRoot string,
) (*AppManager, error) {
return &AppManager{
@@ -468,7 +468,7 @@
RepoAddr: m.repoIO.FullAddress(),
AppDir: appDir,
}
- rendered, err := app.Render(release, env, networks, values, nil, m.vpnKeyGen)
+ rendered, err := app.Render(release, env, networks, values, nil, m.vpnAPIClient)
if err != nil {
return ReleaseResources{}, err
}
@@ -500,7 +500,7 @@
if o.FetchContainerImages {
release.ImageRegistry = imageRegistry
}
- rendered, err = app.Render(release, env, networks, values, localCharts, m.vpnKeyGen)
+ rendered, err = app.Render(release, env, networks, values, localCharts, m.vpnAPIClient)
if err != nil {
return ReleaseResources{}, err
}
@@ -593,7 +593,7 @@
if err != nil {
return ReleaseResources{}, err
}
- rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts, m.vpnKeyGen)
+ rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts, m.vpnAPIClient)
if err != nil {
return ReleaseResources{}, err
}
@@ -613,14 +613,14 @@
if err := m.repoIO.Pull(); err != nil {
return err
}
- var portForward []PortForward
+ var cfg renderedInstance
if _, err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
instanceDir := filepath.Join(m.appDirRoot, instanceId)
renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
if err != nil {
return "", err
}
- portForward = renderedCfg.PortForward
+ cfg = renderedCfg
r.RemoveDir(instanceDir)
kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
kust, err := soft.ReadKustomization(r, kustPath)
@@ -633,9 +633,22 @@
}); err != nil {
return err
}
- if err := closePorts(portForward); err != nil {
+ if err := closePorts(cfg.PortForward); err != nil {
return err
}
+ for vmName, vmCfg := range cfg.Out.VM {
+ if vmCfg.VPN.Enabled {
+ if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil {
+ return err
+ }
+ if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
+ return err
+ }
+ if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil {
+ return err
+ }
+ }
+ }
return nil
}
@@ -951,6 +964,19 @@
type renderedInstance struct {
LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
PortForward []PortForward `json:"portForward"`
+ Out outRendered `json:"out"`
+}
+
+type outRendered struct {
+ VM map[string]vmRendered `json:"vm"`
+}
+
+type vmRendered struct {
+ Username string `json:"username"`
+ VPN struct {
+ Enabled bool `json:"enabled"`
+ AuthKey string `json:"authKey"`
+ } `json:"vpn"`
}
func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
diff --git a/core/installer/derived.go b/core/installer/derived.go
index 508cd3e..d99f02b 100644
--- a/core/installer/derived.go
+++ b/core/installer/derived.go
@@ -69,7 +69,7 @@
values any,
schema Schema,
networks []Network,
- vpnKeyGen VPNAuthKeyGenerator,
+ vpnKeyGen VPNAPIClient,
) (map[string]any, error) {
ret := make(map[string]any)
for _, f := range schema.Fields() {
@@ -100,7 +100,7 @@
return nil, fmt.Errorf("could not resolve username: %+v %s %+v", def.Meta(), v, root)
}
}
- authKey, err := vpnKeyGen.Generate(username)
+ authKey, err := vpnKeyGen.GenerateAuthKey(username)
if err != nil {
return nil, err
}
diff --git a/core/installer/derived_test.go b/core/installer/derived_test.go
index 7a34154..8f638b3 100644
--- a/core/installer/derived_test.go
+++ b/core/installer/derived_test.go
@@ -6,10 +6,22 @@
type testKeyGen struct{}
-func (g testKeyGen) Generate(username string) (string, error) {
+func (g testKeyGen) GenerateAuthKey(username string) (string, error) {
return username, nil
}
+func (g testKeyGen) ExpireKey(username, key string) error {
+ return nil
+}
+
+func (g testKeyGen) ExpireNode(username, node string) error {
+ return nil
+}
+
+func (g testKeyGen) RemoveNode(username, node string) error {
+ return nil
+}
+
func TestDeriveVPNAuthKey(t *testing.T) {
schema := structSchema{
"input",
diff --git a/core/installer/vpn.go b/core/installer/vpn.go
index 4739bf2..01161df 100644
--- a/core/installer/vpn.go
+++ b/core/installer/vpn.go
@@ -2,25 +2,34 @@
import (
"bytes"
+ "encoding/json"
"errors"
"fmt"
"io"
"net/http"
+ "net/url"
)
-type VPNAuthKeyGenerator interface {
- Generate(username string) (string, error)
+type VPNAPIClient interface {
+ GenerateAuthKey(username string) (string, error)
+ ExpireKey(username, key string) error
+ ExpireNode(username, node string) error
+ RemoveNode(username, node string) error
}
type headscaleAPIClient struct {
+ c *http.Client
apiAddr string
}
-func NewHeadscaleAPIClient(apiAddr string) VPNAuthKeyGenerator {
- return &headscaleAPIClient{apiAddr}
+func NewHeadscaleAPIClient(apiAddr string) VPNAPIClient {
+ return &headscaleAPIClient{
+ &http.Client{},
+ apiAddr,
+ }
}
-func (g *headscaleAPIClient) Generate(username string) (string, error) {
+func (g *headscaleAPIClient) GenerateAuthKey(username string) (string, error) {
resp, err := http.Post(fmt.Sprintf("%s/user/%s/preauthkey", g.apiAddr, username), "application/json", nil)
if err != nil {
return "", err
@@ -32,3 +41,70 @@
}
return buf.String(), nil
}
+
+type expirePreAuthKeyReq struct {
+ AuthKey string `json:"authKey"`
+}
+
+func (g *headscaleAPIClient) ExpireKey(username, key string) error {
+ addr, err := url.Parse(fmt.Sprintf("%s/user/%s/preauthkey", g.apiAddr, username))
+ if err != nil {
+ return err
+ }
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(expirePreAuthKeyReq{key}); err != nil {
+ return err
+ }
+ resp, err := g.c.Do(&http.Request{
+ URL: addr,
+ Method: http.MethodDelete,
+ Body: io.NopCloser(&buf),
+ })
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != http.StatusOK {
+ var buf bytes.Buffer
+ io.Copy(&buf, resp.Body)
+ return errors.New(buf.String())
+ }
+ return nil
+}
+
+func (g *headscaleAPIClient) ExpireNode(username, node string) error {
+ resp, err := g.c.Post(
+ fmt.Sprintf("%s/user/%s/node/%s/expire", g.apiAddr, username, node),
+ "text/plain",
+ nil,
+ )
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != http.StatusOK {
+ var buf bytes.Buffer
+ io.Copy(&buf, resp.Body)
+ return errors.New(buf.String())
+ }
+ return nil
+}
+
+func (g *headscaleAPIClient) RemoveNode(username, node string) error {
+ addr, err := url.Parse(fmt.Sprintf("%s/user/%s/node/%s", g.apiAddr, username, node))
+ if err != nil {
+ return err
+ }
+ resp, err := g.c.Do(&http.Request{
+ URL: addr,
+ Method: http.MethodDelete,
+ Body: nil,
+ })
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != http.StatusOK {
+ var buf bytes.Buffer
+ io.Copy(&buf, resp.Body)
+ return errors.New(buf.String())
+ }
+ return nil
+}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 57de8b2..9f60449 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -103,7 +103,7 @@
env installer.EnvConfig
nsc installer.NamespaceCreator
jc installer.JobCreator
- vpnKeyGen installer.VPNAuthKeyGenerator
+ vpnKeyGen installer.VPNAPIClient
workers map[string]map[string]struct{}
appConfigs map[string]appConfig
tmplts dodoAppTmplts
@@ -135,7 +135,7 @@
envAppManagerAddr string,
nsc installer.NamespaceCreator,
jc installer.JobCreator,
- vpnKeyGen installer.VPNAuthKeyGenerator,
+ vpnKeyGen installer.VPNAPIClient,
env installer.EnvConfig,
external bool,
fetchUsersAddr string,