From 86df61986ef05a705fe076759a476c3e8628456c Mon Sep 17 00:00:00 2001
From: Thomas Maurice <thomas@maurice.fr>
Date: Mon, 12 Feb 2024 17:32:18 +0100
Subject: [PATCH] feat(provider): makes the provider API non stupid

---
 pkg/cmd/dkimkey.go        | 187 ++++++++++++-------------------------
 pkg/cmd/root.go           |  11 ---
 pkg/cmd/version.go        |  17 ++++
 pkg/providers/ovh.go      | 191 ++++++++++++++++++++++++++++++++++++++
 pkg/providers/ovh/ovh.go  |  35 -------
 pkg/providers/provider.go |  16 ++++
 pkg/version/version.go    |   7 ++
 7 files changed, 288 insertions(+), 176 deletions(-)
 create mode 100644 pkg/cmd/version.go
 create mode 100644 pkg/providers/ovh.go
 delete mode 100644 pkg/providers/ovh/ovh.go
 create mode 100644 pkg/version/version.go

diff --git a/pkg/cmd/dkimkey.go b/pkg/cmd/dkimkey.go
index 949a7e1..1be46b7 100644
--- a/pkg/cmd/dkimkey.go
+++ b/pkg/cmd/dkimkey.go
@@ -8,8 +8,7 @@ import (
 	"git.maurice.fr/thomas/mailout/pkg/crypto"
 	"git.maurice.fr/thomas/mailout/pkg/database"
 	"git.maurice.fr/thomas/mailout/pkg/models"
-	"git.maurice.fr/thomas/mailout/pkg/providers/ovh"
-	"git.maurice.fr/thomas/mailout/pkg/utils"
+	provider "git.maurice.fr/thomas/mailout/pkg/providers"
 	"github.com/google/uuid"
 	"github.com/pterm/pterm"
 	"github.com/sirupsen/logrus"
@@ -20,11 +19,22 @@ var (
 	flagDKIMKeyActive bool
 	flagSelector      string
 	flagKeyBits       int
+	flagProvider      string
 )
 
 var DKIMKeyCmd = &cobra.Command{
 	Use:   "dkimkey",
 	Short: "manages DKIM keys",
+	PreRunE: func(cmd *cobra.Command, args []string) error {
+		if flagProvider == "" {
+			if cfg.DefaultProvider == "" {
+				logrus.Fatal("no provider specified and no default provider in config, aborting")
+			}
+			flagProvider = cfg.DefaultProvider
+		}
+
+		return nil
+	},
 }
 
 var DKIMKeyAddCmd = &cobra.Command{
@@ -228,93 +238,30 @@ var DKIMKeyPublishCmd = &cobra.Command{
 			"selector": dkimkey.Selector,
 		})
 
-		ovhClient, err := ovh.NewOVHProvider(cfg)
+		pv := flagProvider
+		if flagProvider == "" {
+			pv = cfg.DefaultProvider
+			if pv == "" {
+				logrus.Fatal("no provider specified")
+			}
+		}
+
+		pGen, ok := provider.Providers[pv]
+		if !ok {
+			logger.Fatalf("no such provider: %s", flagProvider)
+		}
+
+		p, err := pGen(cfg)
 		if err != nil {
-			logger.WithError(err).Fatal("could not get OVH DNS provider")
+			logger.WithError(err).Fatal("could not create provider")
 		}
 
-		zone, err := utils.GetZone(dkimkey.DomainName)
+		err = p.AddDKIMRecord(&dkimkey)
 		if err != nil {
-			logger.WithError(err).Fatal("could not determine zone")
-		}
-		subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
-		if err != nil {
-			logger.WithError(err).Fatal("could not determine subdomain")
+			logger.WithError(err).Fatal("could not publish dkim key")
 		}
 
-		if subdomain == "" {
-			subdomain = zone
-		}
-
-		logger = logger.WithFields(logrus.Fields{
-			"zone":      zone,
-			"subdomain": subdomain,
-		})
-
-		dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
-		result := make([]int, 0)
-
-		err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
-
-		if err != nil {
-			logger.WithError(err).Fatal("could not lookup records")
-		}
-
-		type createParams struct {
-			FieldType string `json:"fieldType"`
-			SubDomain string `json:"subDomain"`
-			Target    string `json:"target"`
-			TTL       int    `json:"ttl"`
-		}
-
-		type updateParams struct {
-			SubDomain string `json:"subDomain"`
-			Target    string `json:"target"`
-			TTL       int    `json:"ttl"`
-		}
-
-		if len(result) == 0 {
-			logger.Info("no DKIM records found, creating a new one")
-
-			c := createParams{
-				FieldType: "TXT",
-				SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
-				Target:    fmt.Sprintf("\"v=DKIM1; k=rsa; p=%s\"", dkimkey.PublicKey),
-				TTL:       60,
-			}
-
-			err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/record", zone), &c, nil)
-			if err != nil {
-				logger.WithError(err).Fatal("could not create new record")
-			}
-			logger.Info("created new DKIM record")
-
-			err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
-			if err != nil {
-				logger.WithError(err).Fatal("could not refresh the zone")
-			}
-			logger.Info("refreshed zone")
-		} else if len(result) == 1 {
-			logger.Info("found one record, updating it")
-
-			u := updateParams{
-				SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
-				Target:    fmt.Sprintf("\"v=DKIM1; k=foo; p=%s\"", dkimkey.PublicKey),
-				TTL:       60,
-			}
-
-			err = ovhClient.Client.Put(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), &u, nil)
-			if err != nil {
-				logger.WithError(err).Fatal("could not update record")
-			}
-			logger.Info("updated existing record")
-
-			err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
-			if err != nil {
-				logger.WithError(err).Fatal("could not refresh the zone")
-			}
-			logger.Info("refreshed zone")
-		}
+		logger.Info("successfully published record")
 	},
 }
 
