initial build
This commit is contained in:
parent
c0bb5199ee
commit
816d3c098c
64
.github/workflows/docker.build.yaml
vendored
Normal file
64
.github/workflows/docker.build.yaml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# - cron: '21 19 * * *'
|
||||
push:
|
||||
branches: [ master ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
#pull_request:
|
||||
# branches: [ master ]
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
#images: ${{ env.REGISTRY }}/newsbot.worker
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,7 @@
|
||||
.env
|
||||
|
||||
# Binaries for programs and plugins
|
||||
ddns
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM golang:1.19 as build
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN go build .
|
||||
|
||||
FROM alpine:latest as app
|
||||
|
||||
RUN apk --no-cache add bash libc6-compat && \
|
||||
mkdir /app
|
||||
|
||||
COPY --from=build /app/ddns /app
|
||||
|
||||
CMD [ "/app/ddns" ]
|
30
README.md
30
README.md
@ -1,2 +1,30 @@
|
||||
# cloudflare-ddns
|
||||
Golang based tool to maintain Cloudflare Dynamic DNS for your sites.
|
||||
|
||||
Golang based tool to maintain Cloudflare Dynamic DNS for your sites. Run this locally or as a Docker image.
|
||||
|
||||
When the application starts, it will run every 15 minutes and check your defined hosts and compare the IP Address listed. If they dont match, it will be updated to refect the public IP address of the server its running from.
|
||||
|
||||
This does not have any UI elements so you need to check the logs to se how its going.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/jtom38/cloudflare-ddns:master
|
||||
container_name: cfddns
|
||||
environment:
|
||||
EMAIL: "yourcloudflareemailaddress"
|
||||
API_TOKEN: "cloudflare-api-key"
|
||||
DOMAIN: "exampledomain.com"
|
||||
HOSTS: "example1,example2,www"
|
||||
|
||||
```
|
||||
|
||||
## Credit
|
||||
|
||||
The original post that gave me this idea can be found [here](https://adamtheautomator.com/cloudflare-dynamic-dns/). This was written in PowerShell and thought I could improve on it with Go and Docker.
|
248
cloudflare.go
Normal file
248
cloudflare.go
Normal file
@ -0,0 +1,248 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CloudFlareClient struct {
|
||||
apiToken string
|
||||
email string
|
||||
|
||||
httpClient http.Client
|
||||
}
|
||||
|
||||
func NewCloudFlareClient(ApiToken string, UserEmail string) *CloudFlareClient {
|
||||
c := CloudFlareClient{
|
||||
apiToken: ApiToken,
|
||||
email: UserEmail,
|
||||
}
|
||||
c.httpClient = http.Client{}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
type listDomainZones struct {
|
||||
Result []struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Paused bool `json:"paused,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DevelopmentMode int `json:"development_mode,omitempty"`
|
||||
NameServers []string `json:"name_servers,omitempty"`
|
||||
OriginalNameServers []string `json:"original_name_servers,omitempty"`
|
||||
OriginalRegistrar string `json:"original_registrar,omitempty"`
|
||||
OriginalDnshost interface{} `json:"original_dnshost,omitempty"`
|
||||
ModifiedOn time.Time `json:"modified_on,omitempty"`
|
||||
CreatedOn time.Time `json:"created_on,omitempty"`
|
||||
ActivatedOn time.Time `json:"activated_on,omitempty"`
|
||||
Meta struct {
|
||||
Step int `json:"step,omitempty"`
|
||||
CustomCertificateQuota int `json:"custom_certificate_quota,omitempty"`
|
||||
PageRuleQuota int `json:"page_rule_quota,omitempty"`
|
||||
PhishingDetected bool `json:"phishing_detected,omitempty"`
|
||||
MultipleRailgunsAllowed bool `json:"multiple_railguns_allowed,omitempty"`
|
||||
} `json:"meta,omitempty"`
|
||||
Owner struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
} `json:"owner,omitempty"`
|
||||
Account struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
} `json:"account,omitempty"`
|
||||
Tenant struct {
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
Name interface{} `json:"name,omitempty"`
|
||||
} `json:"tenant,omitempty"`
|
||||
TenantUnit struct {
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
} `json:"tenant_unit,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
Plan struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Price int `json:"price,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Frequency string `json:"frequency,omitempty"`
|
||||
IsSubscribed bool `json:"is_subscribed,omitempty"`
|
||||
CanSubscribe bool `json:"can_subscribe,omitempty"`
|
||||
LegacyID string `json:"legacy_id,omitempty"`
|
||||
LegacyDiscount bool `json:"legacy_discount,omitempty"`
|
||||
ExternallyManaged bool `json:"externally_managed,omitempty"`
|
||||
} `json:"plan,omitempty"`
|
||||
} `json:"result,omitempty"`
|
||||
ResultInfo struct {
|
||||
Page int `json:"page,omitempty"`
|
||||
PerPage int `json:"per_page,omitempty"`
|
||||
TotalPages int `json:"total_pages,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
TotalCount int `json:"total_count,omitempty"`
|
||||
} `json:"result_info,omitempty"`
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []interface{} `json:"errors,omitempty"`
|
||||
Messages []interface{} `json:"messages,omitempty"`
|
||||
}
|
||||
|
||||
// Lists out all the zones bound to an ac
|
||||
//
|
||||
// https://api.cloudflare.com/#zone-list-zones
|
||||
func (c *CloudFlareClient) GetDomainByName(domain string) (*listDomainZones, error) {
|
||||
var items listDomainZones
|
||||
uri := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%v", domain)
|
||||
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return &items, err
|
||||
}
|
||||
req.Header.Set("X-Auth-Email", c.email)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", c.apiToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return &items, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return &items, ErrInvalidStatusCode
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &items, ErrWasNotJson
|
||||
}
|
||||
//log.Print(string(body))
|
||||
err = json.Unmarshal(body, &items)
|
||||
if err != nil {
|
||||
return &items, ErrFailedToDecodeJson
|
||||
}
|
||||
|
||||
if !items.Success {
|
||||
log.Println("Failed to find the requested domain on Cloudflare.")
|
||||
return &items, ErrDomainNotFound
|
||||
}
|
||||
|
||||
return &items, nil
|
||||
}
|
||||
|
||||
type DnsDetails struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []interface{} `json:"errors"`
|
||||
Messages []interface{} `json:"messages"`
|
||||
Result []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Proxiable bool `json:"proxiable"`
|
||||
Proxied bool `json:"proxied"`
|
||||
TTL int `json:"ttl"`
|
||||
Locked bool `json:"locked"`
|
||||
ZoneID string `json:"zone_id"`
|
||||
ZoneName string `json:"zone_name"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
ModifiedOn time.Time `json:"modified_on"`
|
||||
Data struct {
|
||||
} `json:"data"`
|
||||
Meta struct {
|
||||
AutoAdded bool `json:"auto_added"`
|
||||
Source string `json:"source"`
|
||||
} `json:"meta"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func (c *CloudFlareClient) GetDnsEntriesByDomain(DomainId string, Host string, Domain string) (*DnsDetails, error) {
|
||||
var items DnsDetails
|
||||
name := fmt.Sprintf("%v.%v", Host, Domain)
|
||||
uri := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%v/dns_records?name=%v", DomainId, name)
|
||||
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return &items, err
|
||||
}
|
||||
req.Header.Set("X-Auth-Email", c.email)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", c.apiToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return &items, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return &items, ErrInvalidStatusCode
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &items, ErrWasNotJson
|
||||
}
|
||||
err = json.Unmarshal(body, &items)
|
||||
if err != nil {
|
||||
return &items, ErrFailedToDecodeJson
|
||||
}
|
||||
|
||||
if !items.Success {
|
||||
log.Println("Failed to find the requested domain on Cloudflare.")
|
||||
return &items, ErrDomainNotFound
|
||||
}
|
||||
|
||||
return &items, nil
|
||||
}
|
||||
|
||||
type dnsUpdate struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Ttl int `json:"ttl"`
|
||||
Proxied bool `json:"proxied"`
|
||||
}
|
||||
|
||||
func (c *CloudFlareClient) UpdateDnsEntry(DomainId string, DnsDetails *DnsDetails, NewIpAddress string) error {
|
||||
param := dnsUpdate{
|
||||
Type: DnsDetails.Result[0].Type,
|
||||
Name: DnsDetails.Result[0].Name,
|
||||
Content: NewIpAddress,
|
||||
Ttl: DnsDetails.Result[0].TTL,
|
||||
Proxied: DnsDetails.Result[0].Proxied,
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%v/dns_records/%v", DomainId, DnsDetails.Result[0].ID)
|
||||
|
||||
body, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", endpoint, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Auth-Email", c.email)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", c.apiToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Println(resp.Status)
|
||||
return errors.New("failed to update the IP address")
|
||||
}
|
||||
|
||||
//log.Print(resp)
|
||||
return nil
|
||||
}
|
72
config.go
Normal file
72
config.go
Normal file
@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigEmail = "EMAIL"
|
||||
ConfigToken = "API_TOKEN"
|
||||
ConfigDomain = "DOMAIN"
|
||||
ConfigHosts = "HOSTS"
|
||||
)
|
||||
|
||||
type ConfigClient struct{}
|
||||
|
||||
func NewConfigClient() ConfigClient {
|
||||
c := ConfigClient{}
|
||||
c.RefreshEnv()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (cc *ConfigClient) GetConfig(key string) string {
|
||||
res, filled := os.LookupEnv(key)
|
||||
if !filled {
|
||||
log.Printf("Missing the a value for '%v'. Could generate errors.", key)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (cc *ConfigClient) GetFeature(flag string) (bool, error) {
|
||||
cc.RefreshEnv()
|
||||
|
||||
res, filled := os.LookupEnv(flag)
|
||||
if !filled {
|
||||
errorMessage := fmt.Sprintf("'%v' was not found", flag)
|
||||
return false, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(res)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Use this when your ConfigClient has been opened for awhile and you want to ensure you have the most recent env changes.
|
||||
func (cc *ConfigClient) RefreshEnv() {
|
||||
// Check to see if we have the env file on the system
|
||||
_, err := os.Stat(".env")
|
||||
|
||||
// We have the file, load it.
|
||||
if err == nil {
|
||||
_, err := os.Open(".env")
|
||||
if err == nil {
|
||||
loadEnvFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadEnvFile() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
59
cron.go
Normal file
59
cron.go
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type cronClient struct{
|
||||
scheduler *cron.Cron
|
||||
}
|
||||
|
||||
func NewCron() cronClient {
|
||||
c := cronClient{
|
||||
scheduler: cron.New(),
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c cronClient) RunCloudflareCheck(ApiToken string, Email string, Domain string, Hosts []string) {
|
||||
log.Println("Starting check...")
|
||||
log.Println("Checking the current IP Address")
|
||||
currentIp, err := GetCurrentIpAddress()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
cf := NewCloudFlareClient(ApiToken, Email)
|
||||
log.Println("Checking domain information on CloudFlare")
|
||||
domainDetails, err := cf.GetDomainByName(Domain)
|
||||
if err != nil {
|
||||
log.Println("Unable to get information from CloudFlare.")
|
||||
log.Println("Double check the API Token to make sure it's valid.")
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, host := range Hosts {
|
||||
hostname := fmt.Sprintf("%v.%v", host, Domain)
|
||||
log.Printf("Reviewing '%v'", hostname)
|
||||
dns, err := cf.GetDnsEntriesByDomain(domainDetails.Result[0].ID, host, Domain)
|
||||
if err != nil {
|
||||
log.Println("failed to collect dns entry")
|
||||
return
|
||||
}
|
||||
|
||||
if dns.Result[0].Content != currentIp {
|
||||
log.Println("IP Address no longer matches, sending an update")
|
||||
err = cf.UpdateDnsEntry(domainDetails.Result[0].ID, dns, currentIp)
|
||||
if err != nil {
|
||||
log.Println("Failed to update the DNS record!")
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Println("Done!")
|
||||
}
|
11
docker-compose.yaml
Normal file
11
docker-compose.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/jtom38/cloudflare-ddns:master
|
||||
container_name: cfddns
|
||||
environment:
|
||||
EMAIL: "yourcloudflareemailaddress"
|
||||
API_TOKEN: "cloudflare-api-key"
|
||||
DOMAIN: "exampledomain.com"
|
||||
HOSTS: "example1,example2,www"
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module github.com/jtom38/cloudflare/ddns
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
77
main.go
Normal file
77
main.go
Normal file
@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidStatusCode error = errors.New("did not get an acceptiable status code from the server")
|
||||
ErrFailedToDecodeBody error = errors.New("unable to decode the body")
|
||||
ErrFailedToDecodeJson error = errors.New("unexpected json format was returned")
|
||||
ErrWasNotJson error = errors.New("response from server was not json")
|
||||
ErrDomainNotFound error = errors.New("unable to find requested domain on cloudflare")
|
||||
)
|
||||
|
||||
func GetCurrentIpAddress() (string, error) {
|
||||
resp, err := http.Get("https://v4.ident.me")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", ErrInvalidStatusCode
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", ErrFailedToDecodeBody
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := NewConfigClient()
|
||||
email := config.GetConfig(ConfigEmail)
|
||||
if email == "" {
|
||||
log.Println("Unable to find 'EMAIL' env value.")
|
||||
return
|
||||
}
|
||||
|
||||
token := config.GetConfig(ConfigToken)
|
||||
if token == "" {
|
||||
log.Println("Unable to find 'API_TOKEN' env value.")
|
||||
}
|
||||
|
||||
domain := config.GetConfig(ConfigDomain)
|
||||
if token == "" {
|
||||
log.Println("Unable to find 'DOMAIN' env value.")
|
||||
}
|
||||
|
||||
hosts := config.GetConfig(ConfigHosts)
|
||||
if token == "" {
|
||||
log.Println("Unable to find 'HOSTS' env value.")
|
||||
}
|
||||
hostsArray := strings.Split(hosts, ",")
|
||||
log.Println("Env Check: OK")
|
||||
|
||||
cron := NewCron()
|
||||
log.Println("Cloudflare Check will run every 15 minutes.")
|
||||
cron.scheduler.AddFunc("0,15,30,45 * * * *", func() {
|
||||
cron.RunCloudflareCheck(token, email, domain, hostsArray)
|
||||
})
|
||||
cron.scheduler.Start()
|
||||
|
||||
log.Println("Application has started!")
|
||||
for {
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user