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--.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 (.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 }