diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..b2515b4 --- /dev/null +++ b/app_test.go @@ -0,0 +1,52 @@ +package headscale + +import ( + "io/ioutil" + "os" + "testing" + + _ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver + + "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + check.TestingT(t) +} + +var _ = check.Suite(&Suite{}) + +type Suite struct{} + +var tmpDir string +var h Headscale + +func (s *Suite) SetUpTest(c *check.C) { + s.ResetDB(c) +} + +func (s *Suite) TearDownTest(c *check.C) { + os.RemoveAll(tmpDir) +} + +func (s *Suite) ResetDB(c *check.C) { + if len(tmpDir) != 0 { + os.RemoveAll(tmpDir) + } + var err error + tmpDir, err = ioutil.TempDir("", "autoygg-client-test") + if err != nil { + c.Fatal(err) + } + cfg := Config{} + + h = Headscale{ + cfg: cfg, + dbType: "sqlite3", + dbString: tmpDir + "/headscale_test.db", + } + err = h.initDB() + if err != nil { + c.Fatal(err) + } +} diff --git a/cmd/headscale/cli/namespaces.go b/cmd/headscale/cli/namespaces.go index 1fb2de6..2bc9171 100644 --- a/cmd/headscale/cli/namespaces.go +++ b/cmd/headscale/cli/namespaces.go @@ -41,6 +41,34 @@ var CreateNamespaceCmd = &cobra.Command{ }, } +var DestroyNamespaceCmd = &cobra.Command{ + Use: "destroy NAME", + Short: "Destroys a namespace", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("Missing parameters") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + o, _ := cmd.Flags().GetString("output") + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + err = h.DestroyNamespace(args[0]) + if strings.HasPrefix(o, "json") { + JsonOutput(map[string]string{"Result": "Namespace destroyed"}, err, o) + return + } + if err != nil { + fmt.Printf("Error destroying namespace: %s\n", err) + return + } + fmt.Printf("Namespace destroyed\n") + }, +} + var ListNamespacesCmd = &cobra.Command{ Use: "list", Short: "List all the namespaces", diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index ccc75e8..5a2cf06 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -115,6 +115,7 @@ func main() { cli.NamespaceCmd.AddCommand(cli.CreateNamespaceCmd) cli.NamespaceCmd.AddCommand(cli.ListNamespacesCmd) + cli.NamespaceCmd.AddCommand(cli.DestroyNamespaceCmd) cli.NodeCmd.AddCommand(cli.ListNodesCmd) cli.NodeCmd.AddCommand(cli.RegisterCmd) diff --git a/namespaces.go b/namespaces.go index 94ea288..f2b1215 100644 --- a/namespaces.go +++ b/namespaces.go @@ -1,7 +1,6 @@ package headscale import ( - "fmt" "log" "time" @@ -9,6 +8,10 @@ import ( "tailscale.com/tailcfg" ) +const errorNamespaceExists = Error("Namespace already exists") +const errorNamespaceNotFound = Error("Namespace not found") +const errorNamespaceNotEmpty = Error("Namespace not empty") + // Namespace is the way Headscale implements the concept of users in Tailscale // // At the end of the day, users in Tailscale are some kind of 'bubbles' or namespaces @@ -30,7 +33,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) { n := Namespace{} if err := db.Where("name = ?", name).First(&n).Error; err == nil { - return nil, fmt.Errorf("Namespace already exists") + return nil, errorNamespaceExists } n.Name = name if err := db.Create(&n).Error; err != nil { @@ -40,6 +43,37 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) { return &n, nil } +// DestroyNamespace destroys a Namespace. Returns error if the Namespace does +// not exist or if there are machines associated with it. +func (h *Headscale) DestroyNamespace(name string) error { + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return err + } + defer db.Close() + + n, err := h.GetNamespace(name) + if err != nil { + return errorNamespaceNotFound + } + + m, err := h.ListMachinesInNamespace(name) + if err != nil { + return err + } + if len(*m) > 0 { + return errorNamespaceNotEmpty + } + + err = db.Unscoped().Delete(&n).Error + if err != nil { + return err + } + + return nil +} + // GetNamespace fetches a namespace by name func (h *Headscale) GetNamespace(name string) (*Namespace, error) { db, err := h.db() @@ -51,7 +85,7 @@ func (h *Headscale) GetNamespace(name string) (*Namespace, error) { n := Namespace{} if db.First(&n, "name = ?", name).RecordNotFound() { - return nil, fmt.Errorf("Namespace not found") + return nil, errorNamespaceNotFound } return &n, nil } diff --git a/namespaces_test.go b/namespaces_test.go new file mode 100644 index 0000000..42ab487 --- /dev/null +++ b/namespaces_test.go @@ -0,0 +1,57 @@ +package headscale + +import ( + //_ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver + + "gopkg.in/check.v1" +) + +var _ = check.Suite(&Suite{}) + +func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) { + n, err := h.CreateNamespace("test") + c.Assert(err, check.IsNil) + c.Assert(n.Name, check.Equals, "test") + + ns, err := h.ListNamespaces() + c.Assert(err, check.IsNil) + c.Assert(len(*ns), check.Equals, 1) + + err = h.DestroyNamespace("test") + c.Assert(err, check.IsNil) + + _, err = h.GetNamespace("test") + c.Assert(err, check.NotNil) +} + +func (s *Suite) TestDestroyNamespaceErrors(c *check.C) { + err := h.DestroyNamespace("test") + c.Assert(err, check.Equals, errorNamespaceNotFound) + + n, err := h.CreateNamespace("test") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, nil) + c.Assert(err, check.IsNil) + + db, err := h.db() + if err != nil { + c.Fatal(err) + } + defer db.Close() + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + } + db.Save(&m) + + err = h.DestroyNamespace("test") + c.Assert(err, check.Equals, errorNamespaceNotEmpty) +} diff --git a/preauth_keys_test.go b/preauth_keys_test.go index cf13bb9..72a6bfc 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -1,52 +1,11 @@ package headscale import ( - "fmt" - "io/ioutil" - "os" - "testing" "time" - _ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver - "gopkg.in/check.v1" ) -func Test(t *testing.T) { - check.TestingT(t) -} - -var _ = check.Suite(&Suite{}) - -type Suite struct{} - -var tmpDir string -var h Headscale - -func (s *Suite) SetUpSuite(c *check.C) { - var err error - tmpDir, err = ioutil.TempDir("", "autoygg-client-test") - if err != nil { - c.Fatal(err) - } - fmt.Printf("tmpDir is %s\n", tmpDir) - cfg := Config{} - - h = Headscale{ - cfg: cfg, - dbType: "sqlite3", - dbString: tmpDir + "/headscale_test.db", - } - err = h.initDB() - if err != nil { - c.Fatal(err) - } -} - -func (s *Suite) TearDownSuite(c *check.C) { - os.RemoveAll(tmpDir) -} - func (*Suite) TestCreatePreAuthKey(c *check.C) { _, err := h.CreatePreAuthKey("bogus", true, nil)