Add fancy-dns-console migrate to convert an existing deployment into cluster objects
The settings of the ConfigMap-and-YAML deployment have to become objects before the mechanisms that held them can go. The command reads what the old console wrote (the zone--<apex>.zone keys of the zones ConfigMap and the address-overrides RPZ), and optionally the resolver's config.yaml, and converts: zone master files -> DNSZone address-overrides RPZ -> AddressOverride, one per name URL blocklists -> BlockList allow_entries/block_entries -> DomainRule inline-allow / inline-block acl.groups -> ClientGroup By default it only prints manifests, which can be reviewed or committed; -apply creates or updates the objects. It never deletes or edits the ConfigMaps or the config. Each conversion is validated with the code the resolver uses to read the objects, and anything it cannot represent faithfully is listed and left alone: a zone with a record type the API does not write, a list that is a file on the pod, a name that is not a legal object name, an RPZ line that is not an A or AAAA override. Tests apply a conversion of the live data to a fake cluster and read it back through the resolver's source, and run the command against fake ConfigMaps, offline from files, and with the ConfigMaps missing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bdcdbaf23a
commit
2f25463347
9 files changed
+919
No files matched your search
@@ -0,0 +1,188 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubeedit"
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource"
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/migrate"
|
||||
)
|
||||
|
||||
var configMaps = schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
|
||||
|
||||
// newClient builds the cluster client; a test replaces it.
|
||||
var newClient = func(o kubesource.ClientOptions) (dynamic.Interface, error) {
|
||||
c, _, err := kubesource.NewClient(o)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// migrateMain converts an existing ConfigMap-and-YAML deployment into cluster
|
||||
// objects. By default it only prints manifests, so the result can be reviewed
|
||||
// or committed; -apply writes them. It changes nothing else.
|
||||
func migrateMain(args []string, stdout, stderr io.Writer) error {
|
||||
fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
var (
|
||||
ns = fs.String("kube-namespace", os.Getenv("FANCYDNS_KUBE_NAMESPACE"), "namespace of the ConfigMaps and of the new objects (default: the pod's own)")
|
||||
kubeconfig = fs.String("kubeconfig", os.Getenv("FANCYDNS_KUBECONFIG"), "kubeconfig (default: in-cluster, then $KUBECONFIG, then ~/.kube/config)")
|
||||
kubeContext = fs.String("kube-context", os.Getenv("FANCYDNS_KUBE_CONTEXT"), "kubeconfig context")
|
||||
zonesCM = fs.String("zones-configmap", "fancy-dns-zones", "ConfigMap holding the zone--<apex>.zone files (\"\" to skip)")
|
||||
rpzCM = fs.String("rpz-configmap", "fancy-dns-rpz", "ConfigMap holding the address-overrides RPZ (\"\" to skip)")
|
||||
rpzKey = fs.String("rpz-key", "address-overrides.rpz", "key of the RPZ in that ConfigMap")
|
||||
zonesDir = fs.String("zones-dir", "", "read zone files from this directory (<apex>.zone) instead of the ConfigMap")
|
||||
rpzFile = fs.String("rpz-file", "", "read the address-overrides RPZ from this file instead of the ConfigMap")
|
||||
cfgPath = fs.String("config", "", "the resolver's config.yaml, to convert its URL lists, inline entries and ACL groups")
|
||||
apply = fs.Bool("apply", false, "create the objects in the cluster (default: print the manifests)")
|
||||
)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in := migrate.Inputs{Namespace: *ns, ZoneFiles: map[string]string{}}
|
||||
if in.Namespace == "" {
|
||||
in.Namespace = kubesource.Namespace()
|
||||
}
|
||||
if in.Namespace == "" {
|
||||
return errors.New("-kube-namespace is required when not running in a pod")
|
||||
}
|
||||
|
||||
needCluster := *apply || (*zonesDir == "" && *zonesCM != "") || (*rpzFile == "" && *rpzCM != "")
|
||||
var client dynamic.Interface
|
||||
if needCluster {
|
||||
var err error
|
||||
client, err = newClient(kubesource.ClientOptions{Kubeconfig: *kubeconfig, Context: *kubeContext})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var notes []string
|
||||
if *zonesDir != "" {
|
||||
files, err := readZoneDir(*zonesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.ZoneFiles = files
|
||||
} else if *zonesCM != "" {
|
||||
data, found, err := configMapData(ctx, client, in.Namespace, *zonesCM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
notes = append(notes, fmt.Sprintf("ConfigMap %s/%s does not exist: no zone files to convert", in.Namespace, *zonesCM))
|
||||
}
|
||||
for k, v := range data {
|
||||
if strings.HasPrefix(k, "zone--") && strings.HasSuffix(k, ".zone") {
|
||||
in.ZoneFiles[strings.TrimSuffix(strings.TrimPrefix(k, "zone--"), ".zone")] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if *rpzFile != "" {
|
||||
b, err := os.ReadFile(*rpzFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.RPZ = string(b)
|
||||
} else if *rpzCM != "" {
|
||||
data, found, err := configMapData(ctx, client, in.Namespace, *rpzCM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
notes = append(notes, fmt.Sprintf("ConfigMap %s/%s does not exist: no address overrides to convert", in.Namespace, *rpzCM))
|
||||
}
|
||||
in.RPZ = data[*rpzKey]
|
||||
}
|
||||
if *cfgPath != "" {
|
||||
cfg, err := config.Load(*cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w\n(the config expands ${VAR} references; set any variable it names, its value is not used)", err)
|
||||
}
|
||||
in.Config = cfg
|
||||
}
|
||||
|
||||
res := migrate.Convert(in)
|
||||
for _, n := range notes {
|
||||
fmt.Fprintln(stderr, "note:", n)
|
||||
}
|
||||
for _, n := range res.Notes {
|
||||
fmt.Fprintln(stderr, "converted:", n)
|
||||
}
|
||||
for _, s := range res.Skipped {
|
||||
fmt.Fprintln(stderr, "NOT converted:", s)
|
||||
}
|
||||
if len(res.Objects) == 0 {
|
||||
fmt.Fprintln(stderr, "nothing to migrate")
|
||||
return nil
|
||||
}
|
||||
if !*apply {
|
||||
out, err := migrate.YAML(res.Objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stdout.Write(out)
|
||||
fmt.Fprintf(stderr, "\n%d objects printed. Review them, then run again with -apply, or `kubectl apply -f -`.\n", len(res.Objects))
|
||||
return err
|
||||
}
|
||||
ed := kubeedit.New(client, in.Namespace)
|
||||
for _, o := range res.Objects {
|
||||
if err := ed.Apply(ctx, o, "fancy-dns-migrate"); err != nil {
|
||||
return fmt.Errorf("apply %s/%s: %w", o.GetKind(), o.GetName(), err)
|
||||
}
|
||||
fmt.Fprintf(stderr, "applied %s/%s\n", o.GetKind(), o.GetName())
|
||||
}
|
||||
fmt.Fprintf(stderr, "\n%d objects applied in namespace %s. The old ConfigMaps were not touched.\n", len(res.Objects), in.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readZoneDir(dir string) (map[string]string, error) {
|
||||
paths, err := filepath.Glob(filepath.Join(dir, "*.zone"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, p := range paths {
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := strings.TrimSuffix(filepath.Base(p), ".zone")
|
||||
out[strings.TrimPrefix(name, "zone--")] = string(b)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// configMapData returns a ConfigMap's data, and whether the ConfigMap exists.
|
||||
func configMapData(ctx context.Context, c dynamic.Interface, ns, name string) (map[string]string, bool, error) {
|
||||
u, err := c.Resource(configMaps).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("read ConfigMap %s/%s: %w", ns, name, err)
|
||||
}
|
||||
raw, _ := u.Object["data"].(map[string]any)
|
||||
out := make(map[string]string, len(raw))
|
||||
for k, v := range raw {
|
||||
if s, ok := v.(string); ok {
|
||||
out[k] = s
|
||||
}
|
||||
}
|
||||
return out, true, nil
|
||||
}
|
||||
Reference in new issue
Block a user