Skip to content
Draft
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
64 changes: 64 additions & 0 deletions .github/workflows/functions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,69 @@ jobs:
path: ./cluster_log.txt
retention-days: 7

# ----------------
# E2E EXPOSE TESTS
# ----------------
# External exposure (--expose). The cluster here is KinD, which has no
# route.openshift.io API, so the tests that assert a Route skip themselves via
# IsOpenShift(), and the ones that assert func REFUSES a Route are the ones
# that run. Pointing the same target at an OpenShift cluster runs the other
# half; CI has no OpenShift cluster, so that half is run manually.
test-e2e-expose:
name: E2E - Expose
needs: precheck
runs-on: ubuntu-latest
timeout-minutes: 60
env:
FUNC_CLUSTER_RETRIES: 5
FUNC_E2E_CLEAN: false # cluster only used once
FUNC_E2E_VERBOSE: true
# The keda cases needing keda on the cluster are all Route cases, and
# those skip here. The one keda test that does run refuses a too-long
# name before reaching the cluster at all. Flip to "true" if a keda test
# is added that needs the operator; see PR #3914 ('Insufficient cpu') for
# why it is off by default.
FUNC_CLUSTER_KEDA: "false"
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: endersonmenezes/free-disk-space@v3
with:
remove_android: true
remove_dotnet: true
remove_haskell: true
remove_swap: true
rm_cmd: "rmz" # Faster than rm

- name: Install Binaries
run: ./hack/binaries.sh
- name: Allocate Cluster
run: ./hack/cluster.sh
- name: Start Local Registry
run: ./hack/registry.sh
- name: Prepare Images
run: ./hack/images.sh

- name: Run Expose E2E Tests
run: make test-e2e-expose

- uses: ./.github/actions/codecov
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: e2e

# Preserve Cluster Logs
- name: Dump Cluster Logs
if: always()
run: ./hack/dump-logs.sh cluster_log.txt
- name: Archive Cluster Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: cluster-logs-e2e-expose
path: ./cluster_log.txt
retention-days: 7

# ----------------
# E2E PODMAN TESTS
# ----------------
Expand Down Expand Up @@ -457,6 +520,7 @@ jobs:
- test-integration
- test-templates
- test-e2e
- test-e2e-expose
- test-e2e-podman
- test-e2e-runtimes
- test-e2e-config-ci
Expand Down
32 changes: 28 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,36 @@ test: generate/zz_filesystem_generated.go ## Run core unit tests
go test -race -cover -coverprofile=coverage.txt ./...

.PHONY: check
check: check-lint check-goimports check-misspell check-whitespace check-eof ## Check code quality (comprehensive)
check: check-lint check-build-tags check-goimports check-misspell check-whitespace check-eof ## Check code quality (comprehensive)

.PHONY: check-lint
check-lint: $(BIN_GOLANGCI_LINT) ## Run golangci-lint
$(BIN_GOLANGCI_LINT) run --timeout 300s

# cmd/func-util/main.go is behind "exclude_graphdriver_btrfs || !cgo", so the
# default build context compiles only socat.go and every other check in this
# file silently skips it: go vet, golangci-lint and go test all read the
# default context. The sole compile of it anywhere is publish-utils-image in
# functions.yaml, which needs: build and so runs only on a push to main. A type
# error there therefore passes every pull-request check and first fails after
# merge. Vet with the tag so the file is type-checked where the error is cheap.
.PHONY: check-build-tags
check-build-tags: ## Type-check sources the default build context excludes
@echo "Type-checking build-tagged sources..."
go vet -tags exclude_graphdriver_btrfs ./cmd/func-util/

.PHONY: check-goimports
check-goimports: $(BIN_GOIMPORTS) ## Check Go import formatting
@echo "Checking Go import formatting..."
@$(LS_SOURCES) | \
@offenders=$$($(LS_SOURCES) | \
grep '\.go$$' | \
while IFS= read -r file; do [ -f "$$file" ] && echo "$$file"; done | \
xargs $(BIN_GOIMPORTS) -l | grep . && \
(echo "Error: Files with incorrect import formatting found. Run 'goimports -w <file>' to fix."; exit 1) || true
xargs $(BIN_GOIMPORTS) -l); \
if [ -n "$$offenders" ]; then \
echo "$$offenders"; \
echo "Error: Files with incorrect import formatting found. Run 'goimports -w <file>' to fix."; \
exit 1; \
fi

.PHONY: check-misspell
check-misspell: $(BIN_MISSPELL) ## Check for common misspellings
Expand Down Expand Up @@ -317,6 +333,14 @@ test-e2e-lifecycle: func-instrumented-bin ## Run lifecycle hook E2E tests (Start
go test -tags e2e -timeout 60m ./e2e -v -run TestLifecycle_
go tool covdata textfmt -i=$${FUNC_E2E_GOCOVERDIR:-.coverage} -o coverage.txt