@@ -344,60 +291,36 @@ var DKIMKeyUnpublishCmd = &cobra.Command{
 		}
 
 		dkimkey := qRes[0]
+
 		logger := logrus.WithFields(logrus.Fields{
 			"dkimkey":  dkimkey.ID,
 			"selector": dkimkey.Selector,
 		})
 
-		ovhClient, err := ovh.NewOVHProvider(cfg)
-		if err != nil {
-			logger.WithError(err).Fatal("could not get OVH DNS provider")
-		}
-
-		zone, err := utils.GetZone(dkimkey.DomainName)
-		if err != nil {
-			logger.WithError(err).Fatal("could not determine zone")
-		}
-		subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
-		if err != nil {
-			logger.WithError(err).Fatal("could not determine subdomain")
-		}
-
-		if subdomain == "" {
-			subdomain = zone
-		}
-
-		logger = logger.WithFields(logrus.Fields{
-			"zone":      zone,
-			"subdomain": subdomain,
-		})
-
-		dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
-		result := make([]int, 0)
-
-		err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
-
-		if err != nil {
-			logger.WithError(err).Fatal("could not lookup records")
-		}
-
-		if len(result) == 0 {
-			logger.Info("no DKIM records found, no need to do anything")
-		} else if len(result) == 1 {
-			logger.Info("found one record, deleting it")
-
-			err = ovhClient.Client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), nil)
-			if err != nil {
-				logger.WithError(err).Fatal("could not delete record")
+		pv := flagProvider
+		if flagProvider == "" {
+			pv = cfg.DefaultProvider
+			if pv == "" {
+				logrus.Fatal("no provider specified")
 			}
-			logger.Info("deleted existing record")
-
-			err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
-			if err != nil {
-				logger.WithError(err).Fatal("could not refresh the zone")
-			}
-			logger.Info("refreshed zone")
 		}
+
+		pGen, ok := provider.Providers[pv]
+		if !ok {
+			logger.Fatalf("no such provider: %s", flagProvider)
+		}
+
+		p, err := pGen(cfg)
+		if err != nil {
+			logger.WithError(err).Fatal("could not create provider")
+		}
+
+		err = p.DeleteDKIMRecord(&dkimkey)
+		if err != nil {
+			logger.WithError(err).Fatal("could not unpublish dkim key")
+		}
+
+		logger.Info("successfully unpublished record")
 	},
 }
 
@@ -414,4 +337,8 @@ func InitDKIMKeyCmd() {
 	DKIMKeyAddCmd.PersistentFlags().BoolVarP(&flagDKIMKeyActive, "active", "a", true, "whether or not the created key is active")
 	DKIMKeyAddCmd.PersistentFlags().StringVarP(&flagSelector, "selector", "s", "", "force a selector for the key")
 	DKIMKeyAddCmd.PersistentFlags().IntVarP(&flagKeyBits, "key-bits", "k", 2048, "force a size for the key")
+
+	DKIMKeyPublishCmd.PersistentFlags().StringVarP(&flagProvider, "provider", "p", "", "provider to which publish the change to")
+
+	DKIMKeyUnpublishCmd.PersistentFlags().StringVarP(&flagProvider, "provider", "p", "", "provider to which unpublish the change from")
 }
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
index cb7e71e..ce86c25 100644
--- a/pkg/cmd/root.go
+++ b/pkg/cmd/root.go
@@ -1,8 +1,6 @@
 package cmd
 
 import (
-	"fmt"
-
 	"git.maurice.fr/thomas/mailout/pkg/config"
 	"github.com/spf13/cobra"
 )
