Unverified Commit e5f16828 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

refactor(deploy)!: TUI

parent 318a724f
Loading
Loading
Loading
Loading
+7 −13
Changes for CONTRIBUTING.md: 7 added lines, 13 removed lines.
Original line number Diff line number Diff line
@@ -175,11 +175,11 @@ Run integration/deploy commands from the repository root on the Linux host:
cd /path/to/Felis
```

The one-line bootstrap script is intended for a clean Linux host, not a typical
macOS development machine:
The setup TUI is intended for a clean Linux host, not a typical macOS
development machine:

```bash
sudo -E bash deploy/bootstrap.sh
sudo felis setup
```

Useful overrides:
@@ -191,16 +191,10 @@ export FELIS_IMAGE=felis:dev
export FELIS_ROOT_DOMAIN=<node-ip>.nip.io
```

The bootstrap flow installs/configures system services, builds the Felis image,
imports it into k3s, runs migrations, applies CRDs/manifests, and deploys the
control plane.

After bootstrap, use the break-glass console to create or recover the Owner
account:

```bash
sudo felis breakGlass
```
The setup flow wraps the host bootstrap, then continues to Owner account setup
and optional Cloudflare edge setup in the same command. The raw
`deploy/bootstrap.sh` script remains available for low-level host provisioning
when debugging the installer itself.

Use a VM or disposable Linux server for this. Treat it as an integration and
acceptance environment, while keeping normal coding and quick tests local.
+20 −11
Changes for Dockerfile: 20 added lines, 11 removed lines.
Original line number Diff line number Diff line
# Felis control-plane image.
#
# Builds the single multi-call `felis` binary (api / operator / migrate / reaper
# / restore / manifests / setup) as a static, CGO-free executable and ships it on
# a distroless base. Two contracts the rendered Deployments depend on:
# Builds the panel, then the single multi-call `felis` binary (api / operator /
# migrate / reaper / restore / manifests / setup) as a static, CGO-free
# executable and ships it on a distroless base. Two contracts the rendered
# Deployments depend on:
#
#   1. The binary lives on PATH at /usr/local/bin/felis, because the bundle
#      invokes it by bare name (`command: ["felis", ...]` in workloads.go). PATH
#      is pinned explicitly so this holds regardless of base-image defaults.
#   1. The binary lives at /usr/local/bin/felis, and rendered Kubernetes
#      workloads invoke that absolute path rather than relying on PATH lookup.
#   2. The image is meant to be imported into a local containerd (k3s ctr import)
#      and referenced by a NON-:latest tag (e.g. felis:demo). k8s then resolves
#      the default IfNotPresent pull policy against the imported image instead of
@@ -15,21 +15,30 @@
# deploy/bootstrap.sh also extracts this same binary onto the host (docker cp)
# so `felis migrate up` and the `felis setup` TUI run with the identical build.

FROM node:22-bookworm AS panel
WORKDIR /panel
COPY panel/package*.json ./
RUN npm ci
COPY panel/ ./
RUN npm run build

FROM golang:1.26 AS build
WORKDIR /src
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
ARG TARGETOS=linux
ARG TARGETARCH
# Prime the module cache first so source-only edits do not re-download deps.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -trimpath -ldflags="-s -w" -o /out/felis ./cmd/felis
COPY --from=panel /panel/dist ./internal/panel/static
RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="${TARGETARCH:-$(go env GOARCH)}" \
    go build -trimpath -ldflags="-s -w" -o /out/felis ./cmd/felis

FROM gcr.io/distroless/static-debian12:nonroot
# Guarantee bare `felis` resolves no matter what PATH the base image ships.
ENV PATH=/usr/local/bin:/usr/bin:/bin
COPY --from=build /out/felis /usr/local/bin/felis
COPY --chmod=0755 --from=build /out/felis /usr/local/bin/felis
# distroless "nonroot" is uid 65532; the rendered PodSecurityContext pins
# runAsUser 1000 at deploy time, and a static binary needs no /etc/passwd entry,
# so either uid runs the same binary from a read-only root filesystem.
USER 65532:65532
ENTRYPOINT ["felis"]
ENTRYPOINT ["/usr/local/bin/felis"]

bootstrap_asset.go

0 → 100644
+20 −0
Changes for bootstrap_asset.go: 20 added lines, 0 removed lines.
Original line number Diff line number Diff line
package felis

import "embed"

//go:embed deploy/bootstrap.sh
var bootstrapScript string

//go:embed deploy/crd/*.yaml
var bootstrapAssets embed.FS

