feat(tfstated): implement GET and POST methods, states are encrypted in a sqlite3 database

This commit is contained in:
Julien Dessaux 2024-09-30 00:58:49 +02:00
parent baf5aac08e
commit 4ff490806c
Signed by: adyxax
GPG key ID: F92E51B86E07177E
18 changed files with 627 additions and 2 deletions

35
cmd/tfstated/get.go Normal file
View file

@ -0,0 +1,35 @@
package main
import (
"database/sql"
"errors"
"fmt"
"net/http"
"git.adyxax.org/adyxax/tfstated/pkg/database"
)
func handleGet(db *database.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store, no-cache")
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/" {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("{\"msg\": \"No state path provided, cannot GET /\"}"))
return
}
if data, err := db.GetState(r.URL.Path); err != nil {
if errors.Is(err, sql.ErrNoRows) {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
_, _ = w.Write([]byte(fmt.Sprintf("{\"msg\": \"%+v\"}", err)))
} else {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}
})
}

View file

@ -12,6 +12,8 @@ import (
"os/signal"
"sync"
"time"
"git.adyxax.org/adyxax/tfstated/pkg/database"
)
type Config struct {
@ -22,6 +24,7 @@ type Config struct {
func run(
ctx context.Context,
config *Config,
db *database.DB,
args []string,
getenv func(string) string,
stdin io.Reader,
@ -30,10 +33,20 @@ func run(
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
dataEncryptionKey := getenv("DATA_ENCRYPTION_KEY")
if dataEncryptionKey == "" {
return fmt.Errorf("the DATA_ENCRYPTION_KEY environment variable is not set")
}
if err := db.SetDataEncryptionKey(dataEncryptionKey); err != nil {
return err
}
mux := http.NewServeMux()
addRoutes(
mux,
db,
)
httpServer := &http.Server{
Addr: net.JoinHostPort(config.Host, config.Port),
Handler: mux,
@ -79,9 +92,17 @@ func main() {
Port: "8080",
}
db, err := database.NewDB(ctx, "./tfstate.db?_txlock=immediate")
if err != nil {
fmt.Fprintf(os.Stderr, "database init error: %+v\n", err)
os.Exit(1)
}
defer db.Close()
if err := run(
ctx,
&config,
db,
os.Args,
os.Getenv,
os.Stdin,

32
cmd/tfstated/post.go Normal file
View file

@ -0,0 +1,32 @@
package main
import (
"fmt"
"io"
"net/http"
"git.adyxax.org/adyxax/tfstated/pkg/database"
)
func handlePost(db *database.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/" {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("{\"msg\": \"No state path provided, cannot POST /\"}"))
return
}
data, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(fmt.Sprintf("{\"msg\": \"failed to read request body: %+v\"}", err)))
return
}
if err := db.SetState(r.URL.Path, data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(fmt.Sprintf("{\"msg\": \"%+v\"}", err)))
} else {
w.WriteHeader(http.StatusOK)
}
})
}

View file

@ -1,7 +1,16 @@
package main
import "net/http"
import (
"net/http"
func addRoutes(mux *http.ServeMux) {
"git.adyxax.org/adyxax/tfstated/pkg/database"
)
func addRoutes(
mux *http.ServeMux,
db *database.DB,
) {
mux.Handle("GET /healthz", handleHealthz())
mux.Handle("GET /", handleGet(db))
mux.Handle("POST /", handlePost(db))
}