Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ca24449
feat: KEDA Kafka consumer-lag scaling
aliok Sep 4, 2026
21ad31a
test: add integration coverage for KEDA Kafka scaling
aliok Sep 4, 2026
ad471ed
Make linter happy
aliok Sep 4, 2026
5cad927
Make linter happy
aliok Sep 4, 2026
6022212
fix: keda trigger requirement was bypassed when scale is unset
aliok Sep 4, 2026
16e3354
fix: update cmd unit tests for the keda explicit-trigger requirement
aliok Sep 7, 2026
ccea8f9
fix: restore http trigger default for direct Deploy() callers
aliok Sep 8, 2026
8e305de
refactor: rename DeploySpec.Deployer/Expose to ActiveDeployer/ActiveE…
aliok Sep 8, 2026
0893289
refactor: move scale to top level, add KEDA tuning knobs, unify valid…
aliok Sep 8, 2026
819e49a
style: run goimports to fix formatting
aliok Sep 9, 2026
f3734d9
fix(keda): wire up plaintext/configMap SASL password in TriggerAuthen…
aliok Sep 9, 2026
4abac3a
fix(keda): reject unsupported cron trigger and http+kafka combination
aliok Sep 9, 2026
1c306d6
fix(keda): match volume mount path by segment, not prefix
aliok Sep 9, 2026
fa3e4ba
fix(keda): warn instead of discard Kafka cleanup errors
aliok Sep 9, 2026
4f98408
fix(keda): clean up Kafka scaler resources when trigger is dropped
aliok Sep 9, 2026
272f74d
fix(keda): wire mTLS client cert/key into TriggerAuthentication
aliok Sep 9, 2026
e33b3a4
test: fix scale config field in full-path integration helper
aliok Sep 9, 2026
fc24002
docs: correct scale.min/max defaults per deployer
aliok Sep 9, 2026
9f44a77
fix(keda): reject scale.max: 0 for deployer keda
aliok Sep 9, 2026
fb2ad5d
fix(keda): fail fast on unresolvable TLS/SASL cert, reject bad keys
aliok Sep 9, 2026
f9f6a51
fix(keda): error on kafka trigger with missing run.kafka in Deploy
aliok Sep 9, 2026
fa3c964
fix(keda): guard scale.max, fix cluster-local URL port, TLS gate
aliok Sep 9, 2026
4df76ac
fix(keda): reject empty triggers list, correct tls doc wording
aliok Sep 9, 2026
93f322e
fix(keda): reject bad SASL mechanism, fix error msg, reuse ref regex
aliok Sep 9, 2026
4b719e3
fix(functions): fix scale.kpa.target schema, migration data loss
aliok Sep 10, 2026
a14aaed
fix: repair integration tests broken by scale/deploy field moves
aliok Sep 10, 2026
f22b79b
fix(keda): omit blockOwnerDeletion on TriggerAuth/ScaledObject refs
aliok Sep 10, 2026
a828383
fix(keda): preserve finalizers on ScaledObject/TriggerAuth updates
aliok Sep 10, 2026
a958119
fix(keda): run Deploy's static guards before the raw deploy
aliok Sep 10, 2026
7d248e7
fix(keda): delete stale TriggerAuthentication on credential removal
aliok Sep 10, 2026
ca433ca
fix(keda): clear stale exposure when switching to Kafka-only
aliok Sep 10, 2026
284bfef
fix(keda): wire run.kafka.tls.skipVerify to KEDA's scaler
aliok Sep 10, 2026
454746d
fix(functions): don't migrate legacy flat fields into scale.kpa for keda
aliok Sep 10, 2026
77dd9b6
fix(functions): reject duplicate trigger types of the same kind
aliok Sep 10, 2026
fe14f40
fix(cmd): drop cron from --deployer help text
aliok Sep 10, 2026
0f0b597
fix(functions): rename deploy.deployer/expose YAML keys, add migration
aliok Sep 10, 2026
36a5d4e
fix(keda): describe/list Kafka-only functions instead of failing
aliok Sep 10, 2026
4a25a5b
fix(keda): clean up the dropped trigger's scaler before provisioning
aliok Sep 10, 2026
e1b021a
fix(keda): reject unsupported trigger types and http+kafka in Deploy
aliok Sep 10, 2026
f238e3f
fix(keda): fail on partially-resolvable mTLS config
aliok Sep 10, 2026
15053a9
docs: clarify scale.max keda requirement and pollingInterval scope
aliok Sep 10, 2026
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
31 changes: 23 additions & 8 deletions cmd/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,13 @@ func TestDelete_ByProjectClearsDeployedMarker(t *testing.T) {
Runtime: "go",
Registry: TestRegistry,
Deployer: keda.KedaDeployerName, // intent - how to deploy
Deploy: fn.DeploySpec{Namespace: "myns", Deployer: keda.KedaDeployerName},
Deploy: fn.DeploySpec{
Namespace: "myns",
ActiveDeployer: keda.KedaDeployerName,
},
Scale: &fn.ScaleOptions{
KEDA: &fn.KEDAScaleOptions{Triggers: []fn.KEDATrigger{{Type: "http"}}},
},
}
f, err := fn.New().Init(f)
if err != nil {
Expand Down Expand Up @@ -382,8 +388,8 @@ func TestDelete_ByProjectClearsDeployedMarker(t *testing.T) {
if loaded.Deploy.Namespace != "" {
t.Fatalf("expected Deploy.Namespace cleared after a successful undeploy, got %q", loaded.Deploy.Namespace)
}
if loaded.Deploy.Deployer != "" {
t.Fatalf("expected Deploy.Deployer cleared after a successful undeploy, got %q", loaded.Deploy.Deployer)
if loaded.Deploy.ActiveDeployer != "" {
t.Fatalf("expected Deploy.Deployer cleared after a successful undeploy, got %q", loaded.Deploy.ActiveDeployer)
}
if loaded.Deployer != keda.KedaDeployerName {
t.Fatalf("expected the intended Deployer preserved as a remembered choice, got %q", loaded.Deployer)
Expand All @@ -401,7 +407,7 @@ func TestDelete_ByNameLeavesLocalFunctionUntouched(t *testing.T) {
Runtime: "go",
Registry: TestRegistry,
Name: "localfn",
Deploy: fn.DeploySpec{Namespace: "myns", Deployer: keda.KedaDeployerName},
Deploy: fn.DeploySpec{Namespace: "myns", ActiveDeployer: keda.KedaDeployerName},
}
f, err := fn.New().Init(f)
if err != nil {
Expand Down Expand Up @@ -441,7 +447,16 @@ func TestDelete_ByNameLeavesLocalFunctionUntouched(t *testing.T) {
// after removal keeps the INTENT deployer intact and functional.
func TestDelete_ByProjectPreservesDeployerForRedeploy(t *testing.T) {
root := FromTempDirectory(t)
if _, err := fn.New().Init(fn.Function{Runtime: "go", Root: root, Registry: TestRegistry}); err != nil {
f, err := fn.New().Init(fn.Function{Runtime: "go", Root: root, Registry: TestRegistry})
if err != nil {
t.Fatal(err)
}
// keda requires at least one trigger to be declared explicitly.
f.Deployer = keda.KedaDeployerName
f.Scale = &fn.ScaleOptions{
KEDA: &fn.KEDAScaleOptions{Triggers: []fn.KEDATrigger{{Type: "http"}}},
}
if err := f.Write(); err != nil {
t.Fatal(err)
}

Expand Down Expand Up @@ -476,8 +491,8 @@ func TestDelete_ByProjectPreservesDeployerForRedeploy(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Deploy.Deployer != keda.KedaDeployerName {
t.Fatalf("expected the flag-less redeploy to reuse the persisted %q deployer, got %q", keda.KedaDeployerName, loaded.Deploy.Deployer)
if loaded.Deploy.ActiveDeployer != keda.KedaDeployerName {
t.Fatalf("expected the flag-less redeploy to reuse the persisted %q deployer, got %q", keda.KedaDeployerName, loaded.Deploy.ActiveDeployer)
}
}

Expand All @@ -490,7 +505,7 @@ func TestDelete_ByProjectThenRedeployWithDifferentDeployerNotBlocked(t *testing.
Root: root,
Runtime: "go",
Registry: TestRegistry,
Deploy: fn.DeploySpec{Namespace: "myns", Deployer: keda.KedaDeployerName},
Deploy: fn.DeploySpec{Namespace: "myns", ActiveDeployer: keda.KedaDeployerName},
}
f, err := fn.New().Init(f)
if err != nil {
Expand Down
10 changes: 5 additions & 5 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ EXAMPLES
cmd.Flags().StringP("builder", "b", cfg.Builder,
fmt.Sprintf("Builder to use when creating the function's container. Currently supported builders are %s.", KnownBuilders()))
cmd.Flags().String("deployer", cfg.Deployer,
fmt.Sprintf("Type of deployment to use: '%s' for Knative Service, '%s' for Kubernetes Deployment, or '%s' for Deployment with a KEDA HTTP scaler ($FUNC_DEPLOYER)", deployers.Knative, deployers.Kubernetes, deployers.Keda))
fmt.Sprintf("Type of deployment to use: '%s' for Knative Service, '%s' for Kubernetes Deployment, or '%s' for Deployment scaled by KEDA (HTTP or Kafka triggers) ($FUNC_DEPLOYER)", deployers.Knative, deployers.Kubernetes, deployers.Keda))
cmd.Flags().StringP("registry", "r", cfg.Registry,
"Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY)")
cmd.Flags().Bool("registry-insecure", cfg.RegistryInsecure, "Skip TLS certificate verification when communicating in HTTPS with the registry. The value is persisted over consecutive runs ($FUNC_REGISTRY_INSECURE)")
Expand Down Expand Up @@ -306,8 +306,8 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {

// Back-compat: a function deployed before the deployer was recorded has a
// namespace but no deployer, which historically could only mean knative.
if f.Deploy.Namespace != "" && f.Deploy.Deployer == "" {
f.Deploy.Deployer = deployers.Knative
if f.Deploy.Namespace != "" && f.Deploy.ActiveDeployer == "" {
f.Deploy.ActiveDeployer = deployers.Knative
}

if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
Expand Down Expand Up @@ -368,7 +368,7 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
var url string
// Invoke a remote build/push/deploy pipeline
// Returned is the function with fields like Registry, f.Deploy.Image &
// f.Deploy.Namespace, f.Deploy.Expose populated.
// f.Deploy.Namespace, f.Deploy.ActiveExpose populated.
if url, f, err = client.RunPipeline(cmd.Context(), f); err != nil {
return wrapDeploymentError(err)
}
Expand All @@ -377,7 +377,7 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
// After a remote pipeline: intent was active, describer saw no
// applied expose. Typical cause is a func-util image that predates
// this field. Knative is excluded; it never applies expose.
if fn.ExposureRecordMissing(f.Expose, f.Deploy.Expose, f.Deploy.Deployer) {
if fn.ExposureRecordMissing(f.Expose, f.Deploy.ActiveExpose, f.Deploy.ActiveDeployer) {
fmt.Fprintf(cmd.OutOrStderr(), "Warning: expose %q was requested but the cluster's "+
"func-util image applied no external exposure; the function is running cluster-local\n", f.Expose)
}
Expand Down
77 changes: 51 additions & 26 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -386,7 +387,7 @@ func TestDeploy_Envs(t *testing.T) {
t.Fatal(err)
}

// Mock deployer records f.Deploy.Deployer so later redeploys are not
// Mock deployer records f.Deploy.ActiveDeployer so later redeploys are not
// treated as the pre-field legacy (knative) case.
clientFn := NewTestClient(fn.WithDeployer(mock.NewDeployer()))

Expand Down Expand Up @@ -1161,7 +1162,7 @@ func TestDeploy_NamespaceRedeployWarning(t *testing.T) {
f := fn.Function{
Runtime: "go",
Root: root,
Deploy: fn.DeploySpec{Namespace: "funcns", Deployer: deployers.Default},
Deploy: fn.DeploySpec{Namespace: "funcns", ActiveDeployer: deployers.Default},
}
f, err := fn.New().Init(f)
if err != nil {
Expand Down Expand Up @@ -1211,8 +1212,8 @@ func TestDeploy_NamespaceUpdateWarning(t *testing.T) {
Runtime: "go",
Root: root,
Deploy: fn.DeploySpec{
Namespace: "myns",
Deployer: deployers.Default,
Namespace: "myns",
ActiveDeployer: deployers.Default,
},
}
f, err := fn.New().Init(f)
Expand Down Expand Up @@ -1363,7 +1364,7 @@ func TestDeploy_NamespaceChangePreservesExternalRegistry(t *testing.T) {
Runtime: "go",
Root: root,
Registry: "docker.io/user",
Deploy: fn.DeploySpec{Namespace: "ns1", Deployer: deployers.Default},
Deploy: fn.DeploySpec{Namespace: "ns1", ActiveDeployer: deployers.Default},
}
f, err := fn.New().Init(f)
if err != nil {
Expand Down Expand Up @@ -1398,7 +1399,7 @@ func TestDeploy_NamespaceChangeUpdatesInternalRegistry(t *testing.T) {
Runtime: "go",
Root: root,
Registry: "image-registry.openshift-image-registry.svc:5000/ns1",
Deploy: fn.DeploySpec{Namespace: "ns1", Deployer: deployers.Default},
Deploy: fn.DeploySpec{Namespace: "ns1", ActiveDeployer: deployers.Default},
}
f, err := fn.New().Init(f)
if err != nil {
Expand Down Expand Up @@ -2599,8 +2600,8 @@ func TestDeploy_DeployerPersists(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Deploy.Deployer != deployers.Default {
t.Fatalf("expected persisted deployer %q, got %q", deployers.Default, loaded.Deploy.Deployer)
if loaded.Deploy.ActiveDeployer != deployers.Default {
t.Fatalf("expected persisted deployer %q, got %q", deployers.Default, loaded.Deploy.ActiveDeployer)
}
})

Expand Down Expand Up @@ -2629,8 +2630,8 @@ func TestDeploy_DeployerPersists(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Deploy.Deployer != other {
t.Fatalf("expected persisted deployer %q, got %q", other, loaded.Deploy.Deployer)
if loaded.Deploy.ActiveDeployer != other {
t.Fatalf("expected persisted deployer %q, got %q", other, loaded.Deploy.ActiveDeployer)
}

// no --deployer flag: the flag defaults to the persisted value
Expand All @@ -2648,8 +2649,8 @@ func TestDeploy_DeployerPersists(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Deploy.Deployer != other {
t.Fatalf("expected deployer to remain %q after a flag-less redeploy, got %q", other, loaded.Deploy.Deployer)
if loaded.Deploy.ActiveDeployer != other {
t.Fatalf("expected deployer to remain %q after a flag-less redeploy, got %q", other, loaded.Deploy.ActiveDeployer)
}
})
}
Expand Down Expand Up @@ -2682,8 +2683,8 @@ func TestDeploy_DeployerGlobalConfig(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Deploy.Deployer != k8s.KubernetesDeployerName {
t.Fatalf("expected the global config's deployer %q to seed the flagless deploy, got %q", k8s.KubernetesDeployerName, loaded.Deploy.Deployer)
if loaded.Deploy.ActiveDeployer != k8s.KubernetesDeployerName {
t.Fatalf("expected the global config's deployer %q to seed the flagless deploy, got %q", k8s.KubernetesDeployerName, loaded.Deploy.ActiveDeployer)
}
}

Expand Down Expand Up @@ -2716,7 +2717,20 @@ func TestDeploy_DeployerSwitch(t *testing.T) {
Root: root,
Registry: TestRegistry,
// Namespace set == already deployed, which is what the guard gates on.
Deploy: fn.DeploySpec{Namespace: "myns", Deployer: tt.deployedDep},
Deploy: fn.DeploySpec{Namespace: "myns", ActiveDeployer: tt.deployedDep},
}
// keda requires at least one trigger to be declared explicitly,
// but only matters when keda ends up the effective deployer for
// this attempt (an explicit switch away from it does not).
effectiveDeployer := tt.requested
if effectiveDeployer == "" {
effectiveDeployer = tt.deployedDep
}
if effectiveDeployer == keda.KedaDeployerName {
f.Deployer = keda.KedaDeployerName
f.Scale = &fn.ScaleOptions{
KEDA: &fn.KEDAScaleOptions{Triggers: []fn.KEDATrigger{{Type: "http"}}},
}
}
if _, err := fn.New().Init(f); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -2824,8 +2838,8 @@ func TestDeploy_ExposeEmptyVsUnset(t *testing.T) {
if f.Expose != "" {
t.Errorf("expected intent expose empty, got %q", f.Expose)
}
if f.Deploy.Expose != "" {
t.Errorf("expected status expose empty, got %q", f.Deploy.Expose)
if f.Deploy.ActiveExpose != "" {
t.Errorf("expected status expose empty, got %q", f.Deploy.ActiveExpose)
}
})

Expand All @@ -2837,8 +2851,8 @@ func TestDeploy_ExposeEmptyVsUnset(t *testing.T) {
t.Fatalf("expected intent expose 'none', got %q", f.Expose)
}
// status is observed applied mode; "none"/empty both mean cluster-local
if f := loadFn(t, root); f.Deploy.Expose != "" {
t.Fatalf("expected status expose empty for cluster-local, got %q", f.Deploy.Expose)
if f := loadFn(t, root); f.Deploy.ActiveExpose != "" {
t.Fatalf("expected status expose empty for cluster-local, got %q", f.Deploy.ActiveExpose)
}

// redeploy without the flag should keep intent via flag default
Expand Down Expand Up @@ -2897,8 +2911,8 @@ func TestDeploy_ExposeRoutePersists(t *testing.T) {
if f.Expose != "route" {
t.Fatalf("expected intent expose 'route', got %q", f.Expose)
}
if f.Deploy.Expose != "route" {
t.Fatalf("expected status expose 'route', got %q", f.Deploy.Expose)
if f.Deploy.ActiveExpose != "route" {
t.Fatalf("expected status expose 'route', got %q", f.Deploy.ActiveExpose)
}
}

Expand Down Expand Up @@ -2944,9 +2958,20 @@ func TestDeploy_ExposeIgnoredByDeployerNote(t *testing.T) {
// route cases need OpenShift gate open; none/empty do not care.
cleanup := k8s.SetOpenShiftForTest(true, nil)
defer cleanup()
if _, err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil {
f, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
if err != nil {
t.Fatal(err)
}
if slices.Contains(tt.args, "keda") {
// keda requires at least one trigger to be declared explicitly.
f.Deployer = keda.KedaDeployerName
f.Scale = &fn.ScaleOptions{
KEDA: &fn.KEDAScaleOptions{Triggers: []fn.KEDATrigger{{Type: "http"}}},
}
if err := f.Write(); err != nil {
t.Fatal(err)
}
}

builder := mock.NewBuilder()
cmd := NewDeployCmd(NewTestClient(
Expand All @@ -2958,7 +2983,7 @@ func TestDeploy_ExposeIgnoredByDeployerNote(t *testing.T) {
var stderr strings.Builder
cmd.SetOut(&stderr)
cmd.SetErr(&stderr)
err := cmd.Execute()
err = cmd.Execute()

if err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down Expand Up @@ -3016,7 +3041,7 @@ func TestDeploy_RemoteExposeRecordsObservation(t *testing.T) {
pipeliner.RunFn = func(f fn.Function) (string, fn.Function, error) {
// add exposure tracking to the base RunFn
url, f, err := base(f)
f.Deploy.Expose = tt.observed
f.Deploy.ActiveExpose = tt.observed
return url, f, err
}

Expand All @@ -3038,8 +3063,8 @@ func TestDeploy_RemoteExposeRecordsObservation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if f.Deploy.Expose != tt.wantRecord {
t.Errorf("Deploy.Expose = %q, want %q", f.Deploy.Expose, tt.wantRecord)
if f.Deploy.ActiveExpose != tt.wantRecord {
t.Errorf("Deploy.Expose = %q, want %q", f.Deploy.ActiveExpose, tt.wantRecord)
}
warned := strings.Contains(out.String(), "applied no external exposure")
if warned != tt.wantWarning {
Expand Down
2 changes: 1 addition & 1 deletion cmd/func-util/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func deploy(ctx context.Context) error {
// honors --deployer, which travels in func.yaml as intent.
deployer := f.Deployer
if deployer == "" {
deployer = f.Deploy.Deployer
deployer = f.Deploy.ActiveDeployer
}
if deployer == "" {
deployer = knative.KnativeDeployerName
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/func_deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func deploy
-b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack")
--builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)
-c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM)
--deployer string Type of deployment to use: 'knative' for Knative Service, 'raw' for Kubernetes Deployment, or 'keda' for Deployment with a KEDA HTTP scaler ($FUNC_DEPLOYER) (default "knative")
--deployer string Type of deployment to use: 'knative' for Knative Service, 'raw' for Kubernetes Deployment, or 'keda' for Deployment scaled by KEDA (HTTP or Kafka triggers) ($FUNC_DEPLOYER) (default "knative")
--domain string Domain to use for the function's route. Cluster must be configured with domain matching for the given domain (ignored if unrecognized) ($FUNC_DOMAIN)
-e, --env stringArray Environment variable to set in the form NAME=VALUE. You may provide this flag multiple times for setting multiple environment variables. To unset, specify the environment variable name followed by a "-" (e.g., NAME-).
--expose string External exposure mode: 'route' for an OpenShift Route (OpenShift clusters only), 'none' for cluster-local. Default: no exposure. Raw and keda deployers only. ($FUNC_EXPOSE)
Expand Down
Loading
Loading