// BootstrapScript returns the host bootstrap installer embedded in the felis binary.
func BootstrapScript() string {
	return bootstrapScript
}

// MinecraftServerCRD returns the embedded MinecraftServer CRD YAML used by the
// host bootstrap path that runs without a source checkout.
func MinecraftServerCRD() ([]byte, error) {
	return bootstrapAssets.ReadFile("deploy/crd/felis.lolicon.best_minecraftservers.yaml")
}
+23 −2
Changes for cmd/felis/api.go: 23 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import (
	"felis.lolicon.best/internal/apis/felis/v1alpha1"
	"felis.lolicon.best/internal/build"
	"felis.lolicon.best/internal/config"
	"felis.lolicon.best/internal/panel"
	"felis.lolicon.best/internal/restore"
	"felis.lolicon.best/internal/store"
	"felis.lolicon.best/internal/submit"
@@ -34,9 +35,16 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
	fs.SetOutput(stderr)
	cfgPath := fs.String("config", "/etc/felis/felis.toml", "path to felis.toml")
	internalAddr := fs.String("internal-addr", ":8081", "internal-face listen address (service token, no Zero Trust)")
	httpsAddr := fs.String("https-addr", "", "external HTTPS listen address (disabled unless --tls-cert and --tls-key are also set)")
	tlsCert := fs.String("tls-cert", "", "TLS certificate path for --https-addr")
	tlsKey := fs.String("tls-key", "", "TLS private key path for --https-addr")
	if err := fs.Parse(args); err != nil {
		return 2
	}
	if (*httpsAddr == "") != (*tlsCert == "" || *tlsKey == "") {
		fmt.Fprintln(stderr, "felis api: --https-addr requires both --tls-cert and --tls-key")
		return 2
	}

	cfg, err := config.Load(*cfgPath)
	if err != nil {
@@ -157,13 +165,23 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
	}
	fmt.Fprintln(stderr, "felis api: external face fails closed (Access JWKS key function not configured)")

	externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain)
	internalSrv := &http.Server{Addr: *internalAddr, Handler: a.InternalHandler()}
	externalSrv := &http.Server{Addr: cfg.Server.Listen, Handler: a.ExternalHandler()}
	externalSrv := &http.Server{Addr: cfg.Server.Listen, Handler: externalHandler}

	errc := make(chan error, 2)
	errc := make(chan error, 3)
	go func() { errc <- internalSrv.ListenAndServe() }()
	go func() { errc <- externalSrv.ListenAndServe() }()
	var httpsSrv *http.Server
	if *httpsAddr != "" {
		httpsSrv = &http.Server{Addr: *httpsAddr, Handler: externalHandler}
		go func() { errc <- httpsSrv.ListenAndServeTLS(*tlsCert, *tlsKey) }()
	}
	if httpsSrv != nil {
		fmt.Fprintf(stdout, "felis api: internal=%s external=%s https=%s\n", *internalAddr, cfg.Server.Listen, *httpsAddr)
	} else {
		fmt.Fprintf(stdout, "felis api: internal=%s external=%s\n", *internalAddr, cfg.Server.Listen)
	}

	// reconcileBuilds drives the scan-gate translation: poll unfinished builds
	// and advance any whose Job has reached a terminal phase. GET on a build also
@@ -176,6 +194,9 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
		defer cancel()
		_ = internalSrv.Shutdown(shutdownCtx)
		_ = externalSrv.Shutdown(shutdownCtx)
		if httpsSrv != nil {
			_ = httpsSrv.Shutdown(shutdownCtx)
		}
		return 0
	case err := <-errc:
		if err != nil && err != http.ErrServerClosed {
+25 −0
Changes for cmd/felis/bootstrap_assets.go: 25 added lines, 0 removed lines.
Original line number Diff line number Diff line
package main

import (
	"fmt"
	"io"

	felis "felis.lolicon.best"
)

func cmdBootstrapAssets(args []string, stdout, stderr io.Writer) int {
	if len(args) != 1 || args[0] != "crd" {
		fmt.Fprintln(stderr, "felis bootstrap-assets: usage: felis bootstrap-assets crd")
		return 2
	}
	crd, err := felis.MinecraftServerCRD()
	if err != nil {
		fmt.Fprintf(stderr, "felis bootstrap-assets: %v\n", err)
		return 1
	}
	if _, err := stdout.Write(crd); err != nil {
		fmt.Fprintf(stderr, "felis bootstrap-assets: write: %v\n", err)
		return 1
	}
	return 0
}
Loading