forked from tangled.org/core
this repo has no description

knotserver: add query lexicon for owner

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 6adfb3a9 4cdc2d84

verified
Changed files
+225 -151
appview
knotserver
spindle
+31
appview/db/db.go
···
return err
})
+
// repurpose the read-only column to "needs-upgrade"
+
runMigration(conn, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
alter table registrations rename column read_only to needs_upgrade;
+
`)
+
return err
+
})
+
+
// require all knots to upgrade after the release of total xrpc
+
runMigration(conn, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
update registrations set needs_upgrade = 1;
+
`)
+
return err
+
})
+
+
// require all knots to upgrade after the release of total xrpc
+
runMigration(conn, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
alter table spindles add column needs_upgrade integer not null default 0;
+
`)
+
if err != nil {
+
return err
+
}
+
+
_, err = tx.Exec(`
+
update spindles set needs_upgrade = 1;
+
`)
+
return err
+
})
+
return &DB{db}, nil
}
+17 -17
appview/db/registration.go
···
// Registration represents a knot registration. Knot would've been a better
// name but we're stuck with this for historical reasons.
type Registration struct {
-
Id int64
-
Domain string
-
ByDid string
-
Created *time.Time
-
Registered *time.Time
-
ReadOnly bool
+
Id int64
+
Domain string
+
ByDid string
+
Created *time.Time
+
Registered *time.Time
+
NeedsUpgrade bool
}
func (r *Registration) Status() Status {
-
if r.ReadOnly {
-
return ReadOnly
+
if r.NeedsUpgrade {
+
return NeedsUpgrade
} else if r.Registered != nil {
return Registered
} else {
···
return r.Status() == Registered
}
-
func (r *Registration) IsReadOnly() bool {
-
return r.Status() == ReadOnly
+
func (r *Registration) IsNeedsUpgrade() bool {
+
return r.Status() == NeedsUpgrade
}
func (r *Registration) IsPending() bool {
···
const (
Registered Status = iota
Pending
-
ReadOnly
+
NeedsUpgrade
)
func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) {
···
}
query := fmt.Sprintf(`
-
select id, domain, did, created, registered, read_only
+
select id, domain, did, created, registered, needs_upgrade
from registrations
%s
order by created
···
for rows.Next() {
var createdAt string
var registeredAt sql.Null[string]
-
var readOnly int
+
var needsUpgrade int
var reg Registration
-
err = rows.Scan(&reg.Id, &reg.Domain, &reg.ByDid, &createdAt, &registeredAt, &readOnly)
+
err = rows.Scan(&reg.Id, &reg.Domain, &reg.ByDid, &createdAt, &registeredAt, &needsUpgrade)
if err != nil {
return nil, err
}
···
}
}
-
if readOnly != 0 {
-
reg.ReadOnly = true
+
if needsUpgrade != 0 {
+
reg.NeedsUpgrade = true
}
registrations = append(registrations, reg)
···
args = append(args, filter.Arg()...)
}
-
query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0"
+
query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), needs_upgrade = 0"
if len(conditions) > 0 {
query += " where " + strings.Join(conditions, " and ")
}
+14 -7
appview/db/spindle.go
···
)
type Spindle struct {
-
Id int
-
Owner syntax.DID
-
Instance string
-
Verified *time.Time
-
Created time.Time
+
Id int
+
Owner syntax.DID
+
Instance string
+
Verified *time.Time
+
Created time.Time
+
NeedsUpgrade bool
}
type SpindleMember struct {
···
}
query := fmt.Sprintf(
-
`select id, owner, instance, verified, created
+
`select id, owner, instance, verified, created, needs_upgrade
from spindles
%s
order by created
···
var spindle Spindle
var createdAt string
var verified sql.NullString
+
var needsUpgrade int
if err := rows.Scan(
&spindle.Id,
···
&spindle.Instance,
&verified,
&createdAt,
+
&needsUpgrade,
); err != nil {
return nil, err
}
···
spindle.Verified = &t
}
+
if needsUpgrade != 0 {
+
spindle.NeedsUpgrade = true
+
}
+
spindles = append(spindles, spindle)
}
···
whereClause = " where " + strings.Join(conditions, " and ")
}
-
query := fmt.Sprintf(`update spindles set verified = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
+
query := fmt.Sprintf(`update spindles set verified = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now'), needs_upgrade = 0 %s`, whereClause)
res, err := e.Exec(query, args...)
if err != nil {
+5 -57
appview/knots/knots.go
···
import (
"errors"
"fmt"
-
"log"
"log/slog"
"net/http"
"slices"
···
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/serververify"
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/eventconsumer"
"tangled.sh/tangled.sh/core/idresolver"
"tangled.sh/tangled.sh/core/rbac"
···
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry)
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
-
-
r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner)
return r
}
···
if err != nil {
l.Error("verification failed", "err", err)
-
if errors.Is(err, serververify.FetchError) {
-
k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
+
if errors.Is(err, xrpcclient.ErrXrpcUnsupported) {
+
k.Pages.Notice(w, noticeId, "Failed to verify knot, XRPC queries are unsupported on this knot, consider upgrading!")
return
}
···
return
}
-
// if this knot was previously read-only, then emit a record too
+
// if this knot requires upgrade, then emit a record too
//
// this is part of migrating from the old knot system to the new one
-
if registration.ReadOnly {
+
if registration.NeedsUpgrade {
// re-announce by registering under same rkey
client, err := k.OAuth.AuthorizedClient(r)
if err != nil {
···
return
}
updatedRegistration := registrations[0]
-
-
log.Println(updatedRegistration)
w.Header().Set("HX-Reswap", "outerHTML")
k.Pages.KnotListing(w, pages.KnotListingParams{
···
// ok
k.Pages.HxRefresh(w)
}
-
-
func (k *Knots) banner(w http.ResponseWriter, r *http.Request) {
-
user := k.OAuth.GetUser(r)
-
l := k.Logger.With("handler", "banner")
-
l = l.With("did", user.Did)
-
l = l.With("handle", user.Handle)
-
-
allRegistrations, err := db.GetRegistrations(
-
k.Db,
-
db.FilterEq("did", user.Did),
-
)
-
if err != nil {
-
l.Error("non-fatal: failed to get registrations")
-
return
-
}
-
-
httpClient := &http.Client{Timeout: 5 * time.Second}
-
regs404 := []db.Registration{}
-
for _, reg := range allRegistrations {
-
healthURL := fmt.Sprintf("http://%s/xrpc/_health", reg.Domain)
-
-
fmt.Println(healthURL)
-
-
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, healthURL, nil)
-
if err != nil {
-
l.Error("failed to create health check request", "domain", reg.Domain, "err", err)
-
continue
-
}
-
-
resp, err := httpClient.Do(req)
-
if err != nil {
-
l.Error("failed to make health check request", "domain", reg.Domain, "err", err)
-
continue
-
}
-
defer resp.Body.Close()
-
-
if resp.StatusCode == http.StatusNotFound {
-
regs404 = append(regs404, reg)
-
}
-
}
-
if len(regs404) == 0 {
-
return
-
}
-
-
k.Pages.KnotBanner(w, pages.KnotBannerParams{
-
Registrations: regs404,
-
})
-
}
+4 -3
appview/pages/pages.go
···
return p.execute("user/settings/emails", w, params)
}
-
type KnotBannerParams struct {
+
type UpgradeBannerParams struct {
Registrations []db.Registration
+
Spindles []db.Spindle
}
-
func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error {
-
return p.executePlain("knots/fragments/bannerRequiresUpgrade", w, params)
+
func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error {
+
return p.executePlain("banner", w, params)
}
type KnotsParams struct {
+23
appview/pages/templates/banner.html
···
+
{{ define "banner" }}
+
<div class="px-6 py-2">
+
The following services that you administer will have to be upgraded to be compatible with the latest version of Tangled:
+
{{ if .Registrations }}
+
<ul class="list-disc mx-12 my-2">
+
{{range .Registrations}}
+
<li>{{ .Domain }}</li>
+
{{ end }}
+
</ul>
+
Repositories hosted on these knots may not be accessible until upgraded.
+
{{ end }}
+
+
{{ if .Spindles }}
+
<ul class="list-disc mx-12 my-2">
+
{{range .Spindles}}
+
<li>{{ .Instance }}</li>
+
{{ end }}
+
</ul>
+
Pipelines may not be executed on these spindles until upgraded.
+
{{ end }}
+
<a href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations/">Click to read the upgrade guide</a>.
+
</div>
+
{{ end }}
-8
appview/pages/templates/knots/fragments/bannerReadOnly.html
···
-
{{ define "knots/fragments/bannerReadOnly" }}
-
<div class="w-full px-6 py-2 -z-15 bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm">
-
A knot ({{range $i, $r := .Registrations}}{{if ne $i 0}}, {{end}}{{ $r.Domain }}{{ end }})
-
that you administer is presently read-only. Consider upgrading this knot to
-
continue creating repositories on it.
-
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/migrations/knot-1.7.0.md">Click to read the upgrade guide</a>.
-
</div>
-
{{ end }}
-12
appview/pages/templates/knots/fragments/bannerRequiresUpgrade.html
···
-
{{ define "knots/fragments/bannerRequiresUpgrade" }}
-
<div class="px-6 py-2">
-
The following knots that you administer will have to be upgraded to be compatible with the latest version of Tangled:
-
<ul class="list-disc mx-12 my-2">
-
{{range $i, $r := .Registrations}}
-
<li>{{ $r.Domain }}</li>
-
{{ end }}
-
</ul>
-
Repositories hosted on these knots will not be accessible until upgraded.
-
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/migrations/knot-1.8.0.md">Click to read the upgrade guide</a>.
-
</div>
-
{{ end }}
+2 -2
appview/pages/templates/knots/fragments/knotListing.html
···
</span>
{{ template "knots/fragments/addMemberModal" . }}
{{ block "knotDeleteButton" . }} {{ end }}
-
{{ else if .IsReadOnly }}
+
{{ else if .IsNeedsUpgrade }}
<span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}">
-
{{ i "shield-alert" "w-4 h-4" }} read-only
+
{{ i "shield-alert" "w-4 h-4" }} needs upgrade
</span>
{{ block "knotRetryButton" . }} {{ end }}
{{ block "knotDeleteButton" . }} {{ end }}
+1 -1
appview/pages/templates/layouts/base.html
···
{{ if .LoggedInUser }}
<div id="upgrade-banner"
class="z-50 fixed bottom-0 left-0 right-0 w-full flex justify-center bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm"
-
hx-get="/knots/upgradeBanner"
+
hx-get="/banner"
hx-trigger="load"
hx-swap="innerHTML">
</div>
+11 -27
appview/serververify/verify.go
···
"context"
"errors"
"fmt"
-
"io"
-
"net/http"
-
"strings"
-
"time"
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/rbac"
)
···
scheme = "http"
}
-
url := fmt.Sprintf("%s://%s/owner", scheme, domain)
-
req, err := http.NewRequest("GET", url, nil)
-
if err != nil {
-
return "", err
-
}
-
-
client := &http.Client{
-
Timeout: 1 * time.Second,
-
}
-
-
resp, err := client.Do(req.WithContext(ctx))
-
if err != nil || resp.StatusCode != 200 {
-
return "", fmt.Errorf("failed to fetch /owner")
+
host := fmt.Sprintf("%s://%s", scheme, domain)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
}
-
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data
-
if err != nil {
-
return "", fmt.Errorf("failed to read /owner response: %w", err)
+
res, err := tangled.Owner(ctx, xrpcc)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
return "", xrpcerr
}
-
did := strings.TrimSpace(string(body))
-
if did == "" {
-
return "", fmt.Errorf("empty DID in /owner response")
-
}
-
-
return did, nil
+
return res.Owner, nil
}
type OwnerMismatch struct {
···
func RunVerification(ctx context.Context, domain, expectedOwner string, dev bool) error {
observedOwner, err := fetchOwner(ctx, domain, dev)
if err != nil {
-
return fmt.Errorf("%w: %w", FetchError, err)
+
return err
}
if observedOwner != expectedOwner {
+4 -3
appview/spindles/spindles.go
···
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/serververify"
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/idresolver"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/tid"
···
if err != nil {
l.Error("verification failed", "err", err)
-
if errors.Is(err, serververify.FetchError) {
-
s.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
+
if errors.Is(err, xrpcclient.ErrXrpcUnsupported) {
+
s.Pages.Notice(w, noticeId, "Failed to verify spindle, XRPC queries are unsupported on this spindle, consider upgrading!")
return
}
···
}
w.Header().Set("HX-Reswap", "outerHTML")
-
s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]})
+
s.Pages.SpindleListing(w, pages.SpindleListingParams{Spindle: verifiedSpindle[0]})
}
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
+1
appview/state/router.go
···
r.Get("/", s.HomeOrTimeline)
r.Get("/timeline", s.Timeline)
+
r.With(middleware.AuthMiddleware(s.oauth)).Get("/upgradeBanner", s.UpgradeBanner)
r.Route("/repo", func(r chi.Router) {
r.Route("/new", func(r chi.Router) {
+37 -1
appview/state/state.go
···
})
}
+
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
+
user := s.oauth.GetUser(r)
+
l := s.logger.With("handler", "UpgradeBanner")
+
l = l.With("did", user.Did)
+
l = l.With("handle", user.Handle)
+
+
regs, err := db.GetRegistrations(
+
s.db,
+
db.FilterEq("did", user.Did),
+
db.FilterEq("needs_upgrade", 1),
+
)
+
if err != nil {
+
l.Error("non-fatal: failed to get registrations")
+
return
+
}
+
+
spindles, err := db.GetSpindles(
+
s.db,
+
db.FilterEq("did", user.Did),
+
db.FilterEq("needs_upgrade", 1),
+
)
+
if err != nil {
+
l.Error("non-fatal: failed to get spindles")
+
return
+
}
+
+
if regs == nil && spindles == nil {
+
return
+
}
+
+
s.pages.UpgradeBanner(w, pages.UpgradeBannerParams{
+
Registrations: regs,
+
Spindles: spindles,
+
})
+
}
+
func (s *State) Home(w http.ResponseWriter, r *http.Request) {
timeline, err := db.MakeTimeline(s.db, 5)
if err != nil {
···
for _, k := range pubKeys {
key := strings.TrimRight(k.Key, "\n")
-
w.Write([]byte(fmt.Sprintln(key)))
+
fmt.Fprintln(w, key)
}
}
-7
knotserver/router.go
···
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
})
-
owner := func(w http.ResponseWriter, r *http.Request) {
-
w.Write([]byte(h.c.Server.Owner))
-
}
-
// Deprecated: the sh.tangled.knot.owner xrpc call should be used instead
-
r.Get("/owner", owner)
-
r.Route("/{did}", func(r chi.Router) {
r.Route("/{name}", func(r chi.Router) {
// routes for git operations
···
// xrpc apis
r.Route("/xrpc", func(r chi.Router) {
r.Get("/_health", h.Version)
-
r.Get("/sh.tangled.knot.owner", owner)
r.Mount("/", h.XrpcRouter())
})
+31
knotserver/xrpc/owner.go
···
+
package xrpc
+
+
import (
+
"encoding/json"
+
"net/http"
+
+
"tangled.sh/tangled.sh/core/api/tangled"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
+
)
+
+
func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) {
+
owner := x.Config.Server.Owner
+
if owner == "" {
+
writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError)
+
return
+
}
+
+
response := tangled.Owner_Output{
+
Owner: owner,
+
}
+
+
w.Header().Set("Content-Type", "application/json")
+
if err := json.NewEncoder(w).Encode(response); err != nil {
+
x.Logger.Error("failed to encode response", "error", err)
+
writeError(w, xrpcerr.NewXrpcError(
+
xrpcerr.WithTag("InternalServerError"),
+
xrpcerr.WithMessage("failed to encode response"),
+
), http.StatusInternalServerError)
+
return
+
}
+
}
+3
knotserver/xrpc/xrpc.go
···
// knot query endpoints (no auth required)
r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys)
+
// service query endpoints (no auth required)
+
r.Get("/"+tangled.OwnerNSID, x.Owner)
+
return r
}
-3
spindle/server.go
···
w.Write(motd)
})
mux.HandleFunc("/events", s.Events)
-
mux.HandleFunc("/owner", func(w http.ResponseWriter, r *http.Request) {
-
w.Write([]byte(s.cfg.Server.Owner))
-
})
mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
mux.Mount("/xrpc", s.XrpcRouter())
+31
spindle/xrpc/owner.go
···
+
package xrpc
+
+
import (
+
"encoding/json"
+
"net/http"
+
+
"tangled.sh/tangled.sh/core/api/tangled"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
+
)
+
+
func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) {
+
owner := x.Config.Server.Owner
+
if owner == "" {
+
writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError)
+
return
+
}
+
+
response := tangled.Owner_Output{
+
Owner: owner,
+
}
+
+
w.Header().Set("Content-Type", "application/json")
+
if err := json.NewEncoder(w).Encode(response); err != nil {
+
x.Logger.Error("failed to encode response", "error", err)
+
writeError(w, xrpcerr.NewXrpcError(
+
xrpcerr.WithTag("InternalServerError"),
+
xrpcerr.WithMessage("failed to encode response"),
+
), http.StatusInternalServerError)
+
return
+
}
+
}
+10 -3
spindle/xrpc/xrpc.go
···
func (x *Xrpc) Router() http.Handler {
r := chi.NewRouter()
-
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
-
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
-
r.With(x.ServiceAuth.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
+
r.Group(func(r chi.Router) {
+
r.Use(x.ServiceAuth.VerifyServiceAuth)
+
+
r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
+
r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
+
r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
+
})
+
+
// service query endpoints (no auth required)
+
r.Get("/"+tangled.OwnerNSID, x.Owner)
return r
}