@@ -26,15 +24,6 @@ var RootCmd = &cobra.Command{
 	},
 }
 
-var VersionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Print the version number",
-	Long:  ``,
-	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("v0.0.1")
-	},
-}
-
 func InitRootCmd() {
 	InitUserCmd()
 	InitDKIMKeyCmd()
diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go
new file mode 100644
index 0000000..574c987
--- /dev/null
+++ b/pkg/cmd/version.go
@@ -0,0 +1,17 @@
+package cmd
+
+import (
+	"fmt"
+
+	"git.maurice.fr/thomas/mailout/pkg/version"
+	"github.com/spf13/cobra"
+)
+
+var VersionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "Print the version number",
+	Long:  ``,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Printf("mailoutctl %s (%s) built on %s\n", version.Version, version.Commit, version.BuildTime)
+	},
+}
diff --git a/pkg/providers/ovh.go b/pkg/providers/ovh.go
new file mode 100644
index 0000000..06b00ef
--- /dev/null
+++ b/pkg/providers/ovh.go
@@ -0,0 +1,191 @@
+package provider
+
+import (
+	"fmt"
+
+	"git.maurice.fr/thomas/mailout/pkg/config"
+	"git.maurice.fr/thomas/mailout/pkg/models"
+	"git.maurice.fr/thomas/mailout/pkg/utils"
+	ovhgo "github.com/ovh/go-ovh/ovh"
+	"github.com/sirupsen/logrus"
+)
+
+type OVHProvider struct {
+	client *ovhgo.Client
+}
+
+func NewOVHProvider(cfg *config.Config) (Provider, error) {
+	config := cfg.Providers.OVH
+
+	if config == nil {
+		return nil, fmt.Errorf("no ovh configuration specified")
+	}
+
+	c, err := ovhgo.NewClient(
+		config.Endpoint,
+		config.ApplicationKey,
+		config.ApplicationSecret,
+		config.ConsumerKey,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &OVHProvider{
+		client: c,
+	}, nil
+}
+
+func (p *OVHProvider) AddDKIMRecord(dkimkey *models.DKIMKey) error {
+	logger := logrus.WithFields(logrus.Fields{
+		"dkimkey":  dkimkey.ID,
+		"selector": dkimkey.Selector,
+	})
+
+	zone, err := utils.GetZone(dkimkey.DomainName)
+	if err != nil {
+		return fmt.Errorf("could not determine zone: %w", err)
+	}
+
+	subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
+	if err != nil {
+		return fmt.Errorf("could not determine subdomain: %w", err)
+	}
+
+	if subdomain == "" {
+		subdomain = zone
+	}
+
+	logger = logger.WithFields(logrus.Fields{
+		"zone":      zone,
+		"subdomain": subdomain,
+	})
+
+	dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
+	result := make([]int, 0)
+
+	err = p.client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
+
+	if err != nil {
+		return fmt.Errorf("could not lookup records: %w", err)
+	}
+
+	type createParams struct {
+		FieldType string `json:"fieldType"`
+		SubDomain string `json:"subDomain"`
+		Target    string `json:"target"`
+		TTL       int    `json:"ttl"`
+	}
+
+	type updateParams struct {
+		SubDomain string `json:"subDomain"`
+		Target    string `json:"target"`
+		TTL       int    `json:"ttl"`
+	}
+
+	if len(result) == 0 {
+		logger.Info("no DKIM records found, creating a new one")
+
+		c := createParams{
+			FieldType: "TXT",
+			SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
+			Target:    fmt.Sprintf("v=DKIM1; k=rsa; p=%s", dkimkey.PublicKey),
+			TTL:       60,
+		}
+
+		err = p.client.Post(fmt.Sprintf("/domain/zone/%s/record", zone), &c, nil)
+		if err != nil {
+			return fmt.Errorf("could not create new record: %w", err)
+		}
+		logger.Info("created new DKIM record")
+
+		err = p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
+		if err != nil {
+			return fmt.Errorf("could not refresh the zone: %w", err)
+		}
+		logger.Info("refreshed zone")
+	} else if len(result) == 1 {
+		logger.Info("found one record, updating it")
+
+		u := updateParams{
+			SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
+			Target:    fmt.Sprintf("v=DKIM1; k=rsa; p=%s", dkimkey.PublicKey),
+			TTL:       60,
+		}
+
+		err = p.client.Put(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), &u, nil)
+		if err != nil {
+			return fmt.Errorf("could not update record: %w", err)
+		}
+		logger.Info("updated existing record")
+
+		err = p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
+		if err != nil {
+			return fmt.Errorf("could not refresh the zone: %w", err)
+		}
+		logger.Info("refreshed zone")
+	} else {
+		logrus.Error("more than 1 records matched the query, it is unsafe for me to proceed, check the DNS zone manually")
+		return fmt.Errorf("more than one record returned for update/creation")
+	}
+
+	return nil
+}
+
+func (p *OVHProvider) DeleteDKIMRecord(dkimkey *models.DKIMKey) error {
+	logger := logrus.WithFields(logrus.Fields{
+		"dkimkey":  dkimkey.ID,
+		"selector": dkimkey.Selector,
+	})
+
+	zone, err := utils.GetZone(dkimkey.DomainName)
+	if err != nil {
+		return fmt.Errorf("could not determine zone: %w", err)
+	}
+	subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
+	if err != nil {
+		return fmt.Errorf("could not determine subdomain: %w", err)
+	}
+
+	if subdomain == "" {
+		subdomain = zone
+	}
+
+	logger = logger.WithFields(logrus.Fields{
+		"zone":      zone,
+		"subdomain": subdomain,
+	})
+
+	dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
+	result := make([]int, 0)
+
+	err = p.client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
+
+	if err != nil {
+		return fmt.Errorf("could not lookup records: %w", err)
+	}
+
+	if len(result) == 0 {
+		logger.Info("no DKIM records found, no need to do anything")
+	} else if len(result) == 1 {
+		logger.Info("found one record, deleting it")
+
+		err = p.client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), nil)
+		if err != nil {
+			return fmt.Errorf("could not delete record: %w", err)
+		}
+		logger.Info("deleted existing record")
+
+		err = p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
+		if err != nil {
+			return fmt.Errorf("could not refresh the zone: %w", err)
+		}
+		logger.Info("refreshed zone")
+	} else {
+		logrus.Error("more than 1 records matched the query, it is unsafe for me to proceed, check the DNS zone manually")
+		return fmt.Errorf("more than one record returned for deletion")
+	}
+
+	return nil
+}
diff --git a/pkg/providers/ovh/ovh.go b/pkg/providers/ovh/ovh.go
deleted file mode 100644
index 1be1689..0000000
--- a/pkg/providers/ovh/ovh.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package ovh
-
-import (
-	"fmt"
-
-	"git.maurice.fr/thomas/mailout/pkg/config"
-	ovhgo "github.com/ovh/go-ovh/ovh"
-)
-
-type OVHProvider struct {
-	Client *ovhgo.Client
-}
-
-func NewOVHProvider(cfg *config.Config) (*OVHProvider, error) {
-	config := cfg.Providers.OVH
-
-	if config == nil {
-		return nil, fmt.Errorf("no ovh configuration specified")
-	}
-
-	c, err := ovhgo.NewClient(
-		config.Endpoint,
-		config.ApplicationKey,
-		config.ApplicationSecret,
-		config.ConsumerKey,
-	)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return &OVHProvider{
-		Client: c,
-	}, nil
-}
diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go
index 4f504f6..07d2760 100644
--- a/pkg/providers/provider.go
+++ b/pkg/providers/provider.go
@@ -1 +1,17 @@
 package provider
+
+import (
+	"git.maurice.fr/thomas/mailout/pkg/config"
+	"git.maurice.fr/thomas/mailout/pkg/models"
+)
+
+type Provider interface {
+	AddDKIMRecord(*models.DKIMKey) error
+	DeleteDKIMRecord(*models.DKIMKey) error
+}
+
+var (
+	Providers = map[string]func(*config.Config) (Provider, error){
+		"ovh": NewOVHProvider,
+	}
+)
diff --git a/pkg/version/version.go b/pkg/version/version.go
new file mode 100644
index 0000000..6c7b25a
--- /dev/null
+++ b/pkg/version/version.go
@@ -0,0 +1,7 @@
+package version
+
+var (
+	Version   = "master"
+	BuildTime = "now"
+	Commit    = "master"
+)