diff --git a/Dockerfile b/Dockerfile index ad21908..8be7f93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine3.18 AS build_deps +FROM golang:1.21-alpine3.19 AS build_deps # Let scripts know we're running in Docker (useful for containerised development) ENV RUNNING_IN_DOCKER true @@ -22,7 +22,7 @@ COPY . . RUN CGO_ENABLED=0 go build -o webhook -ldflags '-w -extldflags "-static"' . -FROM alpine:3.18 +FROM alpine:3.19 RUN apk add --no-cache ca-certificates diff --git a/deploy/sthome-webhook/Chart.yaml b/deploy/sthome-webhook/Chart.yaml index facb491..730ece9 100644 --- a/deploy/sthome-webhook/Chart.yaml +++ b/deploy/sthome-webhook/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: v0.0.5-alpha.97 +appVersion: v0.0.5-alpha.106 description: Cert-Manager webhook for sthome name: sthome-webhook -version: 0.0.5-alpha.97 +version: 0.0.5-alpha.106 diff --git a/deploy/sthome-webhook/templates/deployment.yaml b/deploy/sthome-webhook/templates/deployment.yaml index 73d593f..24c8d83 100644 --- a/deploy/sthome-webhook/templates/deployment.yaml +++ b/deploy/sthome-webhook/templates/deployment.yaml @@ -62,9 +62,6 @@ spec: - name: scriptdir mountPath: /acme readOnly: false - - name: workdir - mountPath: /workdir - readOnly: false - name: webroot mountPath: /webroot readOnly: false @@ -77,9 +74,6 @@ spec: - name: scriptdir hostPath: path: {{ .Values.host.scriptdir }} - - name: workdir - hostPath: - path: {{ .Values.host.workdir }} - name: webroot hostPath: path: {{ .Values.host.webrootdir }} diff --git a/deploy/sthome-webhook/values.yaml b/deploy/sthome-webhook/values.yaml index 465cae1..37f24de 100644 --- a/deploy/sthome-webhook/values.yaml +++ b/deploy/sthome-webhook/values.yaml @@ -31,7 +31,7 @@ clusterIssuer: image: repository: stuurmcp/cert-manager-webhook-sthome #repository: wstat.sthome.net:5000/cert-manager-webhook-sthome - tag: 0.0.5-alpha.97 + tag: 0.0.5-alpha.106 #pullPolicy should be IfNotPresent. Set to Always for testing purposes pullPolicy: IfNotPresent @@ -66,10 +66,11 @@ pki: servingCertificateDuration: 8760h # 1y host: - workdir: /mnt/stpool1/scripts/acme/cert-manager-webhook-sthome scriptdir: /mnt/stpool1/scripts/acme webrootdir: /mnt/stpool1/apps/static-web-server +extraArgs: + secret: accessKey: "" secretKey: "" diff --git a/main.go b/main.go index eb19d68..39d7570 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,6 @@ import ( "os" "strings" - /* - "fmt" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" - "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" - "github.com/cert-manager/cert-manager/pkg/issuer/acme/dns/util" - */ "github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd" dns "github.com/stuurmcp/cert-manager-webhook-sthome/pkg/dns" ) @@ -32,88 +24,5 @@ func main() { } cmd.RunWebhookServer(GroupName, &dns.LocalDNSProviderSolver{}, - //&dns.SthomeSolver{}, ) } - -/* -type LocalDNSProviderSolver struct { - client kubernetes.Interface -} - -func (p *LocalDNSProviderSolver) Name() string { - return dns.ProviderName + "loc" -} - -func (loc *LocalDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { - cfg, err := dns.LoadConfig(ch.Config) - if err != nil { - return err - } - klog.Infof("CZ: Presenting record for %s, type: %s, uid: %s, key: %s, ns: %s, fqdn: %s, zone: %s, allowambcred: %t, cfg.secret: %s, cfg.email: %s, cfg.allowz: %s", - ch.DNSName, - ch.UID, - ch.Type, - ch.Key, - ch.ResourceNamespace, - ch.ResolvedFQDN, - ch.ResolvedZone, - ch.AllowAmbientCredentials, - cfg.APIKeySecretRef.Name, - cfg.Email, - strings.Join(cfg.AllowedZones, ","), - ) - // TODO: convert shell script to golang - localip := dns.GetOutboundIP(dns.Dnsserver_net) - success, _ := dns.Execute( - dns.Shell, - dns.AcmeAuthCmd, - "set", - ch.DNSName, - ch.ResolvedFQDN, - ch.Key, - "-l", - localip, - "-v", - ) - klog.Infof("Execute set TXT returned success: %t", success) - return nil -} - -func (loc *LocalDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { - //domainName := extractDomainName(ch.ResolvedZone) - // TODO: add code that deletes a record from the DNS provider's console - localip := dns.GetOutboundIP(dns.Dnsserver_net) - success, _ := dns.Execute( - dns.Shell, - dns.AcmeAuthCmd, - "unset", - ch.DNSName, - ch.ResolvedFQDN, - ch.Key, - "-l", - localip, - "-v", - ) - klog.Infof("Execute unset TXT returned success: %t", success) - return nil -} -func (loc *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) - } - loc.client = cl - klog.InfoS("CZ: Successfully initialised kubernetes client!") - return nil -} - -func extractDomainName(zone string) string { - authZone, err := util.FindZoneByFqdn(zone, util.RecursiveNameservers) - if err != nil { - klog.Errorf("could not get zone by fqdn %v", err) - return zone - } - return util.UnFqdn(authZone) -} -*/ diff --git a/pkg/dns/config.go b/pkg/dns/config.go index 46f1913..6a6f72e 100644 --- a/pkg/dns/config.go +++ b/pkg/dns/config.go @@ -15,9 +15,18 @@ const ( ProviderName = "sthome" bashShell = "/bin/bash" zshShell = "/bin/zsh" + Workdir = "/workdir" AcmeDir = "/acme" Shell = bashShell - AcmeAuthCmd = AcmeDir + "/acmeauth.sh" + + AuthScript = "acmeauth.sh" + DnsUpdScript = "updatedns.sh" + KrbConf = "krb5.conf" + Keytab = "krb5.keytab" + AuthScriptMode = 0744 + DnsUpdScriptMode = 0744 + KrbConfMode = 0644 + KeytabMode = 0644 Dnsserver_net = "10.0.0.15" Dnsserver_lan = "192.168.2.1" @@ -25,6 +34,10 @@ const ( Hostserver_lan = "truenas.sthome.lan" ) +var ( + AcmeAuthCmd = AcmeDir + "/" + AuthScript +) + // 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 diff --git a/pkg/dns/shell.go b/pkg/dns/shell.go index 090a8b9..3aa365e 100644 --- a/pkg/dns/shell.go +++ b/pkg/dns/shell.go @@ -2,60 +2,14 @@ package dns import ( "bytes" - "fmt" "io" "os" "os/exec" - "strings" "sync" "k8s.io/klog/v2" ) -func Execute(shell string, arg ...string) (bool, error) { - var outb, errb bytes.Buffer - cmd := exec.Command(shell, arg...) - cmd.Dir = AcmeDir - cmd.Stdout = &outb - cmd.Stderr = &errb - klog.Infof("cmd: %s\n", cmd.String()) - err := cmd.Run() - outstr := strings.TrimSuffix(outb.String(), "\n") - errstr := strings.TrimSuffix(errb.String(), "\n") - klog.Infof("out:\n%s\n", outstr) - if err != nil { - klog.Errorf("Script returned error:\nerr:\n") - klog.Errorf("%s\n============\n", err) - return false, err - } - if errb.String() != "" { - klog.Infof("stderr:\n") - klog.Errorf("%s\n============\n", errstr) - return false, fmt.Errorf("stderr:\n%q", errstr) - } - klog.Infof("Script returned success\n") - return true, nil -} - -// https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html -func Execute2(shell string, arg ...string) (bool, error) { - var stdoutBuf, stderrBuf bytes.Buffer - cmd := exec.Command(shell, arg...) - cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) - cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) - - err := cmd.Run() - if err != nil { - klog.Errorf("Script returned error:\nerr:\n") - klog.Errorf("%s\n============\n", err) - return false, err - } - outStr, errStr := string(stdoutBuf.String()), string(stderrBuf.String()) - fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr) - klog.Infof("Script returned success\n") - return true, nil -} - // CapturingPassThroughWriter is a writer that remembers // data written to it and passes it to w type CapturingPassThroughWriter struct { @@ -80,8 +34,10 @@ func (w *CapturingPassThroughWriter) Bytes() []byte { return w.buf.Bytes() } -func Execute3(shell string, arg ...string) (bool, error) { +func Execute(shell string, arg ...string) (bool, error) { var errStdout, errStderr error + crlf := []byte("\r\n") + lf := []byte("\n") cmd := exec.Command(shell, arg...) stdoutIn, _ := cmd.StdoutPipe() stderrIn, _ := cmd.StderrPipe() @@ -112,9 +68,10 @@ func Execute3(shell string, arg ...string) (bool, error) { } //outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes()) //fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr) - errStr := string(stderr.Bytes()) + errb := bytes.TrimSuffix(stderr.Bytes(), crlf) + errb = bytes.TrimSuffix(errb, lf) if stderr != nil { - klog.Infof("err:\n%s\n", errStr) + klog.Infof("err:\n%s\n", string(errb)) } return true, nil } diff --git a/pkg/dns/solver_local.go b/pkg/dns/solver_local.go index b282c2c..80e5ea1 100644 --- a/pkg/dns/solver_local.go +++ b/pkg/dns/solver_local.go @@ -9,7 +9,6 @@ import ( "k8s.io/klog/v2" "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" - //"github.com/cert-manager/cert-manager/pkg/issuer/acme/dns/util" "github.com/stuurmcp/cert-manager-webhook-sthome/pkg/util" ) @@ -50,9 +49,10 @@ func (loc *LocalDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error if err != nil { return err } + updateWorkdir() // TODO: convert shell script to golang //localip := GetOutboundIP(Dnsserver_net) - success, err := Execute3( + success, err := Execute( Shell, // "-c", AcmeAuthCmd, @@ -81,7 +81,8 @@ func (loc *LocalDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error func (loc *LocalDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { //domainName := extractDomainName(ch.ResolvedZone) //localip := GetOutboundIP(Dnsserver_net) - success, err := Execute3( + updateWorkdir() + success, err := Execute( Shell, // "-c", AcmeAuthCmd, @@ -112,7 +113,8 @@ func (loc *LocalDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, sto return fmt.Errorf("failed to get kubernetes client: %w", err) } loc.client = cl - klog.InfoS("CZ: Successfully initialised kubernetes client!") + setupWorkdir() + klog.Infof("webhook \"%s\" started.", ProviderName) return nil } diff --git a/pkg/dns/utils.go b/pkg/dns/utils.go index 6f8ffa8..2aed93d 100644 --- a/pkg/dns/utils.go +++ b/pkg/dns/utils.go @@ -6,8 +6,18 @@ package dns import ( "encoding/json" "fmt" + "io" + "os" + "sync" + "time" extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/klog/v2" +) + +var ( + wg sync.WaitGroup // wg is used to wait for the program to finish. + mutex sync.Mutex // mutex is used to define a critical section of code. ) // loadConfig is a small helper function that decodes JSON configuration into @@ -25,6 +35,94 @@ func LoadConfig(cfgJSON *extapi.JSON) (LocalDNSProviderConfig, error) { return cfg, nil } +func setupWorkdir() { + if _, err := os.Stat(Workdir); os.IsNotExist(err) { + klog.Infof("Folder \"%s\" does not exist!. Creating it.", Workdir) + err = os.Mkdir(Workdir, 0755) + if err != nil { + panic(err) + } + } + copySrcDestDirFile(AcmeDir, Workdir, AuthScript, AuthScriptMode) + copySrcDestDirFile(AcmeDir, Workdir, DnsUpdScript, DnsUpdScriptMode) + copySrcDestDirFile(AcmeDir, Workdir, KrbConf, KrbConfMode) + copySrcDestDirFile(AcmeDir, Workdir, Keytab, KeytabMode) + AcmeAuthCmd = Workdir + "/" + AuthScript +} + +func updateWorkdir() { + wg.Add(4) // Add a count of two, one for each goroutine. + + go updateIfStale(AuthScript, AuthScriptMode) + go updateIfStale(DnsUpdScript, DnsUpdScriptMode) + go updateIfStale(KrbConf, KrbConfMode) + go updateIfStale(Keytab, KeytabMode) + + wg.Wait() // Wait for the goroutines to finish. +} + +func updateIfStale(filename string, mode os.FileMode) error { + defer wg.Done() // Schedule the call to Done to tell main we are done. + mutex.Lock() + sourceFile := AcmeDir + "/" + filename + destFile := Workdir + "/" + filename + result, err := cmpModTime(sourceFile, destFile) + if result > 0 { + klog.Infof("Updating \"%s\" from \"%s\" folder.", destFile, AcmeDir) + err = CopyFile(sourceFile, destFile, mode) + } + mutex.Unlock() + return err +} +func cmpModTime(file1 string, file2 string) (int, error) { + // Get the fileinfo + fileInfo, err := os.Stat(file1) + if err != nil { + klog.Fatal(err) + } + modtime1 := fileInfo.ModTime() + fileInfo, err = os.Stat(file2) + if err != nil { + klog.Fatal(err) + } + modtime2 := fileInfo.ModTime() + diff := modtime1.Sub(modtime2) + if diff < (time.Duration(0) * time.Second) { + return -1, nil + } + if diff > (time.Duration(0) * time.Second) { + return 1, nil + } + return 0, nil +} + +func copySrcDestDirFile(sourcedir string, destdir string, filename string, mode os.FileMode) error { + sourceFile := sourcedir + "/" + filename + destFile := destdir + "/" + filename + return CopyFile(sourceFile, destFile, mode) +} + +func CopyFile(sourceFile string, destFile string, mode os.FileMode) error { + source, err := os.Open(sourceFile) //open the source file + if err != nil { + panic(err) + } + defer source.Close() + + destination, err := os.Create(destFile) //create the destination file + if err != nil { + panic(err) + } + defer destination.Close() + _, err = io.Copy(destination, source) //copy the contents of source to destination file + if err != nil { + panic(err) + } + err = os.Chmod(destFile, mode) + klog.Infof("Copied %s to %s.", sourceFile, destFile) + return nil +} + /* // quote quotes the provide value func quote(value string) string { diff --git a/version.txt b/version.txt index be06281..dd2a000 100644 --- a/version.txt +++ b/version.txt @@ -1,3 +1,3 @@ -0.0.5-alpha.97 -20240418-0139 -97 \ No newline at end of file +0.0.5-alpha.106 +20240418-1937 +106 \ No newline at end of file