aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2024-11-09 10:50:59 -0500
committerGitHub <noreply@github.com>2024-11-09 10:50:59 -0500
commit20d07c7005665f8e2001b4cbd24e15ec589d9882 (patch)
tree8b98d1446c6d9e42bc2621e633543e0420f43541 /internal
parent2b349f56cb20420dd153f40f22b3654b5079100f (diff)
downloadxesite-20d07c7005665f8e2001b4cbd24e15ec589d9882.tar.xz
xesite-20d07c7005665f8e2001b4cbd24e15ec589d9882.zip
Move to Kubernetes (#853)
* start to lift-and-shift to k8s Signed-off-by: Xe Iaso <me@xeiaso.net> * manifest/xesite: properly configure pod disruption budget, hostmount for xesite as a hack Signed-off-by: Xe Iaso <me@xeiaso.net> * properly slonk readiness Signed-off-by: Xe Iaso <me@xeiaso.net> * manifest: move to aeacus Signed-off-by: Xe Iaso <me@xeiaso.net> * internal: add OnionLocation middleware Signed-off-by: Xe Iaso <me@xeiaso.net> * internal/lume: jettison serving from the zipfile Signed-off-by: Xe Iaso <me@xeiaso.net> * yolo deploy to prod Signed-off-by: Xe Iaso <me@xeiaso.net> * okay use a machineproxy here Signed-off-by: Xe Iaso <me@xeiaso.net> * test CI/CD Signed-off-by: Xe Iaso <me@xeiaso.net> * try civo route Signed-off-by: Xe Iaso <me@xeiaso.net> * lol Signed-off-by: Xe Iaso <me@xeiaso.net> * plan c? Signed-off-by: Xe Iaso <me@xeiaso.net> * specify the region Signed-off-by: Xe Iaso <me@xeiaso.net> * lol Signed-off-by: Xe Iaso <me@xeiaso.net> * blog: hello again kubernetes! Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'internal')
-rw-r--r--internal/cached_token_source.go3
-rw-r--r--internal/domain_redirect.go11
-rw-r--r--internal/k8s/k8s.go133
-rw-r--r--internal/lume/lume.go10
-rw-r--r--internal/onion_location.go36
5 files changed, 183 insertions, 10 deletions
diff --git a/internal/cached_token_source.go b/internal/cached_token_source.go
index f061e15..aebd869 100644
--- a/internal/cached_token_source.go
+++ b/internal/cached_token_source.go
@@ -3,6 +3,7 @@ package internal
import (
"context"
"encoding/json"
+ "errors"
"expvar"
"fmt"
"os"
@@ -13,6 +14,8 @@ import (
var (
tokenRefreshCount = expvar.NewInt("gauge_xesite_token_refresh_count")
+
+ ErrSecretValueDoesntExist = errors.New("internal: can't find oauth2-token in secret")
)
type cachingTokenSource struct {
diff --git a/internal/domain_redirect.go b/internal/domain_redirect.go
index a62f122..fe8bc1c 100644
--- a/internal/domain_redirect.go
+++ b/internal/domain_redirect.go
@@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"net/http"
+ "strings"
)
var (
@@ -28,8 +29,14 @@ func DomainRedirect(next http.Handler, development bool) http.Handler {
}
if r.Host != *redirectDomain {
- http.Redirect(w, r, fmt.Sprintf("https://%s%s", *redirectDomain, r.RequestURI), http.StatusMovedPermanently)
- return
+ if !strings.HasSuffix(r.Host, ".onion") {
+ if r.Method != "GET" {
+ http.Error(w, fmt.Sprintf("go to https://%s%s and try your request again", *redirectDomain, r.RequestURI), http.StatusMisdirectedRequest)
+ return
+ }
+ http.Redirect(w, r, fmt.Sprintf("https://%s%s", *redirectDomain, r.RequestURI), http.StatusMovedPermanently)
+ return
+ }
}
next.ServeHTTP(w, r)
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
new file mode 100644
index 0000000..433cffb
--- /dev/null
+++ b/internal/k8s/k8s.go
@@ -0,0 +1,133 @@
+package k8s
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "expvar"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "golang.org/x/oauth2"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+const secretValue = "oauth2-token"
+
+var (
+ tokenRefreshCount = expvar.NewInt("gauge_xesite_k8s_token_refresh_count")
+
+ ErrSecretValueDoesntExist = errors.New("internal: can't find oauth2-token in secret")
+)
+
+type tokenSource struct {
+ secretName string
+ namespace string
+ base oauth2.TokenSource
+ clientSet kubernetes.Interface
+}
+
+func (kts *tokenSource) Token() (tok *oauth2.Token, err error) {
+ tok, _ = loadTokenFromK8s(context.Background(), kts.clientSet, kts.namespace, kts.secretName)
+
+ if tok != nil && tok.Expiry.After(time.Now()) {
+ return tok, nil
+ }
+
+ if tok, err = kts.base.Token(); err != nil {
+ return nil, err
+ }
+
+ tokenRefreshCount.Add(1)
+
+ if err := kts.saveToken(tok); err != nil {
+ return nil, err
+ }
+
+ return tok, err
+}
+
+func (kts *tokenSource) saveToken(tok *oauth2.Token) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ sec, err := kts.clientSet.CoreV1().Secrets(kts.namespace).Get(ctx, kts.secretName, v1.GetOptions{})
+ if err != nil {
+ return fmt.Errorf("can't get secret %s::%s: %w", kts.namespace, kts.secretName, err)
+ }
+
+ data, err := json.Marshal(tok)
+ if err != nil {
+ return fmt.Errorf("can't marshal json: %w", err)
+ }
+
+ sec.Data[secretValue] = data
+
+ if _, err := kts.clientSet.CoreV1().Secrets(kts.namespace).Update(ctx, sec, v1.UpdateOptions{}); err != nil {
+ return fmt.Errorf("can't update secret %s::%s: %w", kts.namespace, kts.secretName, err)
+ }
+
+ return nil
+}
+
+func loadTokenFromK8s(ctx context.Context, cs kubernetes.Interface, ns, secretName string) (*oauth2.Token, error) {
+ sec, err := cs.CoreV1().Secrets(ns).Get(ctx, secretName, v1.GetOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("can't get secret %s::%s: %w", ns, secretName, err)
+ }
+
+ data, ok := sec.Data[secretValue]
+ if !ok {
+ return nil, ErrSecretValueDoesntExist
+ }
+
+ var tok oauth2.Token
+ if err := json.Unmarshal(data, &tok); err != nil {
+ return nil, fmt.Errorf("can't unmarshal oauth2-token in %s::%s: %w", ns, secretName, err)
+ }
+
+ return &tok, nil
+}
+
+func TokenSource(namespace string, secretName string, config *oauth2.Config) (oauth2.TokenSource, error) {
+ cs, err := getClientSet()
+ if err != nil {
+ return nil, fmt.Errorf("can't get client set: %w", err)
+ }
+
+ tok, err := loadTokenFromK8s(context.Background(), cs, namespace, secretName)
+ if err != nil {
+ return nil, err
+ }
+
+ orig := config.TokenSource(context.Background(), tok)
+
+ return oauth2.ReuseTokenSource(nil, &tokenSource{
+ secretName: secretName,
+ namespace: namespace,
+ base: orig,
+ clientSet: cs,
+ }), nil
+}
+
+func getClientSet() (kubernetes.Interface, error) {
+ config, err := rest.InClusterConfig()
+ if err != nil {
+ config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(os.Getenv("HOME"), ".kube", "config"))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ clientSet, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ return clientSet, nil
+}
diff --git a/internal/lume/lume.go b/internal/lume/lume.go
index 39516d9..24f3451 100644
--- a/internal/lume/lume.go
+++ b/internal/lume/lume.go
@@ -1,7 +1,6 @@
package lume
import (
- "archive/zip"
"bytes"
"context"
"encoding/json"
@@ -359,7 +358,7 @@ func (f *FS) build(ctx context.Context, siteCommit string) error {
zipLoc := filepath.Join(f.opt.DataDir, "site.zip")
- if err := ZipFolder(filepath.Join(cmd.Dir, "_site"), zipLoc); err != nil {
+ if err := ZipFolder(destDir, zipLoc); err != nil {
return fmt.Errorf("lume: can't compress site folder: %w", err)
}
@@ -369,12 +368,7 @@ func (f *FS) build(ctx context.Context, siteCommit string) error {
}
}
- fs, err := zip.OpenReader(zipLoc)
- if err != nil {
- return fmt.Errorf("lume: can't open zip with site content: %w", err)
- }
-
- f.fs = fs
+ f.fs = os.DirFS(destDir)
return nil
}
diff --git a/internal/onion_location.go b/internal/onion_location.go
new file mode 100644
index 0000000..120861d
--- /dev/null
+++ b/internal/onion_location.go
@@ -0,0 +1,36 @@
+package internal
+
+import (
+ "flag"
+ "log/slog"
+ "net/http"
+ "net/url"
+)
+
+var (
+ onionDomain = flag.String("onion-domain", "", "The Tor hidden service domain that this website uses")
+)
+
+func OnionLocation(next http.Handler) http.Handler {
+ if *onionDomain == "" {
+ slog.Debug("no onion domain defined, ignoring OnionLocation middleware")
+ return next
+ }
+
+ slog.Debug("OnionLocation middleware enabled", "onion-domain", *onionDomain)
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ u, err := url.Parse(r.URL.String())
+ if err != nil {
+ slog.Error("[unexpected] can't parse url", "err", err, "url", r.URL.String())
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ u.Scheme = "http"
+ u.Host = *onionDomain
+ w.Header().Add("Onion-Location", u.String())
+
+ next.ServeHTTP(w, r)
+ })
+}