Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cicd, readme #1

Merged
merged 9 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions .github/workflows/_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,51 @@ jobs:
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false

- name: Download dependencies
run: go mod download -x

- name: Run vet
run: go vet ./...

- name: Run tests
run: go test ./...

- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout 1m --verbose

- name: Run tests
run: go test ./...
publish:
needs: build
if: inputs.publish
runs-on: self-hosted

- name: Run vet
run: go vet ./...
steps:
- name: Checkout
uses: actions/checkout@v4
with:
show-progress: false

- uses: axatol/actions/assume-aws-role@release
with:
aws-region: us-east-1
role-to-assume: ${{ secrets.AWS_ECR_IMAGE_PUBLISHER_ROLE_ARN }}

- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: true

- name: Build
run: make image
run: make build-image IMAGE_TAG=${{ inputs.image-tag }}

- name: Publish
if: inputs.publish
run: docker push axatol/external-dns-cloudflare-tunnel-webhook:${{ inputs.image-tag }}
run: make publish-image IMAGE_TAG=${{ inputs.image-tag }}

- name: Prune ECR
uses: axatol/actions/prune-ecr-repository@release
with:
repository-name: external-dns-cloudflare-tunnel-webhook
5 changes: 4 additions & 1 deletion .github/workflows/on-push-master.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
name: On push to master

on:
pull_request:
push:
branches:
- master
paths-ignore:
- "**/*.md"
- ".github/**"

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ COPY . .
ARG BUILD_COMMIT=unknown
ARG BUILD_TIME=unknown
RUN --mount=type=cache,target=/root/.cache/go-build \
make build BUILD_COMMIT=${BUILD_COMMIT} BUILD_TIME=${BUILD_TIME} OUTPUT=/go/bin/app
make build-binary BUILD_COMMIT=${BUILD_COMMIT} BUILD_TIME=${BUILD_TIME} OUTPUT=/go/bin/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /go/bin/app /app
Expand Down
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ OUTPUT ?= dist/app
BUILD_TIME ?= $(shell date +"%Y-%m-%dT%H:%M:%S%z")
BUILD_COMMIT ?= $(shell git rev-parse HEAD)
IMAGE_PLATFORM ?= linux/amd64
IMAGE_NAME ?= axatol/external-dns-cloudflare-tunnel-webhook
IMAGE_NAME ?= public.ecr.aws/axatol/external-dns-cloudflare-tunnel-webhook
IMAGE_TAG ?= latest

LDFLAGS = -s -w
LDFLAGS += -X main.buildTime=$(BUILD_TIME)
LDFLAGS += -X main.buildCommit=$(BUILD_COMMIT)

build:
build-binary:
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(OUTPUT)

image:
build-image:
docker build \
--tag $(IMAGE_NAME):$(IMAGE_TAG) \
--platform $(IMAGE_PLATFORM) \
--build-arg BUILD_COMMIT=$(BUILD_COMMIT) \
--build-arg BUILD_TIME=$(BUILD_TIME) \
.

publish-image:
docker push $(IMAGE_NAME):$(IMAGE_TAG)
85 changes: 84 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,84 @@
# external-dns-cloudflare-tunnel-webhook
# external-dns-cloudflare-tunnel-webhook

> [!WARNING]
> This provider is experimental

This is a provider for use with [external-dns](https://github.com/kubernetes-sigs/external-dns) via the webhook mechanism. It provides the ability to create public hostnames and backing DNS records for Cloudflare Tunnels.

> [!NOTE]
> Due to limitations of the external-dns webhook mechanism and my lack of brainpower, this provider only supports backing a single tunnel. To support more tunnels, deploy more instances of this provider.

## Deploying

You will need:

- A Kubernetes cluster
- Helm CLI installed
- A Cloudflare account with some form of authorization with scopes
- All accounts - Cloudflare Tunnel:Edit
- All zones - DNS:Edit

Ensure you have a secret with your Cloudflare credentials.

```shell
kubectl create secret generic cloudflare-credentials --from-literal=CLOUDFLARE_API_TOKEN=blah
```

Create a values file, see below for a minimum config.

```shell
cat <<EOF > ./values.yaml
logLevel: info
logFormat: json
interval: 1h
provider:
name: webhook
webhook:
image:
repository: docker.io/axatol/external-dns-cloudflare-tunnel-webhook
tag: latest
env:
- name: CLOUDFLARE_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-credentials
key: CLOUDFLARE_API_TOKEN
EOF
```

Install the external-dns chart.

```shell
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update
helm upgrade external-dns-cloudflare-tunnel external-dns/external-dns \
--install \
--atomic \
--create-namespace \
--namespace external-dns \
--values ./values.yaml
```

## Configuration

| Environment variable | Type | Default | Notes |
| ----------------------- | --------------- | ------------------ | ----- |
| `LOG_LEVEL` | `string` | `"info"` | |
| `LOG_FORMAT` | `string` | `"json"` | |
| `CLOUDFLARE_API_KEY` | `string` | `""` | ^1 |
| `CLOUDFLARE_API_EMAIL` | `string` | `""` | ^1 |
| `CLOUDFLARE_API_TOKEN` | `string` | `""` | ^1 |
| `CLOUDFLARE_ACCOUNT_ID` | `string` | | ^2 |
| `CLOUDFLARE_TUNNEL_ID` | `string` | | ^2 |
| `CLOUDFLARE_SYNC_DNS` | `bool` | `"false"` | |
| `PORT` | `int64` | `"8888"` | |
| `READ_TIMEOUT` | `time.Duration` | `"5s"` | |
| `WRITE_TIMEOUT` | `time.Duration` | `"10s"` | |
| `DRY_RUN` | `bool` | `"false"` | |
| `DOMAIN_FILTER` | `[]string` | `"" delimiter:","` | ^3 |

1. Must specify:
- _both_ `CLOUDFLARE_API_KEY` and `CLOUDFLARE_API_EMAIL`
- _or_ `CLOUDFLARE_API_TOKEN`
2. Required field
3. Specify multiple by delimiting with `,`
2 changes: 1 addition & 1 deletion pkg/provider/records.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func ApplyChanges(ctx context.Context, cf cf.Cloudflare, changes []Change) error
Type: endpoint.RecordTypeCNAME,
TTL: 1,
Proxied: cloudflare.BoolPtr(true),
Comment: fmt.Sprintf("external-dns-cloudflare-tunnel-webhook/%s", change.Service),
Comment: change.Service,
}

