diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 90ad48a..70df6fa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,6 +34,7 @@ jobs: permissions: contents: read checks: write + packages: write steps: - uses: actions/checkout@v4.2.1 - uses: actions/setup-go@v5.0.2 @@ -59,6 +60,13 @@ jobs: name: conformance-report path: '*-report.yaml' + - uses: docker/login-action@v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - run: make docker-push + release-please: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index c606008..afae3bb 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,8 @@ CONTAINER_TOOL ?= docker SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec +E2E_TIMEOUT ?= 5m + .PHONY: all all: build @@ -68,7 +70,7 @@ test: manifests generate fmt vet envtest ## Run tests. # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. test-e2e: - go test ./test/e2e/ -v -ginkgo.v -timeout 5m + go test ./test/e2e/ -v -ginkgo.v -timeout $(E2E_TIMEOUT) .PHONY: lint lint: golangci-lint ## Run golangci-lint linter diff --git a/README.md b/README.md index 7a79c57..dffb674 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ Manage Kubernetes ingress traffic with Cloudflare Tunnels via the [Gateway API]( 1. Install v1 or later of the Gateway API CRDs: `kubectl apply -k github.com/kubernetes-sigs/gateway-api//config/crd?ref=v1.0.0` 2. Install cloudflare-kubernetes-gateway: `kubectl apply -k github.com/pl4nty/cloudflare-kubernetes-gateway//config/default?ref=v0.6.0` 3. [Find your Cloudflare account ID](https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/) -3. [Create a Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with the Account Cloudflare Tunnel Edit and Zone DNS Edit permissions -4. Use them to create a Secret: `kubectl create secret -n cloudflare-gateway generic cloudflare --from-literal=ACCOUNT_ID=your-account-id --from-literal=TOKEN=your-token` -5. Create a file containing your GatewayClass, then apply it with `kubectl apply -f file.yaml`: +4. [Create a Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with the Account Cloudflare Tunnel Edit and Zone DNS Edit permissions +5. Use them to create a Secret: `kubectl create secret -n cloudflare-gateway generic cloudflare --from-literal=ACCOUNT_ID=your-account-id --from-literal=TOKEN=your-token` +6. Create a file containing your GatewayClass, then apply it with `kubectl apply -f file.yaml`: + ```yaml apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass @@ -23,6 +24,7 @@ spec: namespace: cloudflare-gateway name: cloudflare ``` + 7. [Create Gateways and HTTPRoutes](https://gateway-api.sigs.k8s.io/guides/http-routing/) to start managing traffic! For example: ```yaml @@ -55,6 +57,8 @@ spec: port: 80 ``` +8. (optional) Install Prometheus ServiceMonitors to collect controller and cloudflared metrics: `kubectl apply -k github.com/pl4nty/cloudflare-kubernetes-gateway//config/prometheus?ref=v0.6.0` + ## Features The v1 Core spec is not yet supported, as some features (eg header-based routing) aren't available with Tunnels. The following features are supported: diff --git a/config/default/image_metrics_service.yaml b/config/default/image_metrics_service.yaml new file mode 100644 index 0000000..14c98df --- /dev/null +++ b/config/default/image_metrics_service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: image + app.kubernetes.io/name: cloudflare-kubernetes-gateway + app.kubernetes.io/managed-by: kustomize + name: image-metrics-service + namespace: system +spec: + ports: + - name: http + port: 2000 + protocol: TCP + targetPort: 2000 + selector: + app.kubernetes.io/name: cloudflare-kubernetes-gateway + app.kubernetes.io/managed-by: GatewayController diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index f6efdac..9736f77 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -25,16 +25,17 @@ resources: # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] To enable the controller manager metrics service, uncomment the following line. -#- metrics_service.yaml +- metrics_service.yaml +- image_metrics_service.yaml # Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager -#patches: +patches: # [METRICS] The following patch will enable the metrics endpoint. Ensure that you also protect this endpoint. # More info: https://book.kubebuilder.io/reference/metrics # If you want to expose the metric endpoint of your controller-manager uncomment the following line. -#- path: manager_metrics_patch.yaml -# target: -# kind: Deployment +- path: manager_metrics_patch.yaml + target: + kind: Deployment # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml diff --git a/config/prometheus/image_monitor.yaml b/config/prometheus/image_monitor.yaml new file mode 100644 index 0000000..037da72 --- /dev/null +++ b/config/prometheus/image_monitor.yaml @@ -0,0 +1,18 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: image + app.kubernetes.io/name: cloudflare-kubernetes-gateway + app.kubernetes.io/managed-by: kustomize + name: image-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: http # Ensure this is the name of the port that exposes HTTP metrics + scheme: http + selector: + matchLabels: + control-plane: image diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml index ed13716..c7122a2 100644 --- a/config/prometheus/kustomization.yaml +++ b/config/prometheus/kustomization.yaml @@ -1,2 +1,3 @@ resources: - monitor.yaml +- image_monitor.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index eb9df97..5f5465d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -12,6 +12,7 @@ rules: - create - get - list + - update - watch - apiGroups: - "" diff --git a/internal/controller/gateway_controller.go b/internal/controller/gateway_controller.go index a376f65..7d2c014 100644 --- a/internal/controller/gateway_controller.go +++ b/internal/controller/gateway_controller.go @@ -50,7 +50,7 @@ type GatewayReconciler struct { // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;update;watch // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/finalizers,verbs=update // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/status,verbs=update -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;get;list;watch +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;get;list;update;watch // +kubebuilder:rbac:groups=core,resources=events,verbs=create // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get @@ -412,6 +412,34 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct log.Error(err, "Failed to get Deployment") // Let's return the error for the reconciliation be re-trigged again return ctrl.Result{}, err + } else { + // Define a new deployment + dep, err := r.deploymentForGateway(gateway, token) + if err != nil { + log.Error(err, "Failed to define new Deployment resource for Gateway") + + // The following implementation will update the status + meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionAccepted), + Status: metav1.ConditionFalse, Reason: "Reconciling", ObservedGeneration: gateway.Generation, + Message: fmt.Sprintf("Failed to update Deployment for the custom resource (%s): (%s)", gateway.Name, err)}) + + if err := r.Status().Update(ctx, gateway); err != nil { + log.Error(err, "Failed to update Gateway status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, err + } + + if err := r.Update(ctx, dep); err != nil { + if strings.Contains(err.Error(), "apply your changes to the latest version and try again") { + log.Info("Conflict when updating Deployment, retrying") + return ctrl.Result{Requeue: true}, nil + } else { + log.Error(err, "Failed to update Deployment") + return ctrl.Result{}, err + } + } } if err := r.Get(ctx, req.NamespacedName, gateway); err != nil { diff --git a/internal/controller/gatewayclass_controller.go b/internal/controller/gatewayclass_controller.go index a500a74..8b8347b 100644 --- a/internal/controller/gatewayclass_controller.go +++ b/internal/controller/gatewayclass_controller.go @@ -2,6 +2,7 @@ package controller import ( "context" + "fmt" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -56,22 +57,28 @@ func (r *GatewayClassReconciler) Reconcile(ctx context.Context, req ctrl.Request } // validate parameters - var ok bool + msg := "" _, api, err := InitCloudflareApi(ctx, r.Client, gatewayClass.Name) if err == nil { token, err := api.User.Tokens.Verify(ctx) if err == nil { - ok = token.Status == "active" + if token.Status != "active" { + msg = fmt.Sprintf("Token status is %s, is not active. Please check the Cloudflare dashboard", token.Status) + } + } else { + msg = err.Error() + " Ensure ACCOUNT_ID and TOKEN are valid" } + } else { + msg = err.Error() + " Ensure ACCOUNT_ID and TOKEN are set" } var condition metav1.Condition - if !ok { + if msg != "" { condition = metav1.Condition{ Type: string(gatewayv1.GatewayClassConditionStatusAccepted), Status: metav1.ConditionFalse, Reason: string(gatewayv1.GatewayClassReasonInvalidParameters), - Message: "Unable to initialize Cloudflare API from secret in GatewayClass parameterRef. Ensure ACCOUNT_ID and TOKEN are set", + Message: "Unable to initialize Cloudflare API. " + msg, ObservedGeneration: gatewayClass.Generation, } } else {