diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index df545064d..c7f6e19a1 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- -FROM golang:1.24.3 +FROM golang:1.24.7 # Avoid warnings by switching to noninteractive ENV DEBIAN_FRONTEND=noninteractive diff --git a/.github/workflows/build_canary.yml b/.github/workflows/build_canary.yml index c923669a3..6211f79c2 100644 --- a/.github/workflows/build_canary.yml +++ b/.github/workflows/build_canary.yml @@ -16,7 +16,7 @@ jobs: packages: write id-token: write # needed for signing the images with GitHub OIDC Token **not production ready** - container: ghcr.io/kedacore/keda-tools:1.24.3 + container: ghcr.io/kedacore/keda-tools:1.24.7 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index b5c43a7f6..c069c6a15 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -12,7 +12,7 @@ jobs: packages: write id-token: write # needed for signing the images with GitHub OIDC Token **not production ready** - container: ghcr.io/kedacore/keda-tools:1.24.3 + container: ghcr.io/kedacore/keda-tools:1.24.7 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 15746d8ca..d6875faeb 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -19,14 +19,14 @@ jobs: strategy: fail-fast: false matrix: - kubernetesVersion: [v1.32, v1.31, v1.30] + kubernetesVersion: [v1.33, v1.32, v1.31] include: + - kubernetesVersion: v1.33 + kindImage: kindest/node:v1.33.4@sha256:25a6018e48dfcaee478f4a59af81157a437f15e6e140bf103f85a2e7cd0cbbf2 - kubernetesVersion: v1.32 - kindImage: kindest/node:v1.32.0@sha256:c48c62eac5da28cdadcf560d1d8616cfa6783b58f0d94cf63ad1bf49600cb027 + kindImage: kindest/node:v1.32.8@sha256:abd489f042d2b644e2d033f5c2d900bc707798d075e8186cb65e3f1367a9d5a1 - kubernetesVersion: v1.31 - kindImage: kindest/node:v1.31.4@sha256:2cb39f7295fe7eafee0842b1052a599a4fb0f8bcf3f83d96c7f4864c357c6c30 - - kubernetesVersion: v1.30 - kindImage: kindest/node:v1.30.8@sha256:17cd608b3971338d9180b00776cb766c50d0a0b6b904ab4ff52fd3fc5c6369bf + kindImage: kindest/node:v1.31.12@sha256:0f5cc49c5e73c0c2bb6e2df56e7df189240d83cf94edfa30946482eb08ec57d2 steps: - name: Install prerequisites run: | @@ -49,6 +49,7 @@ jobs: with: node_image: ${{ matrix.kindImage }} cluster_name: cluster + version: v0.30.0 - name: Generate images and push to the cluster run: | @@ -96,14 +97,14 @@ jobs: strategy: fail-fast: false matrix: - kubernetesVersion: [v1.32, v1.31, v1.30] + kubernetesVersion: [v1.33, v1.32, v1.31] include: + - kubernetesVersion: v1.33 + kindImage: kindest/node:v1.33.4@sha256:25a6018e48dfcaee478f4a59af81157a437f15e6e140bf103f85a2e7cd0cbbf2 - kubernetesVersion: v1.32 - kindImage: kindest/node:v1.32.0@sha256:c48c62eac5da28cdadcf560d1d8616cfa6783b58f0d94cf63ad1bf49600cb027 + kindImage: kindest/node:v1.32.8@sha256:abd489f042d2b644e2d033f5c2d900bc707798d075e8186cb65e3f1367a9d5a1 - kubernetesVersion: v1.31 - kindImage: kindest/node:v1.31.4@sha256:2cb39f7295fe7eafee0842b1052a599a4fb0f8bcf3f83d96c7f4864c357c6c30 - - kubernetesVersion: v1.30 - kindImage: kindest/node:v1.30.8@sha256:17cd608b3971338d9180b00776cb766c50d0a0b6b904ab4ff52fd3fc5c6369bf + kindImage: kindest/node:v1.31.12@sha256:0f5cc49c5e73c0c2bb6e2df56e7df189240d83cf94edfa30946482eb08ec57d2 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 @@ -119,6 +120,7 @@ jobs: with: node_image: ${{ matrix.kindImage }} cluster_name: ${{ runner.name }} + version: v0.30.0 - name: Push images to the cluster run: | diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index 47a055ffe..ae289ee68 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -13,7 +13,7 @@ permissions: jobs: build_scaler: runs-on: ubuntu-latest - container: ghcr.io/kedacore/keda-tools:1.24.3 + container: ghcr.io/kedacore/keda-tools:1.24.7 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 - name: Register workspace path @@ -25,7 +25,7 @@ jobs: build_operator: runs-on: ubuntu-latest - container: ghcr.io/kedacore/keda-tools:1.24.3 + container: ghcr.io/kedacore/keda-tools:1.24.7 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 - name: Register workspace path @@ -37,7 +37,7 @@ jobs: build_interceptor: runs-on: ubuntu-latest - container: ghcr.io/kedacore/keda-tools:1.24.3 + container: ghcr.io/kedacore/keda-tools:1.24.7 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 - name: Register workspace path diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a72a663ff..a1744294e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,7 +16,7 @@ jobs: validate: name: validate - ${{ matrix.name }} runs-on: ${{ matrix.runner }} - container: ghcr.io/kedacore/keda-tools:1.24.3 + container: ghcr.io/kedacore/keda-tools:1.24.7 strategy: matrix: include: diff --git a/README.md b/README.md index 07e25430a..f40417aeb 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,4 @@ We are a Cloud Native Computing Foundation (CNCF) graduated project. ## Code of Conduct Please refer to the organization-wide [Code of Conduct document](https://github.com/kedacore/.github/blob/main/CODE_OF_CONDUCT.md). +# Force push marker - will be removed diff --git a/docs/install.md b/docs/install.md index 4bded79d7..cc8f855ba 100644 --- a/docs/install.md +++ b/docs/install.md @@ -75,8 +75,9 @@ helm upgrade kedahttp ./charts/keda-add-ons-http \ | HTTP Add-On version | KEDA version | Kubernetes version | |---------------------|-------------------|--------------------| -| main | v2.16 | v1.30 - v1.32 | -| 0.10.0 | v2.16 | v1.30 - v1.32 | +| main | v2.17 | v1.31 - v1.33 | +| 0.11.0 | v2.17 | v1.31 - v1.33 | +| 0.10.0 | v2.16 | v1.20 - v1.32 | | 0.9.0 | v2.16 | v1.29 - v1.31 | | 0.8.0 | v2.14 | v1.27 - v1.29 | | 0.7.0 | v2.13 | v1.27 - v1.29 | diff --git a/docs/operate.md b/docs/operate.md index a05d0a781..8085a1904 100644 --- a/docs/operate.md +++ b/docs/operate.md @@ -18,6 +18,26 @@ The OTEL exporter can be enabled by setting the `OTEL_EXPORTER_OTLP_METRICS_ENAB If you need to provide any headers such as authentication details in order to utilise your OTEL collector you can add them into the `OTEL_EXPORTER_OTLP_HEADERS` environment variable. The frequency at which the metrics are exported can be configured by setting `OTEL_METRIC_EXPORT_INTERVAL` to the number of seconds you require between each export interval (`30` by default). +# Configuring DNS for the KEDA HTTP Add-on interceptor proxy + +By default, the interceptor proxy uses short DNS names (e.g., `service.namespace`) when routing requests to backend services. In some cluster configurations, you may need to use fully qualified domain names (FQDN) instead. + +You can configure the cluster domain suffix by setting the `KEDA_HTTP_CLUSTER_DOMAIN` environment variable on the interceptor deployment. When set, this value will be appended to the short DNS name to form an FQDN. + +Examples: +- **Default behavior** (empty/not set): Routes to `service.namespace:port` +- **Standard Kubernetes cluster** (`svc.cluster.local`): Routes to `service.namespace.svc.cluster.local:port` +- **Custom cluster domain** (`svc.my-domain.com`): Routes to `service.namespace.svc.my-domain.com:port` + +```yaml +# Example interceptor deployment configuration +env: + - name: KEDA_HTTP_CLUSTER_DOMAIN + value: "svc.cluster.local" +``` + +> **Note:** According to the [Kubernetes DNS specification](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/), both short names (`service.namespace`) and FQDNs (`service.namespace.svc.cluster.local`) should resolve identically in most Kubernetes clusters. However, certain DNS policies, network policies, or custom DNS configurations may require the use of FQDNs. If you're experiencing DNS resolution issues, check your cluster's `dnsPolicy` and `NetworkPolicy` settings. + # Configuring TLS for the KEDA HTTP Add-on interceptor proxy The interceptor proxy has the ability to run both a HTTP and HTTPS server simultaneously to allow you to scale workloads that use either protocol. By default, the interceptor proxy will only serve over HTTP, but this behavior can be changed by configuring the appropriate environment variables on the deployment. diff --git a/go.mod b/go.mod index 8ea77a881..c5a27f43e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kedacore/http-add-on -go 1.24.3 +go 1.24.7 require ( github.com/go-logr/logr v1.4.3 diff --git a/interceptor/Dockerfile b/interceptor/Dockerfile index 9c6f66c7e..10c73df89 100644 --- a/interceptor/Dockerfile +++ b/interceptor/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.24.3 as builder +FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.24.7 as builder WORKDIR /workspace COPY go.* . RUN go mod download diff --git a/interceptor/config/serving.go b/interceptor/config/serving.go index 3dfb0c540..0b10d13b5 100644 --- a/interceptor/config/serving.go +++ b/interceptor/config/serving.go @@ -49,6 +49,10 @@ type Serving struct { TLSPort int `envconfig:"KEDA_HTTP_PROXY_TLS_PORT" default:"8443"` // ProfilingAddr if not empty, pprof will be available on this address, assuming host:port here ProfilingAddr string `envconfig:"PROFILING_BIND_ADDRESS" default:""` + // ClusterDomain is the Kubernetes cluster domain (e.g., "svc.cluster.local") + // If empty, uses short form DNS (service.namespace) + // If set, uses FQDN (service.namespace.ClusterDomain) + ClusterDomain string `envconfig:"KEDA_HTTP_CLUSTER_DOMAIN" default:""` } // Parse parses standard configs using envconfig and returns a pointer to the diff --git a/interceptor/forward_wait_func.go b/interceptor/forward_wait_func.go index d1ff35ffb..926755003 100644 --- a/interceptor/forward_wait_func.go +++ b/interceptor/forward_wait_func.go @@ -17,7 +17,11 @@ type forwardWaitFunc func(context.Context, string, string) (bool, error) func workloadActiveEndpoints(endpoints discov1.EndpointSlice) int { total := 0 for _, e := range endpoints.Endpoints { - total += len(e.Addresses) + if e.Conditions.Ready == nil || *e.Conditions.Ready { + // null should be treated as ready, per: + // https://github.com/kubernetes/api/blob/1446cdecbe6b6afe81373ddedc4dfdb86a7f0bcd/discovery/v1/types.go#L136-L158 + total += len(e.Addresses) + } } return total } diff --git a/interceptor/main.go b/interceptor/main.go index 238f50ed7..8893e0bce 100644 --- a/interceptor/main.go +++ b/interceptor/main.go @@ -198,7 +198,7 @@ func main() { setupLog.Info("starting the proxy server with TLS enabled", "port", proxyTLSPort) - if err := runProxyServer(ctx, ctrl.Log, queues, waitFunc, routingTable, svcCache, timeoutCfg, proxyTLSPort, proxyTLSEnabled, proxyTLSConfig, tracingCfg); !util.IsIgnoredErr(err) { + if err := runProxyServer(ctx, ctrl.Log, queues, waitFunc, routingTable, svcCache, timeoutCfg, proxyTLSPort, proxyTLSEnabled, proxyTLSConfig, tracingCfg, servingCfg.ClusterDomain); !util.IsIgnoredErr(err) { setupLog.Error(err, "tls proxy server failed") return err } @@ -212,7 +212,7 @@ func main() { setupLog.Info("starting the proxy server with TLS disabled", "port", proxyPort) k8sSharedInformerFactory.WaitForCacheSync(ctx.Done()) - if err := runProxyServer(ctx, ctrl.Log, queues, waitFunc, routingTable, svcCache, timeoutCfg, proxyPort, false, nil, tracingCfg); !util.IsIgnoredErr(err) { + if err := runProxyServer(ctx, ctrl.Log, queues, waitFunc, routingTable, svcCache, timeoutCfg, proxyPort, false, nil, tracingCfg, servingCfg.ClusterDomain); !util.IsIgnoredErr(err) { setupLog.Error(err, "proxy server failed") return err } @@ -409,6 +409,7 @@ func runProxyServer( tlsEnabled bool, tlsConfig map[string]interface{}, tracingConfig *config.Tracing, + clusterDomain string, ) error { dialer := kedanet.NewNetDialer(timeouts.Connect, timeouts.KeepAlive) dialContextFunc := kedanet.DialContextWithRetry(dialer, timeouts.DefaultBackoff()) @@ -456,6 +457,7 @@ func runProxyServer( upstreamHandler, svcCache, tlsEnabled, + clusterDomain, ) if tracingConfig.Enabled { diff --git a/interceptor/main_test.go b/interceptor/main_test.go index adefff29a..f8a3ab2ee 100644 --- a/interceptor/main_test.go +++ b/interceptor/main_test.go @@ -94,6 +94,7 @@ func TestRunProxyServerCountMiddleware(t *testing.T) { false, map[string]interface{}{}, &tracingCfg, + "", ) }) // wait for server to start @@ -234,6 +235,7 @@ func TestRunProxyServerWithTLSCountMiddleware(t *testing.T) { true, map[string]interface{}{"certificatePath": "../certs/tls.crt", "keyPath": "../certs/tls.key", "skipVerify": true}, &tracingCfg, + "", ) }) @@ -384,6 +386,7 @@ func TestRunProxyServerWithMultipleCertsTLSCountMiddleware(t *testing.T) { true, map[string]interface{}{"certstorePaths": "../certs"}, &tracingCfg, + "", ) }) diff --git a/interceptor/middleware/routing.go b/interceptor/middleware/routing.go index 83ea23b07..26a9281ef 100644 --- a/interceptor/middleware/routing.go +++ b/interceptor/middleware/routing.go @@ -26,15 +26,17 @@ type Routing struct { upstreamHandler http.Handler svcCache k8s.ServiceCache tlsEnabled bool + clusterDomain string } -func NewRouting(routingTable routing.Table, probeHandler http.Handler, upstreamHandler http.Handler, svcCache k8s.ServiceCache, tlsEnabled bool) *Routing { +func NewRouting(routingTable routing.Table, probeHandler http.Handler, upstreamHandler http.Handler, svcCache k8s.ServiceCache, tlsEnabled bool, clusterDomain string) *Routing { return &Routing{ routingTable: routingTable, probeHandler: probeHandler, upstreamHandler: upstreamHandler, svcCache: svcCache, tlsEnabled: tlsEnabled, + clusterDomain: clusterDomain, } } @@ -109,19 +111,24 @@ func (rm *Routing) streamFromHTTPSO(ctx context.Context, httpso *httpv1alpha1.HT if err != nil { return nil, fmt.Errorf("failed to get port: %w", err) } + + // Build the host part with optional cluster domain + host := fmt.Sprintf("%s.%s", reference.GetServiceName(), httpso.GetNamespace()) + if rm.clusterDomain != "" { + host = fmt.Sprintf("%s.%s", host, rm.clusterDomain) + } + if rm.tlsEnabled { return url.Parse(fmt.Sprintf( - "https://%s.%s:%d", - reference.GetServiceName(), - httpso.GetNamespace(), + "https://%s:%d", + host, port, )) } //goland:noinspection HttpUrlsUsage return url.Parse(fmt.Sprintf( - "http://%s.%s:%d", - reference.GetServiceName(), - httpso.GetNamespace(), + "http://%s:%d", + host, port, )) } diff --git a/interceptor/middleware/routing_test.go b/interceptor/middleware/routing_test.go index 6f3ae4f31..6f9191875 100644 --- a/interceptor/middleware/routing_test.go +++ b/interceptor/middleware/routing_test.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "net/http" "net/http/httptest" @@ -27,7 +28,7 @@ var _ = Describe("RoutingMiddleware", func() { upstreamHandler.Handle("/upstream", emptyHandler) svcCache := k8s.NewFakeServiceCache() - rm := NewRouting(routingTable, probeHandler, upstreamHandler, svcCache, false) + rm := NewRouting(routingTable, probeHandler, upstreamHandler, svcCache, false, "") Expect(rm).NotTo(BeNil()) Expect(rm.routingTable).To(Equal(routingTable)) Expect(rm.probeHandler).To(Equal(probeHandler)) @@ -97,7 +98,7 @@ var _ = Describe("RoutingMiddleware", func() { probeHandler = http.NewServeMux() routingTable = routingtest.NewTable() svcCache = k8s.NewFakeServiceCache() - routingMiddleware = NewRouting(routingTable, probeHandler, upstreamHandler, svcCache, false) + routingMiddleware = NewRouting(routingTable, probeHandler, upstreamHandler, svcCache, false, "") w = httptest.NewRecorder() @@ -171,6 +172,35 @@ var _ = Describe("RoutingMiddleware", func() { }) }) + When("route is found with cluster domain configured", func() { + It("routes to the upstream handler with FQDN", func() { + // Create routing middleware with cluster domain + routingMiddlewareWithDomain := NewRouting(routingTable, probeHandler, upstreamHandler, svcCache, false, "svc.cluster.local") + + var ( + sc = http.StatusTeapot + st = http.StatusText(sc) + ) + + var uh bool + upstreamHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + + _, err := w.Write([]byte(st)) + Expect(err).NotTo(HaveOccurred()) + + uh = true + })) + + routingTable.Memory[host] = &httpso + + routingMiddlewareWithDomain.ServeHTTP(w, r) + Expect(uh).To(BeTrue()) + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + }) + When("route is found with portName but endpoints are mismatched", func() { It("errors to route to upstream handler", func() { var ( @@ -267,6 +297,56 @@ var _ = Describe("RoutingMiddleware", func() { }) }) + Context("streamFromHTTPSO with cluster domain", func() { + var ( + routingMiddleware *Routing + routingMiddlewareWithDomain *Routing + httpso httpv1alpha1.HTTPScaledObject + svcCache *k8s.FakeServiceCache + ) + + BeforeEach(func() { + svcCache = k8s.NewFakeServiceCache() + routingMiddleware = NewRouting(nil, nil, nil, svcCache, false, "") + routingMiddlewareWithDomain = NewRouting(nil, nil, nil, svcCache, false, "svc.cluster.local") + + httpso = httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-httpso", + Namespace: "test-namespace", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Service: "test-service", + Port: 8080, + }, + }, + } + }) + + It("returns short DNS name when cluster domain is empty", func() { + stream, err := routingMiddleware.streamFromHTTPSO(context.Background(), &httpso, httpso.Spec.ScaleTargetRef) + Expect(err).NotTo(HaveOccurred()) + Expect(stream).NotTo(BeNil()) + Expect(stream.String()).To(Equal("http://test-service.test-namespace:8080")) + }) + + It("returns FQDN when cluster domain is set", func() { + stream, err := routingMiddlewareWithDomain.streamFromHTTPSO(context.Background(), &httpso, httpso.Spec.ScaleTargetRef) + Expect(err).NotTo(HaveOccurred()) + Expect(stream).NotTo(BeNil()) + Expect(stream.String()).To(Equal("http://test-service.test-namespace.svc.cluster.local:8080")) + }) + + It("returns FQDN with TLS when cluster domain is set and TLS enabled", func() { + routingMiddlewareWithDomainAndTLS := NewRouting(nil, nil, nil, svcCache, true, "svc.cluster.local") + stream, err := routingMiddlewareWithDomainAndTLS.streamFromHTTPSO(context.Background(), &httpso, httpso.Spec.ScaleTargetRef) + Expect(err).NotTo(HaveOccurred()) + Expect(stream).NotTo(BeNil()) + Expect(stream.String()).To(Equal("https://test-service.test-namespace.svc.cluster.local:8080")) + }) + }) + Context("isKubeProbe", func() { const ( uaKey = "User-Agent" diff --git a/interceptor/proxy_handlers_integration_test.go b/interceptor/proxy_handlers_integration_test.go index 77f0df016..7fa7593cf 100644 --- a/interceptor/proxy_handlers_integration_test.go +++ b/interceptor/proxy_handlers_integration_test.go @@ -322,6 +322,7 @@ func newHarness( &config.Tracing{}), svcCache, false, + "", ) proxySrv, proxySrvURL, err := kedanet.StartTestServer(proxyHdl) diff --git a/operator/Dockerfile b/operator/Dockerfile index b7a9d86f0..b6da526b9 100644 --- a/operator/Dockerfile +++ b/operator/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.24.3 as builder +FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.24.7 as builder WORKDIR /workspace COPY go.* . RUN go mod download diff --git a/scaler/Dockerfile b/scaler/Dockerfile index 2afac99c9..30e745fc0 100644 --- a/scaler/Dockerfile +++ b/scaler/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.24.3 as builder +FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.24.7 as builder WORKDIR /workspace COPY go.* . RUN go mod download