From 3d7431193158d25d34f4287dbcba44220cdaebe2 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Thu, 3 Oct 2024 00:13:09 +0200 Subject: feat(tfstated): implement states locking --- cmd/tfstated/lock.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ cmd/tfstated/post.go | 11 +++++++++-- cmd/tfstated/routes.go | 2 ++ cmd/tfstated/unlock.go | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 cmd/tfstated/lock.go create mode 100644 cmd/tfstated/unlock.go (limited to 'cmd') diff --git a/cmd/tfstated/lock.go b/cmd/tfstated/lock.go new file mode 100644 index 0000000..e34c5b5 --- /dev/null +++ b/cmd/tfstated/lock.go @@ -0,0 +1,49 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "time" + + "git.adyxax.org/adyxax/tfstated/pkg/database" +) + +type lockRequest struct { + Created time.Time `json:"Created"` + ID string `json:"ID"` + Info string `json:"Info"` + Operation string `json:"Operation"` + Path string `json:"Path"` + Version string `json:"Version"` + Who string `json:"Who"` +} + +func handleLock(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + _ = encode(w, http.StatusBadRequest, + fmt.Errorf("no state path provided, cannot LOCK /")) + return + } + + var lock lockRequest + if err := decode(r, &lock); err != nil { + _ = encode(w, http.StatusBadRequest, err) + return + } + if success, err := db.SetLockOrGetExistingLock(r.URL.Path, &lock); err != nil { + if errors.Is(err, sql.ErrNoRows) { + _ = encode(w, http.StatusNotFound, + fmt.Errorf("state path not found: %s", r.URL.Path)) + } else { + _ = errorResponse(w, http.StatusInternalServerError, err) + } + } else if success { + w.WriteHeader(http.StatusOK) + } else { + _ = encode(w, http.StatusConflict, lock) + } + }) +} diff --git a/cmd/tfstated/post.go b/cmd/tfstated/post.go index b88109e..1d570b3 100644 --- a/cmd/tfstated/post.go +++ b/cmd/tfstated/post.go @@ -16,13 +16,20 @@ func handlePost(db *database.DB) http.Handler { ) return } + + id := r.URL.Query().Get("ID") + data, err := io.ReadAll(r.Body) if err != nil { _ = errorResponse(w, http.StatusBadRequest, err) return } - if err := db.SetState(r.URL.Path, data); err != nil { - _ = errorResponse(w, http.StatusInternalServerError, err) + if idMismatch, err := db.SetState(r.URL.Path, data, id); err != nil { + if idMismatch { + _ = errorResponse(w, http.StatusConflict, err) + } else { + _ = errorResponse(w, http.StatusInternalServerError, err) + } } else { w.WriteHeader(http.StatusOK) } diff --git a/cmd/tfstated/routes.go b/cmd/tfstated/routes.go index da46078..e2700d2 100644 --- a/cmd/tfstated/routes.go +++ b/cmd/tfstated/routes.go @@ -14,5 +14,7 @@ func addRoutes( mux.Handle("DELETE /", handleDelete(db)) mux.Handle("GET /", handleGet(db)) + mux.Handle("LOCK /", handleLock(db)) mux.Handle("POST /", handlePost(db)) + mux.Handle("UNLOCK /", handleUnlock(db)) } diff --git a/cmd/tfstated/unlock.go b/cmd/tfstated/unlock.go new file mode 100644 index 0000000..af5ad57 --- /dev/null +++ b/cmd/tfstated/unlock.go @@ -0,0 +1,38 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + + "git.adyxax.org/adyxax/tfstated/pkg/database" +) + +func handleUnlock(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + _ = encode(w, http.StatusBadRequest, + fmt.Errorf("no state path provided, cannot LOCK /")) + return + } + + var lock lockRequest + if err := decode(r, &lock); err != nil { + _ = encode(w, http.StatusBadRequest, err) + return + } + if success, err := db.Unlock(r.URL.Path, &lock); err != nil { + if errors.Is(err, sql.ErrNoRows) { + _ = encode(w, http.StatusNotFound, + fmt.Errorf("state path not found: %s", r.URL.Path)) + } else { + _ = errorResponse(w, http.StatusInternalServerError, err) + } + } else if success { + w.WriteHeader(http.StatusOK) + } else { + _ = encode(w, http.StatusConflict, lock) + } + }) +} -- cgit v1.2.3