Files
Felis/cmd/felis/tui_storage_apply.go
T

144 lines
5.2 KiB
Go

package main
import (
"context"
"fmt"
"strings"
"felis.lolicon.best/internal/config"
"felis.lolicon.best/internal/platform"
"felis.lolicon.best/internal/submit"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
// applyStorageConfig persists the operator's storage choice and rolls felis-api so
// it picks up the new backend. For local it stamps user_uploads_context at the
// uploads PVC mount; for S3 it stamps the s3:// base + the [registry.s3] endpoint
// and credential refs, and creates the felis-uploads-s3 Secret the deployment reads
// the keys from. It mirrors applyReverseProxy — the same write-config →
// apply-secret → roll chain, hardcoding the default "felis" namespace as the rest
// of the wizard does.
func applyStorageConfig(ctx context.Context, method storageMethod, in s3Inputs) error {
var uploadsCtx string
var s3cfg config.RegistryS3Config
if method == storageS3 {
// Preflight the coordinates BEFORE touching config, the Secret, or the
// deployment: a mistyped key, wrong endpoint, or missing bucket fails here at
// the keyboard instead of silently at the first real upload. Nothing has been
// written yet, so a failed check leaves the install untouched.
if err := submit.CheckS3Access(ctx, submit.S3StoreConfig{
Base: "s3://" + in.bucket,
Endpoint: in.endpoint,
Region: in.region,
AccessKey: in.accessKey,
SecretKey: in.secretKey,
}); err != nil {
return err
}
uploadsCtx = "s3://" + in.bucket
s3cfg = config.RegistryS3Config{
Endpoint: in.endpoint,
Region: in.region,
AccessKeyRef: platform.UploadsS3AccessKeyEnv,
SecretKeyRef: platform.UploadsS3SecretKeyEnv,
}
} else {
uploadsCtx = platform.UploadsLocalPath
}
if err := writeStorageConfig(uploadsCtx, s3cfg); err != nil {
return err
}
// S3: land the credentials in their own Secret BEFORE the roll, so the optional
// env refs resolve on the fresh pod, and on the host before that, so the next
// installer run can apply the Secret again (hostcreds.go). Local needs no Secret.
if method == storageS3 {
for _, c := range []struct{ path, value string }{
{hostUploadsS3AccessKeyPath, in.accessKey},
{hostUploadsS3SecretKeyPath, in.secretKey},
} {
if err := writeHostCredential(c.path, c.value); err != nil {
return fmt.Errorf("keep the bucket credentials in %s: %w", c.path, err)
}
}
if err := applyUploadsS3Secret(ctx, in.accessKey, in.secretKey); err != nil {
return err
}
}
if err := applyFelisConfigSecret(ctx); err != nil {
return err
}
if err := kubectl(ctx, "-n", "felis", "rollout", "restart", "deployment/felis-api"); err != nil {
return err
}
return kubectl(ctx, "-n", "felis", "rollout", "status", "deployment/felis-api", "--timeout=180s")
}
// currentStorageInputs reads the storage backend already recorded in felis.toml so
// the reconfigure flow can pre-select the method and pre-fill the non-secret S3
// fields (endpoint/bucket/region). The credentials (the felis-uploads-s3 Secret
// and its host copies) are deliberately never read back — they must be re-entered
// to change.
// Any read error falls back to a blank local default rather than blocking reconfig.
func currentStorageInputs() (storageMethod, s3Inputs) {
cfg, err := config.Load(hostSetupConfigPath)
if err != nil {
return storageLocal, s3Inputs{}
}
base := cfg.Registry.UserUploadsContext
if !strings.HasPrefix(strings.ToLower(base), "s3://") {
return storageLocal, s3Inputs{}
}
bucket := base[len("s3://"):]
if i := strings.IndexByte(bucket, '/'); i >= 0 {
bucket = bucket[:i]
}
return storageS3, s3Inputs{
endpoint: cfg.Registry.S3.Endpoint,
bucket: bucket,
region: cfg.Registry.S3.Region,
}
}
// writeStorageConfig stamps the uploads backend into both the host and pod config
// files. The S3 subtable is set for S3 and cleared (zero value) for local, so
// switching backends never leaves stale coordinates behind.
func writeStorageConfig(uploadsCtx string, s3cfg config.RegistryS3Config) error {
for _, path := range []string{hostSetupConfigPath, podSetupConfigPath} {
cfg, err := config.Load(path)
if err != nil {
return err
}
cfg.Registry.UserUploadsContext = uploadsCtx
cfg.Registry.S3 = s3cfg
if err := writeConfig(path, cfg); err != nil {
return err
}
}
return nil
}
// applyUploadsS3Secret creates (or replaces) the felis-uploads-s3 Secret the
// felis-api Deployment mounts the S3 credentials from. The Secret is rendered
// in-process and piped to `kubectl apply` — the keys are NEVER passed as
// command-line args, so they never appear in the host process table.
func applyUploadsS3Secret(ctx context.Context, accessKey, secretKey string) error {
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
ObjectMeta: metav1.ObjectMeta{Name: platform.UploadsS3SecretName, Namespace: "felis"},
Type: corev1.SecretTypeOpaque,
StringData: map[string]string{
platform.UploadsS3SecretAccessKey: accessKey,
platform.UploadsS3SecretSecretKey: secretKey,
},
}
manifest, err := yaml.Marshal(secret)
if err != nil {
return fmt.Errorf("render uploads s3 secret: %w", err)
}
return kubectlWithInput(ctx, manifest, "apply", "-f", "-")
}