.PHONY: test-e2e-expose
test-e2e-expose: func-instrumented-bin ## Run external exposure E2E tests (--expose)
# Runtime and other options can be configured using the FUNC_E2E_* environment variables. see e2e_test.go
# Assertions about a Route skip unless the target cluster is OpenShift, and the
# assertions that func REFUSES a Route skip unless it is not. See records/test-plan-exposure.md
go test -tags e2e -timeout 30m ./e2e -v -run TestExpose_
go tool covdata textfmt -i=$${FUNC_E2E_GOCOVERDIR:-.coverage} -o coverage.txt

.PHONY: test-e2e-config-ci
test-e2e-config-ci: func-instrumented-bin ## CI tests for generated GitHub Workflows
# Runtime and other options can be configured using the FUNC_E2E_* environment variables. see e2e_test.go
Expand Down
29 changes: 20 additions & 9 deletions cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"os"

"github.com/ory/viper"
"knative.dev/func/pkg/deployers"
"knative.dev/func/pkg/keda"
"knative.dev/func/pkg/ocproute"

"knative.dev/func/cmd/prompt"
"knative.dev/func/pkg/buildpacks"
Expand Down Expand Up @@ -71,7 +73,8 @@ func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) {
fn.WithRepositoriesPath(config.RepositoriesPath()),
fn.WithScaffolder(buildpacks.NewScaffolder(cfg.Verbose)),
fn.WithBuilder(buildpacks.NewBuilder(buildpacks.WithVerbose(cfg.Verbose))),
fn.WithRemovers(knative.NewRemover(cfg.Verbose), k8s.NewRemover(cfg.Verbose), keda.NewRemover(cfg.Verbose)),
fn.WithRemovers(knative.NewRemover(cfg.Verbose), k8s.NewRemover(cfg.Verbose),
keda.NewRemover(cfg.Verbose)),
fn.WithDescribers(
knative.NewDescriber(cfg.Verbose, knative.WithDescriberTransport(t)),
k8s.NewDescriber(cfg.Verbose, k8s.WithDescriberTransport(t)),
Expand Down Expand Up @@ -173,22 +176,30 @@ func newKnativeDeployer(verbose bool) fn.Deployer {
return knative.NewDeployer(options...)
}

// newK8sDeployer builds the raw deployer.
//
// The Exposer is attached unconditionally, not only when the deploy asks for a
// Route. The record saying whether teardown is owed lives on the cluster, so
// wiring time cannot know.
func newK8sDeployer(verbose bool) fn.Deployer {
options := []k8s.DeployerOpt{
return k8s.NewDeployer(
k8s.WithDeployerVerbose(verbose),
k8s.WithDeployerDecorator(deployDecorator{}),
}

return k8s.NewDeployer(options...)
k8s.WithExposer(ocproute.New(deployers.Kubernetes)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like in both cases (keda and k8s deployers), the exposer could be defaulted to this, as there would presumably be no import cycle with ocproute, and it's compatible with both OpenShift and vanilla Kubernetes.

That would greatly simplify the CLI (and anyone wanting to use Functions via the library), since a simple
deployer := k8s.NewDeployer() would be sufficient.

Is there a reason we need the deploy decorator and ocproute exposer instantiated here in the CLI?

@gauron99 gauron99 Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently default to no exposure because of simplicity. as the first commit I removed my "one deployer switch" which was ok, the deployed raw -> redeploy with keda because the complexity of keeping that alive was more trouble at the time with adding the exposer and felt low ROI

If we make route the default now, when we later introduce the gateway it will require to make some form of this again because the expose is platform specific.
OpenShift -> route
Vanilla -> gateway

Then there is also the question of... "I need to undeploy old exposer! OK I need the... exposer" - this whole thing is to be refactored with the gateway (second exposer in mind) but I deferred for later in interest of time to make this PR

)
}

// newKedaDeployer builds the keda deployer. The Exposer is keda's own, never
// the embedded raw deployer's, so Routes point at the interceptor rather than
// bypassing it. Attached unconditionally for the reason in newK8sDeployer,
// which bites harder here: keda's Route has no owner reference, so a Route

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honest take — definitely load-bearing — a sharp call, even.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotta love the AI wording!

// nothing goes looking for is a Route nothing ever removes.
func newKedaDeployer(verbose bool) fn.Deployer {
options := []keda.DeployerOpt{
return keda.NewDeployer(
keda.WithDeployerVerbose(verbose),
keda.WithDeployerDecorator(deployDecorator{}),
}

return keda.NewDeployer(options...)
keda.WithExposer(ocproute.New(deployers.Keda)),
)
}

type deployDecorator struct {
Expand Down
20 changes: 20 additions & 0 deletions cmd/completion_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,23 @@ func CompleteDeployerList(cmd *cobra.Command, args []string, complete string) (m

return
}

func CompleteExposeList(cmd *cobra.Command, args []string, complete string) (matches []string, d cobra.ShellCompDirective) {
values := fn.ExposeModes

d = cobra.ShellCompDirectiveNoFileComp
matches = []string{}

if len(complete) == 0 {
matches = values
return
}

for _, v := range values {
if strings.HasPrefix(v, complete) {
matches = append(matches, v)
}
}

return
}
Loading
Loading