diff --git a/go.mod b/go.mod index 72ffe9b..4638fef 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/stretchr/testify v1.8.4 k8s.io/apiextensions-apiserver v0.29.0 k8s.io/client-go v0.29.0 + github.com/sirupsen/logrus v1.9.3 ) require ( diff --git a/main.go b/main.go index 1e487bb..60c1520 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,10 @@ package main import ( - "encoding/json" - "fmt" "os" - extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - - cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" - "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd" - "github.com/cert-manager/cert-manager/pkg/issuer/acme/dns/util" -) - -const ( - providerName = "sthome" + "github.com/stuurmcp/cert-manager-webhook-sthome/sthome" ) var GroupName = os.Getenv("GROUP_NAME") @@ -26,132 +14,6 @@ func main() { panic("GROUP_NAME must be specified") } cmd.RunWebhookServer(GroupName, - &sthomeDNSProviderSolver{}, + &sthome.localDNSProviderSolver{}, ) } - -// sthomeDNSProviderSolver implements the provider-specific logic needed to -// 'present' an ACME challenge TXT record for your own DNS provider. -// To do so, it must implement the `github.com/cert-manager/cert-manager/pkg/acme/webhook.Solver` -// interface. -type sthomeDNSProviderSolver struct { - // If a Kubernetes 'clientset' is needed, you must: - // 1. uncomment the additional `client` field in this structure below - // 2. uncomment the "k8s.io/client-go/kubernetes" import at the top of the file - // 3. uncomment the relevant code in the Initialize method below - // 4. ensure your webhook's service account has the required RBAC role - // assigned to it for interacting with the Kubernetes APIs you need. - client kubernetes.Clientset -} - -// sthomeDNSProviderConfig is a structure that is used to decode into when -// solving a DNS01 challenge. -// This information is provided by cert-manager, and may be a reference to -// additional configuration that's needed to solve the challenge for this -// particular certificate or issuer. -// This typically includes references to Secret resources containing DNS -// provider credentials, in cases where a 'multi-tenant' DNS solver is being -// created. -// If you do *not* require per-issuer or per-certificate configuration to be -// provided to your webhook, you can skip decoding altogether in favour of -// using CLI flags or similar to provide configuration. -// You should not include sensitive information here. If credentials need to -// be used by your provider here, you should reference a Kubernetes Secret -// resource and fetch these credentials using a Kubernetes clientset. -type sthomeDNSProviderConfig struct { - // Change the two fields below according to the format of the configuration - // to be decoded. - // These fields will be set by users in the - // `issuer.spec.acme.dns01.providers.webhook.config` field. - - Email string `json:"email"` - APIKeySecretRef cmmetav1.SecretKeySelector `json:"apiKeySecretRef"` -} - -// Name is used as the name for this DNS solver when referencing it on the ACME -// Issuer resource. -// This should be unique **within the group name**, i.e. you can have two -// solvers configured with the same Name() **so long as they do not co-exist -// within a single webhook deployment**. -// For example, `cloudflare` may be used as the name of a solver. -func (c *sthomeDNSProviderSolver) Name() string { - return providerName -} - -// Present is responsible for actually presenting the DNS record with the -// DNS provider. -// This method should tolerate being called multiple times with the same value. -// cert-manager itself will later perform a self check to ensure that the -// solver has correctly configured the DNS provider. -func (c *sthomeDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { - domainName := extractDomainName(ch.ResolvedZone) - cfg, err := loadConfig(ch.Config) - if err != nil { - return err - } - - // TODO: do something more useful with the decoded configuration - fmt.Printf("Decoded configuration %v", cfg) - fmt.Printf("presenting record for %s (%s)\n", ch.ResolvedFQDN, domainName) - // TODO: add code that sets a record in the DNS provider's console - return nil -} - -// CleanUp should delete the relevant TXT record from the DNS provider console. -// If multiple TXT records exist with the same record name (e.g. -// _acme-challenge.example.com) then **only** the record with the same `key` -// value provided on the ChallengeRequest should be cleaned up. -// This is in order to facilitate multiple DNS validations for the same domain -// concurrently. -func (c *sthomeDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { - // TODO: add code that deletes a record from the DNS provider's console - return nil -} - -// Initialize will be called when the webhook first starts. -// This method can be used to instantiate the webhook, i.e. initialising -// connections or warming up caches. -// Typically, the kubeClientConfig parameter is used to build a Kubernetes -// client that can be used to fetch resources from the Kubernetes API, e.g. -// Secret resources containing credentials used to authenticate with DNS -// provider accounts. -// The stopCh can be used to handle early termination of the webhook, in cases -// where a SIGTERM or similar signal is sent to the webhook process. -func (c *sthomeDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { - ///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO - ///// YOUR sthome DNS PROVIDER - - cl, err := kubernetes.NewForConfig(kubeClientConfig) - if err != nil { - return err - } - - c.client = *cl - - ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE - return nil -} - -// loadConfig is a small helper function that decodes JSON configuration into -// the typed config struct. -func loadConfig(cfgJSON *extapi.JSON) (sthomeDNSProviderConfig, error) { - cfg := sthomeDNSProviderConfig{} - // handle the 'base case' where no configuration has been provided - if cfgJSON == nil { - return cfg, nil - } - if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil { - return cfg, fmt.Errorf("error decoding solver config: %v", err) - } - - return cfg, nil -} - -func extractDomainName(zone string) string { - authZone, err := util.FindZoneByFqdn(zone, util.RecursiveNameservers) - if err != nil { - fmt.Printf("could not get zone by fqdn %v", err) - return zone - } - return util.UnFqdn(authZone) -} diff --git a/sthome/config.go b/sthome/config.go new file mode 100644 index 0000000..79643b7 --- /dev/null +++ b/sthome/config.go @@ -0,0 +1,29 @@ +package sthome + +import ( + v1 "k8s.io/api/core/v1" +) + +// localDNSProviderConfig is a structure that is used to decode into when +// solving a DNS01 challenge. +// This information is provided by cert-manager, and may be a reference to +// additional configuration that's needed to solve the challenge for this +// particular certificate or issuer. +// This typically includes references to Secret resources containing DNS +// provider credentials, in cases where a 'multi-tenant' DNS solver is being +// created. +// If you do *not* require per-issuer or per-certificate configuration to be +// provided to your webhook, you can skip decoding altogether in favour of +// using CLI flags or similar to provide configuration. +// You should not include sensitive information here. If credentials need to +// be used by your provider here, you should reference a Kubernetes Secret +// resource and fetch these credentials using a Kubernetes clientset. +type localDNSProviderConfig struct { + // Change the two fields below according to the format of the configuration + // to be decoded. + // These fields will be set by users in the + // `issuer.spec.acme.dns01.providers.webhook.config` field. + + Email string `json:"email"` + APIKeySecretRef v1.SecretKeySelector `json:"apiKeySecretRef"` +} diff --git a/sthome/dns.go b/sthome/dns.go index a9bc9f8..640d265 100644 --- a/sthome/dns.go +++ b/sthome/dns.go @@ -1,3 +1,4 @@ +// not implemented package sthome import ( diff --git a/sthome/shell.go b/sthome/shell.go new file mode 100644 index 0000000..cfa715c --- /dev/null +++ b/sthome/shell.go @@ -0,0 +1,28 @@ +package sthome + +import ( + "os" + "os/exec" +) + +func Execute(script string, command []string) (bool, error) { + + cmd := &exec.Cmd{ + Path: script, + Args: command, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + + err := cmd.Start() + if err != nil { + return false, err + } + + err = cmd.Wait() + if err != nil { + return false, err + } + + return true, nil +} diff --git a/sthome/solver.local.go b/sthome/solver.local.go new file mode 100644 index 0000000..383dfae --- /dev/null +++ b/sthome/solver.local.go @@ -0,0 +1,119 @@ +package sthome + +import ( + "fmt" + + //"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + //cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" + //"github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd" + "github.com/cert-manager/cert-manager/pkg/issuer/acme/dns/util" +) + +const ( + providerName = "sthome" + dnsUpdaterScript = "/mnt/stpool1/scripts/acme/updatedns.sh" +) + +// localDNSProviderSolver implements the provider-specific logic needed to +// 'present' an ACME challenge TXT record for your own DNS provider. +// To do so, it must implement the `github.com/cert-manager/cert-manager/pkg/acme/webhook.Solver` +// interface. +type localDNSProviderSolver struct { + client kubernetes.Clientset + //client kubernetes.Interface +} + +// Name is used as the name for this DNS solver when referencing it on the ACME +// Issuer resource. +// This should be unique **within the group name**, i.e. you can have two +// solvers configured with the same Name() **so long as they do not co-exist +// within a single webhook deployment**. +// For example, `cloudflare` may be used as the name of a solver. +func (p *localDNSProviderSolver) Name() string { + return providerName +} + +// Present is responsible for actually presenting the DNS record with the +// DNS provider. +// This method should tolerate being called multiple times with the same value. +// cert-manager itself will later perform a self check to ensure that the +// solver has correctly configured the DNS provider. +func (loc *localDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { + domainName := extractDomainName(ch.ResolvedZone) + cfg, err := loadConfig(ch.Config) + if err != nil { + return err + } + + // TODO: do something more useful with the decoded configuration + fmt.Printf("Decoded configuration %v", cfg) + fmt.Printf("presenting record for %s (%s)\n", ch.ResolvedFQDN, domainName) + // TODO: add code that sets a record in the DNS provider's console + + // shell command + command := []string{ + dnsUpdaterScript, + "arg1=-set", + "arg2=.net", + fmt.Sprintf("arg3=%s", ch.DNSName), + "arg4=TXT", + fmt.Sprintf("arg5=%s", ch.Key), + } + Execute(dnsUpdaterScript, command) + + return nil +} + +// CleanUp should delete the relevant TXT record from the DNS provider console. +// If multiple TXT records exist with the same record name (e.g. +// _acme-challenge.example.com) then **only** the record with the same `key` +// value provided on the ChallengeRequest should be cleaned up. +// This is in order to facilitate multiple DNS validations for the same domain +// concurrently. +func (s *localDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { + // TODO: add code that deletes a record from the DNS provider's console + + // shell command + command := []string{ + dnsUpdaterScript, + "arg1=-unset", + "arg2=.net", + fmt.Sprintf("arg3=%s", ch.DNSName), + "arg4=TXT", + fmt.Sprintf("arg5=%s", ch.Key), + } + Execute(dnsUpdaterScript, command) + return nil +} + +// Initialize will be called when the webhook first starts. +// This method can be used to instantiate the webhook, i.e. initialising +// connections or warming up caches. +// Typically, the kubeClientConfig parameter is used to build a Kubernetes +// client that can be used to fetch resources from the Kubernetes API, e.g. +// Secret resources containing credentials used to authenticate with DNS +// provider accounts. +// The stopCh can be used to handle early termination of the webhook, in cases +// where a SIGTERM or similar signal is sent to the webhook process. +func (c *localDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { + cl, err := kubernetes.NewForConfig(kubeClientConfig) + if err != nil { + return fmt.Errorf("failed to get kubernetes client: %w", err) + } + c.client = *cl + return nil +} + +func extractDomainName(zone string) string { + authZone, err := util.FindZoneByFqdn(zone, util.RecursiveNameservers) + if err != nil { + fmt.Printf("could not get zone by fqdn %v", err) + return zone + } + return util.UnFqdn(authZone) +} diff --git a/sthome/sthome.go b/sthome/solver.sthome.go similarity index 96% rename from sthome/sthome.go rename to sthome/solver.sthome.go index dfaeabc..bcfbf06 100644 --- a/sthome/sthome.go +++ b/sthome/solver.sthome.go @@ -1,4 +1,4 @@ -// must pass cert-manager DNS conformance tests +// not implemented package sthome import ( diff --git a/sthome/utils.go b/sthome/utils.go new file mode 100644 index 0000000..590a2fd --- /dev/null +++ b/sthome/utils.go @@ -0,0 +1,23 @@ +package sthome + +import ( + "encoding/json" + "fmt" + + extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +// loadConfig is a small helper function that decodes JSON configuration into +// the typed config struct. +func loadConfig(cfgJSON *extapi.JSON) (localDNSProviderConfig, error) { + cfg := localDNSProviderConfig{} + // handle the 'base case' where no configuration has been provided + if cfgJSON == nil { + return cfg, nil + } + if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil { + return cfg, fmt.Errorf("error decoding solver config: %v", err) + } + + return cfg, nil +}