diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-11-09 10:50:59 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-09 10:50:59 -0500 |
| commit | 20d07c7005665f8e2001b4cbd24e15ec589d9882 (patch) | |
| tree | 8b98d1446c6d9e42bc2621e633543e0420f43541 /internal | |
| parent | 2b349f56cb20420dd153f40f22b3654b5079100f (diff) | |
| download | xesite-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.go | 3 | ||||
| -rw-r--r-- | internal/domain_redirect.go | 11 | ||||
| -rw-r--r-- | internal/k8s/k8s.go | 133 | ||||
| -rw-r--r-- | internal/lume/lume.go | 10 | ||||
| -rw-r--r-- | internal/onion_location.go | 36 |
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) + }) +} |