switch change.Action {
Expand Down
35 changes: 12 additions & 23 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,13 @@ func handleNegotiation(p provider.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
raw, err := json.Marshal(p.GetDomainFilter())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
write(w, http.StatusInternalServerError, nil)
return
}

log.Debug().Str("action", "handleNegotiation").RawJSON("domain_filter", raw).Send()
w.Header().Set(contentTypeHeader, externalDNSMediaType)
w.WriteHeader(http.StatusOK)
w.Write(raw)
write(w, http.StatusOK, raw)
}
}

Expand All @@ -61,23 +59,20 @@ func handleGetRecords(p provider.Provider) http.HandlerFunc {
records, err := p.Records(r.Context())
if err != nil {
log.Error().Err(err).Msg("failed to get records")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
write(w, http.StatusInternalServerError, nil)
return
}

raw, err := json.Marshal(records)
if err != nil {
log.Error().Err(err).Msg("failed to marshal records to json")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
write(w, http.StatusInternalServerError, nil)
return
}

log.Debug().RawJSON("records", raw).Send()
w.Header().Set(contentTypeHeader, externalDNSMediaType)
w.WriteHeader(http.StatusOK)
w.Write(raw)
write(w, http.StatusOK, raw)
}
}

Expand All @@ -89,20 +84,18 @@ func handleApplyChanges(p provider.Provider) http.HandlerFunc {
if err := json.NewDecoder(r.Body).Decode(&changes); err != nil {
log.Error().Err(err).Msg("failed to decode changes")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(http.StatusText(http.StatusBadRequest)))
write(w, http.StatusBadRequest, nil)
return
}

if err := p.ApplyChanges(r.Context(), &changes); err != nil {
log.Error().Err(err).Any("changes", changes).Msg("failed to apply changes")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
write(w, http.StatusInternalServerError, nil)
return
}

log.Debug().Any("changes", changes).Send()
w.WriteHeader(http.StatusNoContent)
w.Write([]byte(http.StatusText(http.StatusNoContent)))
write(w, http.StatusNoContent, nil)
}
}

Expand All @@ -113,30 +106,26 @@ func handleAdjustEndpoints(p provider.Provider) http.HandlerFunc {
var endpoints []*endpoint.Endpoint
if err := json.NewDecoder(r.Body).Decode(&endpoints); err != nil {
log.Error().Err(err).Msg("failed to decode endpoints")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(http.StatusText(http.StatusBadRequest)))
write(w, http.StatusBadRequest, nil)
return
}

endpoints, err := p.AdjustEndpoints(endpoints)
if err != nil {
log.Error().Err(err).Any("endpoints", endpoints).Msg("failed to adjust endpoints")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
write(w, http.StatusInternalServerError, nil)
return
}

raw, err := json.Marshal(endpoints)
if err != nil {
log.Error().Err(err).Any("endpoints", endpoints).Msg("failed to marshal endpoints to json")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
write(w, http.StatusInternalServerError, nil)
return
}

log.Debug().Any("endpoints", endpoints).Send()
w.Header().Set(contentTypeHeader, externalDNSMediaType)
w.WriteHeader(http.StatusOK)
w.Write(raw)
write(w, http.StatusOK, raw)
}
}
21 changes: 21 additions & 0 deletions pkg/server/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package server

import (
"fmt"
"net/http"

"github.com/rs/zerolog/log"
)

func write(w http.ResponseWriter, status int, body []byte) {
w.WriteHeader(status)

raw := []byte(http.StatusText(status))
if body != nil {
raw = body
}

if _, err := w.Write(raw); err != nil {
log.Error().Err(fmt.Errorf("failed to write response: %s", err)).Send()
}
}
Loading