diff --git a/.gitignore b/.gitignore index 7dcb1e9..81754e1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +_out # Ignore the built binary cert-manager-webhook-example diff --git a/deploy/example-webhook/.helmignore b/deploy/hetzner-webhook/.helmignore similarity index 100% rename from deploy/example-webhook/.helmignore rename to deploy/hetzner-webhook/.helmignore diff --git a/deploy/example-webhook/Chart.yaml b/deploy/hetzner-webhook/Chart.yaml similarity index 80% rename from deploy/example-webhook/Chart.yaml rename to deploy/hetzner-webhook/Chart.yaml index 77c6ead..58e44ec 100644 --- a/deploy/example-webhook/Chart.yaml +++ b/deploy/hetzner-webhook/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes -name: example-webhook +name: hetzner-webhook version: 0.1.0 diff --git a/deploy/example-webhook/templates/NOTES.txt b/deploy/hetzner-webhook/templates/NOTES.txt similarity index 100% rename from deploy/example-webhook/templates/NOTES.txt rename to deploy/hetzner-webhook/templates/NOTES.txt diff --git a/deploy/example-webhook/templates/_helpers.tpl b/deploy/hetzner-webhook/templates/_helpers.tpl similarity index 100% rename from deploy/example-webhook/templates/_helpers.tpl rename to deploy/hetzner-webhook/templates/_helpers.tpl diff --git a/deploy/example-webhook/templates/apiservice.yaml b/deploy/hetzner-webhook/templates/apiservice.yaml similarity index 100% rename from deploy/example-webhook/templates/apiservice.yaml rename to deploy/hetzner-webhook/templates/apiservice.yaml diff --git a/deploy/example-webhook/templates/deployment.yaml b/deploy/hetzner-webhook/templates/deployment.yaml similarity index 100% rename from deploy/example-webhook/templates/deployment.yaml rename to deploy/hetzner-webhook/templates/deployment.yaml diff --git a/deploy/example-webhook/templates/pki.yaml b/deploy/hetzner-webhook/templates/pki.yaml similarity index 100% rename from deploy/example-webhook/templates/pki.yaml rename to deploy/hetzner-webhook/templates/pki.yaml diff --git a/deploy/example-webhook/templates/rbac.yaml b/deploy/hetzner-webhook/templates/rbac.yaml similarity index 100% rename from deploy/example-webhook/templates/rbac.yaml rename to deploy/hetzner-webhook/templates/rbac.yaml diff --git a/deploy/example-webhook/templates/service.yaml b/deploy/hetzner-webhook/templates/service.yaml similarity index 100% rename from deploy/example-webhook/templates/service.yaml rename to deploy/hetzner-webhook/templates/service.yaml diff --git a/deploy/example-webhook/values.yaml b/deploy/hetzner-webhook/values.yaml similarity index 97% rename from deploy/example-webhook/values.yaml rename to deploy/hetzner-webhook/values.yaml index 66d4922..ab3fb38 100644 --- a/deploy/example-webhook/values.yaml +++ b/deploy/hetzner-webhook/values.yaml @@ -9,7 +9,7 @@ groupName: dns.hetzner.cloud certManager: - namespace: kube-system + namespace: cert-manager serviceAccountName: cert-manager image: diff --git a/go.mod b/go.mod index 21e7aae..c0f7fc9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/jetstack/cert-manager-webhook-example +module github.com/mecodia/cert-manager-webhook-hetzner go 1.13 diff --git a/main.go b/main.go index 85aeac9..f8ad94b 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,13 @@ package main import ( + "bytes" "encoding/json" "fmt" + "io/ioutil" + "net/http" "os" + "strings" extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" //"k8s.io/client-go/kubernetes" @@ -26,15 +30,15 @@ func main() { // webhook, where the Name() method will be used to disambiguate between // the different implementations. cmd.RunWebhookServer(GroupName, - &customDNSProviderSolver{}, + &hetznerDNSProviderSolver{}, ) } -// customDNSProviderSolver implements the provider-specific logic needed to +// hetznerDNSProviderSolver 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/jetstack/cert-manager/pkg/acme/webhook.Solver` // interface. -type customDNSProviderSolver struct { +type hetznerDNSProviderSolver 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 @@ -44,7 +48,7 @@ type customDNSProviderSolver struct { //client kubernetes.Clientset } -// customDNSProviderConfig is a structure that is used to decode into when +// hetznerDNSProviderConfig 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 @@ -58,14 +62,13 @@ type customDNSProviderSolver struct { // 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 customDNSProviderConfig struct { +type hetznerDNSProviderConfig 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 v1alpha1.SecretKeySelector `json:"apiKeySecretRef"` + APIKey string `json:"apiKey"` } // Name is used as the name for this DNS solver when referencing it on the ACME @@ -74,8 +77,29 @@ type customDNSProviderConfig struct { // 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 *customDNSProviderSolver) Name() string { - return "my-custom-solver" +func (c *hetznerDNSProviderSolver) Name() string { + return "hetzner" +} + +type Zones struct { + Zones []Zone `json:"zones"` +} + +type Zone struct { + ZoneID string `json:"id"` +} + +type Entries struct { + Records []Entry `json:"records"` +} + +type Entry struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + TTL int `json:"ttl"` + Type string `json:"type"` + Value string `json:"value"` + ZoneID string `json:"zone_id"` } // Present is responsible for actually presenting the DNS record with the @@ -83,7 +107,7 @@ func (c *customDNSProviderSolver) Name() string { // 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 *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { +func (c *hetznerDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { cfg, err := loadConfig(ch.Config) if err != nil { return err @@ -92,6 +116,55 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { // TODO: do something more useful with the decoded configuration fmt.Printf("Decoded configuration %v", cfg) + name, zone := c.getDomainAndEntry(ch) + + // Get Zones (GET https://dns.hetzner.com/api/v1/zones) + // Create client + client := &http.Client{} + + // Create request + req, err := http.NewRequest("GET", "https://dns.hetzner.com/api/v1/zones?search_name="+zone, nil) + // Headers + req.Header.Add("Auth-API-Token", cfg.APIKey) + + // Fetch Request + resp, err := client.Do(req) + if err != nil { + fmt.Println("Failure : ", err) + } + + // Read Response Body + respBody := Zones{} + json.NewDecoder(resp.Body).Decode(&respBody) + + // Display Results + fmt.Println("response Status : ", resp.Status) + fmt.Println("response Headers : ", resp.Header) + fmt.Println("response Body : ", respBody.Zones[0].ZoneID) + + // Create DNS + entry, err := json.Marshal(Entry{"", name, 300, "TXT", ch.Key, respBody.Zones[0].ZoneID}) + body := bytes.NewBuffer(entry) + + // Create request + req, err = http.NewRequest("POST", "https://dns.hetzner.com/api/v1/records", body) + // Headers + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Auth-API-Token", cfg.APIKey) + + // Fetch Request + resp, err = client.Do(req) + if err != nil { + fmt.Println("Failure : ", err) + } + + // Read Response Body + respBody2, _ := ioutil.ReadAll(resp.Body) + + // Display Results + fmt.Println("response Status : ", resp.Status) + fmt.Println("response Headers : ", resp.Header) + fmt.Println("response Body : ", string(respBody2)) // TODO: add code that sets a record in the DNS provider's console return nil } @@ -102,7 +175,89 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { // 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 *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { +func (c *hetznerDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { + 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) + + name, zone := c.getDomainAndEntry(ch) + + // Get Zones (GET https://dns.hetzner.com/api/v1/zones) + // Create client + client := &http.Client{} + + // Create request + zReq, err := http.NewRequest("GET", "https://dns.hetzner.com/api/v1/zones?search_name="+zone, nil) + // Headers + zReq.Header.Add("Auth-API-Token", cfg.APIKey) + + // Fetch Request + zResp, err := client.Do(zReq) + if err != nil { + fmt.Println("Failure : ", err) + } + + // Read Response Body + zRespBody := Zones{} + json.NewDecoder(zResp.Body).Decode(&zRespBody) + + // Display Results + fmt.Println("response Status : ", zResp.Status) + fmt.Println("response Headers : ", zResp.Header) + fmt.Println("response Body : ", zRespBody.Zones[0].ZoneID) + fmt.Println("response Body : ", name) + + // Create request + eReq, err := http.NewRequest("GET", "https://dns.hetzner.com/api/v1/records?zone_id="+zRespBody.Zones[0].ZoneID, nil) + // Headers + eReq.Header.Add("Auth-API-Token", cfg.APIKey) + + // Fetch Request + eResp, err := client.Do(eReq) + if err != nil { + fmt.Println("Failure : ", err) + } + + // Read Response Body + eRespBody := Entries{} + json.NewDecoder(eResp.Body).Decode(&eRespBody) + + // Display Results + fmt.Println("response Status : ", eResp.Status) + fmt.Println("response Headers : ", eResp.Header) + fmt.Println("response Body : ", eRespBody) + + for _, e := range eRespBody.Records { + if e.Type == "TXT" && e.Name == name && e.Value == ch.Key { + fmt.Println("Found DOMAIN: ", e) + // Delete Record (DELETE https://dns.hetzner.com/api/v1/records/1) + // Create request + req, err := http.NewRequest("DELETE", "https://dns.hetzner.com/api/v1/records/"+e.ID, nil) + + // Headers + req.Header.Add("Auth-API-Token", cfg.APIKey) + + // Fetch Request + resp, err := client.Do(req) + + if err != nil { + fmt.Println("Failure : ", err) + } + + // Read Response Body + respBody, _ := ioutil.ReadAll(resp.Body) + + // Display Results + fmt.Println("response Status : ", resp.Status) + fmt.Println("response Headers : ", resp.Header) + fmt.Println("response Body : ", string(respBody)) + } + } + // TODO: add code that deletes a record from the DNS provider's console return nil } @@ -116,25 +271,14 @@ func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { // 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 *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { - ///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO - ///// YOUR CUSTOM DNS PROVIDER - - //cl, err := kubernetes.NewForConfig(kubeClientConfig) - //if err != nil { - // return err - //} - // - //c.client = cl - - ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE +func (c *hetznerDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { return nil } // loadConfig is a small helper function that decodes JSON configuration into // the typed config struct. -func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) { - cfg := customDNSProviderConfig{} +func loadConfig(cfgJSON *extapi.JSON) (hetznerDNSProviderConfig, error) { + cfg := hetznerDNSProviderConfig{} // handle the 'base case' where no configuration has been provided if cfgJSON == nil { return cfg, nil @@ -145,3 +289,11 @@ func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) { return cfg, nil } + +func (c *hetznerDNSProviderSolver) getDomainAndEntry(ch *v1alpha1.ChallengeRequest) (string, string) { + // Both ch.ResolvedZone and ch.ResolvedFQDN end with a dot: '.' + entry := strings.TrimSuffix(ch.ResolvedFQDN, ch.ResolvedZone) + entry = strings.TrimSuffix(entry, ".") + domain := strings.TrimSuffix(ch.ResolvedZone, ".") + return entry, domain +} diff --git a/main_test.go b/main_test.go index 4e32419..64e0589 100644 --- a/main_test.go +++ b/main_test.go @@ -8,7 +8,8 @@ import ( ) var ( - zone = os.Getenv("TEST_ZONE_NAME") + zone = os.Getenv("TEST_ZONE_NAME") + kubeBuilderBinPath = "./_out/kubebuilder/bin/" ) func TestRunsSuite(t *testing.T) { @@ -16,7 +17,8 @@ func TestRunsSuite(t *testing.T) { // snippet of valid configuration that should be included on the // ChallengeRequest passed as part of the test cases. - fixture := dns.NewFixture(&customDNSProviderSolver{}, + fixture := dns.NewFixture(&hetznerDNSProviderSolver{}, + dns.SetBinariesPath(kubeBuilderBinPath), dns.SetResolvedZone(zone), dns.SetAllowAmbientCredentials(false), dns.SetManifestPath("testdata/my-custom-solver"), diff --git a/scripts/fetch-test-binaries.sh b/scripts/fetch-test-binaries.sh index f1f641a..04130a3 100755 --- a/scripts/fetch-test-binaries.sh +++ b/scripts/fetch-test-binaries.sh @@ -1 +1,61 @@ #!/usr/bin/env bash + +set -e + +#hack_dir=$(dirname ${BASH_SOURCE}) +#source ${hack_dir}/common.sh + +k8s_version=1.16.4 +goarch=amd64 +goos="unknown" + +if [[ "$OSTYPE" == "linux-gnu" ]]; then + goos="linux" +elif [[ "$OSTYPE" == "darwin"* ]]; then + goos="darwin" +fi + +if [[ "$goos" == "unknown" ]]; then + echo "OS '$OSTYPE' not supported. Aborting." >&2 + exit 1 +fi + +tmp_root=./_out +kb_root_dir=$tmp_root/kubebuilder + +# Turn colors in this script off by setting the NO_COLOR variable in your +# environment to any value: +# +# $ NO_COLOR=1 test.sh +NO_COLOR=${NO_COLOR:-""} +if [ -z "$NO_COLOR" ]; then + header=$'\e[1;33m' + reset=$'\e[0m' +else + header='' + reset='' +fi + +function header_text { + echo "$header$*$reset" +} + +# fetch k8s API gen tools and make it available under kb_root_dir/bin. +function fetch_kb_tools { + header_text "fetching tools" + mkdir -p $tmp_root + kb_tools_archive_name="kubebuilder-tools-$k8s_version-$goos-$goarch.tar.gz" + kb_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/$kb_tools_archive_name" + + kb_tools_archive_path="$tmp_root/$kb_tools_archive_name" + if [ ! -f $kb_tools_archive_path ]; then + curl -sL ${kb_tools_download_url} -o "$kb_tools_archive_path" + fi + tar -zvxf "$kb_tools_archive_path" -C "$tmp_root/" +} + +header_text "using tools" +fetch_kb_tools + +header_text "kubebuilder tools (etcd, kubectl, kube-apiserver)used to perform local tests installed under $tmp_root/kubebuilder/bin/" +exit 0 diff --git a/testdata/my-custom-solver/config.json b/testdata/my-custom-solver/config.json index 0967ef4..4c0a7b8 100644 --- a/testdata/my-custom-solver/config.json +++ b/testdata/my-custom-solver/config.json @@ -1 +1,4 @@ -{} +{ + "apiKey": "EBEMLAlXhFAW05jyeoFMJPe2e12wJEf0" +} + \ No newline at end of file