Skip to content
This repository has been archived by the owner on Apr 12, 2023. It is now read-only.

Commit

Permalink
add update directory (#83)
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Mason <mimason@equinix.com>
  • Loading branch information
mikemrm committed Feb 24, 2023
1 parent c2cde43 commit ae0c631
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 0 deletions.
7 changes: 7 additions & 0 deletions api/v1/types.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions app/v1/callback/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
// events.
type Config struct {
CreateDirectory func(context.Context, *apiv1.Directory) error
UpdateDirectory func(context.Context, *apiv1.Directory) error
DeleteDirectory func(context.Context, apiv1.DirectoryID) error
}

Expand Down Expand Up @@ -43,6 +44,17 @@ func (s *AppStorageWithCallback) CreateDirectory(ctx context.Context, d *apiv1.D
return d, nil
}

// UpdateDirectory updates the directory in storage and then calls the callback.
func (s *AppStorageWithCallback) UpdateDirectory(ctx context.Context, d *apiv1.Directory) error {
if err := s.impl.UpdateDirectory(ctx, d); err != nil {
return err
}
if err := s.cfg.UpdateDirectory(ctx, d); err != nil {
return err
}
return nil
}

func (s *AppStorageWithCallback) DeleteDirectory(
ctx context.Context, id apiv1.DirectoryID,
) ([]*apiv1.Directory, error) {
Expand Down
5 changes: 5 additions & 0 deletions app/v1/sql/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func (s *sqlstorage) CreateDirectory(ctx context.Context, d *apiv1.Directory) (*
return d, nil
}

func (s *sqlstorage) UpdateDirectory(ctx context.Context, d *apiv1.Directory) error {
// nothing to be done
return nil
}

func (s *sqlstorage) DeleteDirectory(ctx context.Context, id apiv1.DirectoryID) ([]*apiv1.Directory, error) {
// soft delete directory
var affected []*apiv1.Directory
Expand Down
34 changes: 34 additions & 0 deletions client/v1/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,40 @@ func (c *httpClient) CreateDirectory(
return &dir, nil
}

func (c *httpClient) UpdateDirectory(
ctx context.Context,
id v1.DirectoryID,
udr *v1.UpdateDirectoryRequest,
) (*v1.DirectoryFetch, error) {
r, err := c.encode(udr)
if err != nil {
return nil, err
}

path, err := url.JoinPath("/api/v1/directories", id.String())
if err != nil {
return nil, fmt.Errorf("error updating directory: %w", err)
}

resp, err := c.DoRaw(ctx, http.MethodPatch, path, r)
if err != nil {
return nil, fmt.Errorf("error updating directory: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error updating directory: %s", resp.Status)
}

var dir v1.DirectoryFetch
err = dir.Parse(resp.Body)
if err != nil {
return nil, fmt.Errorf("error parsing response: %w", err)
}

return &dir, nil
}

func (c *httpClient) CreateRoot(
ctx context.Context,
cdr *v1.CreateDirectoryRequest,
Expand Down
1 change: 1 addition & 0 deletions client/v1/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type ReadOnlyClient interface {
type Client interface {
ReadOnlyClient
CreateDirectory(c context.Context, r *v1.CreateDirectoryRequest, parent v1.DirectoryID) (*v1.DirectoryFetch, error)
UpdateDirectory(c context.Context, id v1.DirectoryID, r *v1.UpdateDirectoryRequest) (*v1.DirectoryFetch, error)
DeleteDirectory(c context.Context, id v1.DirectoryID) (*v1.DirectoryList, error)
}

Expand Down
50 changes: 50 additions & 0 deletions internal/httpsrv/treemanager/treemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func newHandler(

r.GET("/api/v1/directories/:id", authMW.AuthRequired(), getDirectory(s))
r.POST("/api/v1/directories/:id", authMW.AuthRequired(), createDirectory(s))
r.PATCH("/api/v1/directories/:id", authMW.AuthRequired(), updateDirectory(s))
r.DELETE("/api/v1/directories/:id", authMW.AuthRequired(), deleteDirectory(s))

r.GET("/api/v1/directories/:id/children", authMW.AuthRequired(), listChildren(s))
Expand Down Expand Up @@ -223,6 +224,55 @@ func createDirectory(s *common.Server) gin.HandlerFunc {
}
}

func updateDirectory(s *common.Server) gin.HandlerFunc {
return func(c *gin.Context) {
idstr := c.Param("id")

id, err := v1.ParseDirectoryID(idstr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid id",
})
return
}

var req v1.UpdateDirectoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

d, err := s.T.GetDirectory(c, id)
if errors.Is(err, storage.ErrDirectoryNotFound) {
c.JSON(http.StatusNotFound, gin.H{
"error": "directory not found",
})
return
}

if req.Name != "" {
d.Name = req.Name
}

if req.Metadata != nil {
d.Metadata = req.Metadata
}

if err := s.T.UpdateDirectory(c, d); err != nil {
s.L.Error("error updating directory", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to update directory",
})
return
}

c.JSON(http.StatusOK, &v1.DirectoryFetch{
Directory: *d,
Version: v1.APIVersion,
})
}
}

func deleteDirectory(s *common.Server) gin.HandlerFunc {
return func(c *gin.Context) {
idstr := c.Param("id")
Expand Down
97 changes: 97 additions & 0 deletions internal/httpsrv/treemanager/treemanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,33 @@ func TestDirectoryOperations(t *testing.T) {
// The directory should be the parent.
assert.Equal(t, rd.Directory.Id, listrd.Directories[0], "directory is not the same")

d2, err := cli.UpdateDirectory(context.Background(), d.Directory.Id, &apiv1.UpdateDirectoryRequest{
Name: "test2",
Metadata: &apiv1.DirectoryMetadata{
"item1": "value1",
},
})
assert.NoError(t, err, "expected no error updating directory")
assert.Equal(t, "test2", d2.Directory.Name, "expected name to be updated")
assert.Contains(t, map[string]string(*d2.Directory.Metadata), "item1", "expected metadata to be updated")

d2, err = cli.UpdateDirectory(context.Background(), d.Directory.Id, &apiv1.UpdateDirectoryRequest{
Name: "",
Metadata: &apiv1.DirectoryMetadata{
"item2": "value2",
},
})
assert.NoError(t, err, "expected no error updating directory")
assert.Equal(t, "test2", d2.Directory.Name, "expected name to be not change")
assert.Contains(t, map[string]string(*d2.Directory.Metadata), "item2", "expected metadata to be updated")

d2, err = cli.UpdateDirectory(context.Background(), d.Directory.Id, &apiv1.UpdateDirectoryRequest{
Name: "test3",
})
assert.NoError(t, err, "expected no error updating directory")
assert.Equal(t, "test3", d2.Directory.Name, "expected name to be updated")
assert.Contains(t, map[string]string(*d2.Directory.Metadata), "item2", "expected metadata to not be updated")

// Delete directory
affected, err := cli.DeleteDirectory(context.Background(), d.Directory.Id)
assert.NoError(t, err, "error deleting child directory")
Expand Down Expand Up @@ -286,6 +313,76 @@ func TestDirectoryOperations(t *testing.T) {
resp.Body.Close()
}

type mockDriver struct {
storage.DirectoryAdmin
UpdateError error
}

// UpdateDirectory returns UpdateError if not nil.
func (m *mockDriver) UpdateDirectory(ctx context.Context, d *apiv1.Directory) error {
if m.UpdateError != nil {
return m.UpdateError
}

return m.DirectoryAdmin.UpdateDirectory(ctx, d)
}

func TestUpdateDirectoryError(t *testing.T) {
t.Parallel()

auditBuf := &strings.Builder{}
skt := testutils.NewUnixsocketPath(t)

memDriver, _ := newMemoryStorage(t)

// no simple way to make the driver return an error, so force an error to fully test.
updateErrorStore := &mockDriver{
DirectoryAdmin: memDriver,
UpdateError: fmt.Errorf("update error"),
}

srv := newTestServer(t, skt, updateErrorStore, nil, auditBuf)

defer func() {
err := srv.Shutdown()
assert.NoError(t, err, "error shutting down server")
}()

go testutils.RunTestServer(t, srv)

cli := testutils.NewTestClient(t, skt, getStubServerAddress(t, skt), nil)

testutils.WaitForServer(t, cli)

// Create a new root.
rd, err := cli.CreateRoot(context.Background(), &apiv1.CreateDirectoryRequest{
Version: apiv1.APIVersion,
Name: "root",
})
assert.NoError(t, err, "error creating root")
assert.NotNil(t, rd, "root directory is nil")

// Check that we have an audit log for this
assert.Contains(t, auditBuf.String(), "POST:/api/v1/roots")
auditBuf.Reset()

// Create a new directory.
d, err := cli.CreateDirectory(context.Background(), &apiv1.CreateDirectoryRequest{
Version: apiv1.APIVersion,
Name: "test",
}, rd.Directory.Id)
assert.NoError(t, err, "error creating directory")
assert.NotNil(t, d, "directory is nil")

_, err = cli.UpdateDirectory(context.Background(), d.Directory.Id, &apiv1.UpdateDirectoryRequest{
Name: "test2",
Metadata: &apiv1.DirectoryMetadata{
"item1": "value1",
},
})
assert.Error(t, err, "expected error updating directory")
}

func TestErroneousCalls(t *testing.T) {
t.Parallel()

Expand Down
26 changes: 26 additions & 0 deletions storage/crdb/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ func (t *Driver) CreateDirectory(ctx context.Context, d *v1.Directory) (*v1.Dire
return d, nil
}

// UpdateDirectory updates the directory.
func (t *Driver) UpdateDirectory(ctx context.Context, d *v1.Directory) error {
if t.readOnly {
return storage.ErrReadOnly
}

if d.Metadata == nil {
d.Metadata = &v1.DirectoryMetadata{}
}

err := t.db.QueryRowContext(ctx, `
UPDATE directories
SET
name = $1,
metadata = $2,
updated_at = NOW()
WHERE id = $3
RETURNING updated_at
`, d.Name, d.Metadata, d.Id).Scan(&d.UpdatedAt)
if err != nil {
return fmt.Errorf("error updating directory: %w", err)
}

return nil
}

// DeleteDirectory soft deletes the provided directory id.
// If the provided directory has children, all child directories are soft deleted as well.
func (t *Driver) DeleteDirectory(ctx context.Context, id v1.DirectoryID) ([]*v1.Directory, error) {
Expand Down
50 changes: 50 additions & 0 deletions storage/crdb/driver/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -144,6 +145,55 @@ func TestCreateAndGetDirectory(t *testing.T) {
assert.Equal(t, 1, count, "should have 1 rows")
}

func TestCreateAndUpdateDirectory(t *testing.T) {
t.Parallel()

db := utils.GetNewTestDB(t, baseDBURL)
store := driver.NewDirectoryDriver(db)

rootdir := withRootDir(t, store)

d := &v1.Directory{
Name: "testdir",
Parent: &rootdir.Id,
}

rd, err := store.CreateDirectory(context.Background(), d)
assert.NoError(t, err, "error creating directory")
assert.NotNil(t, rd.Metadata, "metadata should not be nil")
assert.Equal(t, d.Name, rd.Name, "name should match")
assert.Equal(t, d.Parent, rd.Parent, "parent id should match")

oldUpdatedAt := rd.UpdatedAt

rd.Name = "newtestdir"
rd.Metadata = nil

// update directory
err = store.UpdateDirectory(context.Background(), rd)
assert.NoError(t, err, "error querying db")
assert.NotEqual(t, oldUpdatedAt, rd.UpdatedAt, "original directory should not have old UpdatedAt")

// query database to ensure values updated
var (
newName string
newUpdatedAt time.Time
)

err = db.QueryRow("SELECT name, updated_at FROM directories WHERE id = $1", rd.Id).Scan(&newName, &newUpdatedAt)
assert.NoError(t, err, "error querying db")

assert.Equal(t, "newtestdir", newName, "database should have new name")
assert.NotEqual(t, oldUpdatedAt, newUpdatedAt, "database should not have old UpdatedAt")
assert.Equal(t, rd.UpdatedAt, newUpdatedAt, "database and directory should have matching UpdatedAt")

// enable read only
driver.WithReadOnly()(store)

err = store.UpdateDirectory(context.Background(), rd)
assert.Error(t, err, "read only error should've been returned")
}

func TestCreateDirectoryWithParentThatDoesntExist(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions storage/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type RootReader interface {
// Writer is the interface that allows doing basic write operations.
type Writer interface {
CreateDirectory(ctx context.Context, d *v1.Directory) (*v1.Directory, error)
UpdateDirectory(ctx context.Context, d *v1.Directory) error
DeleteDirectory(ctx context.Context, id v1.DirectoryID) ([]*v1.Directory, error)
}

Expand Down
11 changes: 11 additions & 0 deletions storage/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ func (t *Driver) CreateDirectory(ctx context.Context, d *v1.Directory) (*v1.Dire
return dir, nil
}

// UpdateDirectory updates the directory provided.
func (t *Driver) UpdateDirectory(ctx context.Context, d *v1.Directory) error {
if d.Metadata == nil {
d.Metadata = &v1.DirectoryMetadata{}
}

t.dirMap.Store(d.Id, d)

return nil
}

// DeleteDirectory deletes a directory.
func (t *Driver) DeleteDirectory(ctx context.Context, id v1.DirectoryID) ([]*v1.Directory, error) {
dir, err := t.GetDirectory(ctx, id)
Expand Down
Loading

0 comments on commit ae0c631

Please sign in to comment.