Compare commits

..

No commits in common. "main" and "v0.15.0" have entirely different histories.

1399 changed files with 21256 additions and 168262 deletions

View File

@ -2,7 +2,7 @@ style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://github.com/knative/func
repository_url: https://github.com/boson-project/func
options:
commits:
filters:

View File

@ -1,27 +0,0 @@
coverage:
# Commit status https://docs.codecov.io/docs/commit-status are used
# to block PR based on coverage threshold.
status:
project:
default:
target: auto
threshold: 4%
patch:
# Disable the coverage threshold of the patch, so that PRs are
# only failing because of overall project coverage threshold.
# See https://docs.codecov.io/docs/commit-status#disabling-a-status.
default: false
comment:
# Update existing comment or create new if deleted.
behavior: default
ignore:
- "testdata"
- "**/zz*_generated.go"
- "templates"
- "hack"
- "test"
- "generate"
- "docs"
- "plugin"
- "schema"
- "third_party"

9
.gitattributes vendored
View File

@ -1,9 +0,0 @@
testdata/repository-a.git/objects/*/* ignore-lint=true
testdata/repository.git/objects/*/* ignore-lint=true
templates/node/*/package-lock.json ignore-lint=true
templates/typescript/*/package-lock.json ignore-lint=true
version.txt linguist-generated=true
zz_filesystem_generated.go linguist-generated=true
docker/zz_close_guarding_client_generated.go linguist-generated=true
pkg/oci/testdata/test-links/* ignore-lint=true

View File

@ -5,258 +5,139 @@ on:
branches:
- "main"
jobs:
check:
runs-on: "ubuntu-latest"
test:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Lint
run: make check && make check-templates
- name: Check that 'func.yaml schema' is up-to-date
run: make schema-check
- name: Check embedded templates content
run: go test -run "^\QTestFileSystems\E$/^\Qembedded\E$" ./pkg/filesystem
test-unit:
strategy:
matrix:
java: [ 21 ]
os: [ "ubuntu-latest", "windows-latest", "macos-latest" ]
runs-on: ${{ matrix.os }}
steps:
- run: git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: actions/setup-java@v4
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- name: Determine download URL for pkger
id: pkger-download-url
uses: actions/github-script@v2
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Unit Test
result-encoding: string
script: |
return github.repos.getReleaseByTag({
owner: "markbates",
repo: "pkger",
tag: "v0.17.1"
}).then(result => {
return result.data.assets
.filter(a => a.name.includes('Linux_x86'))
.map(a => a.browser_download_url)[0];
})
- name: Install pkger
run: |
curl -s -L -o pkger.tgz ${{ steps.pkger-download-url.outputs.result }}
tar xvzf pkger.tgz
- name: Test
run: make test
- name: Template Unit Tests
run: make test-templates
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: unit-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
test-integration:
runs-on: "ubuntu-latest"
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Integration Tests
run: make test-integration
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: integration-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
e2e-test:
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: E2E Test
run: make test-e2e
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: e2e-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
e2e-on-cluster-test:
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: imjasonh/setup-ko@v0.6
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Deploy Test Git Server
run: ./hack/install-git-server.sh
- name: E2E On Cluster Test
env:
E2E_RUNTIMES: ""
run: make test-e2e-on-cluster
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: e2e-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
PKGER: "./${{ steps.pkger-binaries.outputs.binary }}"
- name: Lint
run: make check
outputs:
pkger: ${{ steps.pkger-download-url.outputs.result }}
build:
needs: [check, test-unit, test-integration, e2e-test, e2e-on-cluster-test]
build-and-publish:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
# Create a release, or update the release PR
- uses: GoogleCloudPlatform/release-please-action@v2.24.1
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: simple
bump-minor-pre-major: true
# Checkout
- uses: actions/checkout@v2
# Tag
# If a release was created, tag `vX.Y.Z` synchronously which:
# 1. Triggers release-please to create draft release, allowing manual
# proofreading (and probably editing) of release notes.
# (often raw commit messages are a bit overwhelming, overly granular
# or at least could use some context when included as release notes).
# 2. Ensures the tag exists for subsequent action tasks which rely on
# this metadata, such as including version in release binaries.
# The release-please action does add this tag, but asynchronously.
# Note that tag is created annotated such that it shows tagging metadata
# when queried rather than just the associated commit metadata.
- name: tag
if: ${{ steps.release.outputs.release_created }}
run: |
git config user.name github-actions[bot]
git tag -d v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} || true
git push origin :v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} || true
git tag -a v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} -m "Release v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}"
git push origin v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} || true
- uses: actions/setup-go@v2
- name: Install pkger
run: |
curl -s -L -o pkger.tgz ${{ needs.test.outputs.pkger }}
tar xvzf pkger.tgz
# Standard build tasks
- name: Build
run: make cross-platform
# Upload all build artifacts
- uses: actions/upload-artifact@v4
env:
PKGER: "./pkger"
# NOTE:
# release-please adds the version asynchronously. Without using the
# synchonous tagging step above, the version can be explicitly passed
# to the build using the following environment variable. However this
# has the side-effect of causing inter-relese binaries to not include
# verbose version information, because the special version `tip` is
# overriden with a blank string in those cases.
# VERS: ${{ steps.release.outputs.tag_name }}
# Upload all build artifacts whether it's a release or not
- uses: actions/upload-artifact@v2
with:
name: OSX Binary (AMD)
name: OSX Binary
path: func_darwin_amd64
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
with:
name: OSX Binary (ARM)
path: func_darwin_arm64
- uses: actions/upload-artifact@v4
with:
name: Linux Binary (AMD)
name: Linux Binary
path: func_linux_amd64
- uses: actions/upload-artifact@v4
with:
name: Linux Binary (ARM)
path: func_linux_arm64
- uses: actions/upload-artifact@v4
with:
name: Linux Binary (PPC64LE)
path: func_linux_ppc64le
- uses: actions/upload-artifact@v4
with:
name: Linux Binary (S390X)
path: func_linux_s390x
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
with:
name: Windows Binary
path: func_windows_amd64.exe
publish-utils-image:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: docker/setup-qemu-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
run: |
for a in amd64 arm64 ppc64le s390x; do
CGO_ENABLED=0 GOARCH="$a" go build -o "func-util-$a" -trimpath -ldflags '-w -s' ./cmd/func-util
done
docker buildx create --name multiarch --driver docker-container --use
docker buildx build . -f Dockerfile.utils \
--platform=linux/ppc64le,linux/s390x,linux/amd64,linux/arm64 \
--push \
-t "ghcr.io/knative/func-utils:v2" \
--annotation index:org.opencontainers.image.description="Knative Func Utils Image" \
--annotation index:org.opencontainers.image.source="https://github.com/knative/func" \
--annotation index:org.opencontainers.image.vendor="https://github.com/knative/func" \
--annotation index:org.opencontainers.image.url="https://github.com/knative/func/pkgs/container/func-utils"
# The following steps are only executed if this is a release
- name: Compress Binaries
if: ${{ steps.release.outputs.release_created }}
run: gzip func_darwin_amd64 func_linux_amd64 func_windows_amd64.exe
publish-image:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: imjasonh/setup-ko@v0.6
- run: ko build --platform=linux/ppc64le,linux/s390x,linux/amd64,linux/arm64 -B ./cmd/func
# Upload all binaries
- name: Upload Darwin Binary
uses: actions/upload-release-asset@v1
if: ${{ steps.release.outputs.release_created }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.release.outputs.upload_url }}
asset_path: ./func_darwin_amd64.gz
asset_name: func_darwin_amd64.gz
asset_content_type: application/x-gzip
- name: Upload Linux Binary
uses: actions/upload-release-asset@v1
if: ${{ steps.release.outputs.release_created }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.release.outputs.upload_url }}
asset_path: ./func_linux_amd64.gz
asset_name: func_linux_amd64.gz
asset_content_type: application/x-gzip
- name: Upload Windows Binary
uses: actions/upload-release-asset@v1
if: ${{ steps.release.outputs.release_created }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.release.outputs.upload_url }}
asset_path: ./func_windows_amd64.exe.gz
asset_name: func_windows_amd64.exe.gz
asset_content_type: application/x-gzip

View File

@ -1,14 +0,0 @@
# Copyright 2020 The Knative Authors.
# SPDX-License-Identifier: Apache-2.0
# This file is automagically synced here from github.com/knative-extensions/knobots
name: Build
on:
pull_request:
branches: [ 'main', 'release-*' ]
jobs:
build:
uses: knative/actions/.github/workflows/reusable-go-build.yaml@main

View File

@ -1,19 +0,0 @@
# Copyright 2022 The Knative Authors.
# SPDX-License-Identifier: Apache-2.0
# This file is automagically synced here from github.com/knative-extensions/knobots
name: Test
on:
push:
branches: [ 'main', 'release-*' ]
pull_request:
branches: [ 'main', 'release-*' ]
jobs:
test:
uses: knative/actions/.github/workflows/reusable-go-test.yaml@main
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,17 +0,0 @@
# Copyright 2020 The Knative Authors.
# SPDX-License-Identifier: Apache-2.0
# This file is automagically synced here from github.com/knative-extensions/knobots
name: 'Security'
on:
push:
branches: [ 'main', 'release-*' ]
pull_request:
branches: [ 'main', 'release-*' ]
jobs:
analyze:
uses: knative/actions/.github/workflows/reusable-security.yaml@main

View File

@ -1,14 +0,0 @@
# Copyright 2020 The Knative Authors.
# SPDX-License-Identifier: Apache-2.0
# This file is automagically synced here from github.com/knative-extensions/knobots
name: 'Close stale'
on:
schedule:
- cron: '0 1 * * *'
jobs:
stale:
uses: knative/actions/.github/workflows/reusable-stale.yaml@main

View File

@ -1,15 +0,0 @@
# Copyright 2020 The Knative Authors.
# SPDX-License-Identifier: Apache-2.0
# This file is automagically synced here from github.com/knative-extensions/knobots
name: Code Style
on:
pull_request:
branches: [ 'main', 'release-*' ]
jobs:
style:
uses: knative/actions/.github/workflows/reusable-style.yaml@main

View File

@ -1,26 +0,0 @@
# Copyright 2020 The Knative Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file is automagically synced here from github.com/knative-extensions/.github
# repo by knobots: https://github.com/knative-extensions/knobots and will be overwritten.
name: Verify
on:
pull_request:
branches: [ 'main', 'release-*' ]
jobs:
verify:
uses: knative/actions/.github/workflows/reusable-verify-codegen.yaml@main

93
.github/workflows/pull_requests.yaml vendored Normal file
View File

@ -0,0 +1,93 @@
# Build and test all pull requests
name: Pull Requests
on: [pull_request]
jobs:
build:
strategy:
matrix:
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- name: Determine platform binaries
id: pkger-binaries
uses: actions/github-script@v2
with:
result-encoding: string
script: |
let platform, binary;
switch ('${{matrix.os}}') {
case 'ubuntu-latest':
platform = 'Linux_x86'
binary = 'pkger'
break
case 'windows-latest':
platform = 'Windows_x86'
binary = 'pkger.exe'
break
case 'macos-latest':
platform = 'Darwin_x86'
binary = 'pkger'
break
}
core.setOutput('platform', platform)
core.setOutput('binary', binary)
- name: Determine download URL for latest pkger
id: pkger-download-url
uses: actions/github-script@v2
with:
result-encoding: string
script: |
let platform = "${{ steps.pkger-binaries.outputs.platform }}"
let binary = "${{ steps.pkger-binaries.outputs.binary }}"
core.info('PLATFORM: ' + platform)
core.info('BINARY: ' + binary)
return github.repos.getReleaseByTag({
owner: "markbates",
repo: "pkger",
tag: "v0.17.1"
}).then(result => {
return result.data.assets
.filter(a => a.name.includes(platform))
.map(a => a.browser_download_url)[0];
})
- name: Install pkger
run: |
curl -s -L -o pkger.tgz ${{ steps.pkger-download-url.outputs.result }}
tar xvzf pkger.tgz
- name: Test
run: make test
env:
PKGER: "./${{ steps.pkger-binaries.outputs.binary }}"
- name: Build
run: make build
env:
PKGER: "./${{ steps.pkger-binaries.outputs.binary }}"
- name: Lint
run: make check
integration-test:
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
- name: Provision Cluster
uses: lkingland/kind-action@v1 # use ./hack/allocate.sh locally
with:
version: v0.10.0
kubectl_version: v1.20.0
knative_serving: v0.22.0
knative_kourier: v0.22.0
knative_eventing: v0.22.0
config: testdata/cluster.yaml
- name: Configure Cluster
run: ./hack/configure.sh
- name: Integration Test
run: make test-integration

View File

@ -1,16 +0,0 @@
name: Func Check Schema
on: [pull_request]
jobs:
check:
name: Check Schema
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Check that 'func.yaml schema' is up-to-date
run: make schema-check

View File

@ -1,64 +0,0 @@
name: Func E2E OnCluster RT Test
on: [pull_request]
jobs:
test:
name: On Cluster RT Test
continue-on-error: true
strategy:
matrix:
os: ["ubuntu-latest"]
func_builder: ["pack", "s2i"]
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: imjasonh/setup-ko@v0.6
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Deploy Test Git Server
run: ./hack/install-git-server.sh
- name: E2E On Cluster Test (Runtimes)
env:
TEST_TAGS: runtime
E2E_REGISTRY_URL: registry.default.svc.cluster.local:5000
FUNC_REPO_REF: ${{ github.event.pull_request.head.repo.full_name }}
FUNC_REPO_BRANCH_REF: ${{ github.head_ref }}
FUNC_BUILDER: ${{ matrix.func_builder }}
run: make test-e2e-on-cluster
- name: Dump Cluster Logs
if: always()
run: |
echo "::group::cluster events"
kubectl get events -A
echo "::endgroup::"
echo "::group::cluster containers logs"
stern '.*' --all-namespaces --no-follow
echo "::endgroup::"

View File

@ -1,55 +0,0 @@
name: Func E2E OnCluster Test
on: [pull_request]
jobs:
test:
name: On Cluster Test
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: imjasonh/setup-ko@v0.6
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Deploy Test Git Server
run: ./hack/install-git-server.sh
- name: E2E On Cluster Test
env:
E2E_RUNTIMES: ""
E2E_REGISTRY_URL: registry.default.svc.cluster.local:5000
FUNC_REPO_REF: ${{ github.event.pull_request.head.repo.full_name }}
FUNC_REPO_BRANCH_REF: ${{ github.head_ref }}
run: make test-e2e-on-cluster
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: e2e-tests

View File

@ -1,78 +0,0 @@
name: Func E2E Lifecycle Test
on: [pull_request]
concurrency:
group: ci-e1e-${{ github.ref }}-1
cancel-in-progress: true
jobs:
test:
name: E2E Test
continue-on-error: true
strategy:
matrix:
os: [ "ubuntu-latest", "ubuntu-24.04-arm" ]
runtime: ["go", "quarkus"]
include:
- os: ubuntu-latest
runtime: node
- os: ubuntu-latest
runtime: typescript
- os: ubuntu-latest
runtime: springboot
- os: ubuntu-latest
runtime: rust
- os: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Install Binaries
env:
ARCH: ${{ matrix.arch }}
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt for ${{matrix.runtime}} ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: Build
run: make
- name: E2E runtime for ${{ matrix.runtime }}
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt for ${{matrix.runtime}} ------------------"
make test-e2e-runtime runtime=${{ matrix.runtime }} && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
done
echo "------------------ finished! attempt $attempt ------------------"

View File

@ -1,47 +0,0 @@
name: Func E2E Test
on: [pull_request]
jobs:
test:
name: E2E Test
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: E2E Test
run: make test-e2e
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: e2e-tests

View File

@ -1,21 +0,0 @@
name: Func Check Embedded FS
on: [pull_request]
jobs:
test:
name: Func Check Embedded FS
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Check embedded templates content
run: |
if ! make check-embedded-fs; then
echo "Content of templates directory and embedded FS (zz_filesystem_generated.go) doesn't match!"
echo "Consult https:.github.com/knative/func/blob/main/docs/CONTRIBUTING.md#templates ."
exit 1
fi

View File

@ -1,84 +0,0 @@
name: Func Integration Test
on: [pull_request]
jobs:
test:
name: Integration Test
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Remove Unnecessary Software
run: |
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /opt/ghc || true
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
echo "TEKTON_TESTS_ENABLED=1" >> "$GITHUB_ENV"
echo "GITLAB_TESTS_ENABLED=1" >> "$GITHUB_ENV"
echo "GITLAB_HOSTNAME=gitlab.localtest.me" >> "$GITHUB_ENV"
echo "GITLAB_ROOT_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32})" >> "$GITHUB_ENV"
echo "PAC_CONTROLLER_HOSTNAME=pac-ctr.localtest.me" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: imjasonh/setup-ko@v0.6
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Install Gitlab
run: ./hack/install-gitlab.sh
- name: Patch Hosts
run: ./hack/patch-hosts.sh
- name: Integration Test
env:
FUNC_REPO_REF: ${{ github.event.pull_request.head.repo.full_name }}
FUNC_REPO_BRANCH_REF: ${{ github.head_ref }}
run: make test-integration
- name: Dump Cluster Logs
if: always()
run: |
echo "::group::cluster events" >> cluster_log.txt
kubectl get events -A >> cluster_log.txt 2>&1
echo "::endgroup::" >> cluster_log.txt
echo "::group::cluster containers logs" >> cluster_log.txt
stern '.*' --all-namespaces --no-follow >> cluster_log.txt 2>&1
echo "::endgroup::" >> cluster_log.txt
- name: "Archive log results"
if: always()
uses: actions/upload-artifact@v4
with:
name: cluster-logs
path: ./cluster_log.txt
retention-days: 7
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: integration-tests

View File

@ -1,85 +0,0 @@
name: Func Podman Next Test
on:
schedule:
- cron: '0 2 * * *'
jobs:
test:
name: Podman Next Test
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Remove Unnecessary Software
run: |
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /opt/ghc || true
- name: Install Podman Next (Nightly Build)
env:
FEDORA_RELEASE: 41
BASE_ARCH: x86_64
run: |
sudo apt update
sudo mkdir -p /etc/yum.repos.d
sudo apt install dnf -y
sudo apt install dnf-plugins-core -y
sudo apt install alien -y
sudo touch /etc/yum.repos.d/fedora.repo
sudo chmod 666 /etc/yum.repos.d/fedora.repo
cat << EOF >> /etc/yum.repos.d/fedora.repo
[fedora]
name=Fedora $FEDORA_RELEASE
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$FEDORA_RELEASE&arch=$BASE_ARCH
enabled=1
countme=1
metadata_expire=7d
repo_gpgcheck=0
type=rpm
gpgcheck=0
EOF
sudo chmod 644 /etc/yum.repos.d/fedora.repo
sudo dnf copr enable rhcontainerbot/podman-next fedora-$FEDORA_RELEASE-$BASE_ARCH --releasever=$FEDORA_RELEASE -y
sudo dnf download crun conmon podman --releasever=$FEDORA_RELEASE -y
sudo alien --to-deb $(ls -1 crun*.rpm) --install
sudo alien --to-deb $(ls -1 conmon*.rpm) --install
sudo alien --to-deb $(ls -1 podman*.rpm) --install
podman info
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Integration Test Podman
env:
FUNC_REPO_REF: ${{ github.event.pull_request.head.repo.full_name }}
FUNC_REPO_BRANCH_REF: ${{ github.head_ref }}
run: ./hack/test-integration-podman.sh

View File

@ -1,54 +0,0 @@
name: Func Podman Test
on: [pull_request]
jobs:
test:
name: Podman Test
strategy:
matrix:
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Set Environment Variables
run: |
echo "KUBECONFIG=${{ github.workspace }}/hack/bin/kubeconfig.yaml" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/hack/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Install Podman
run: |
sudo apt update
sudo apt install podman -y
podman info
- name: Install Binaries
run: ./hack/install-binaries.sh
- name: Allocate Cluster
run: |
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]
do
attempt=$((attempt+1))
echo "------------------ Attempt $attempt ------------------"
./hack/allocate.sh && break
echo "------------------ failed, retrying... ------------------"
if [ $attempt -ge $max_attempts ]; then
echo "------------------ max # of retries reached, exiting ------------------"
exit 1
fi
./hack/delete.sh
echo "------------------ sleep for 5 minutes ------------------"
sleep 300
done
echo "------------------ finished! attempt $attempt ------------------"
- name: Local Registry
run: ./hack/registry.sh
- name: Setup testing images
run: ./hack/setup-testing-images.sh
- name: Integration Test Podman
env:
FUNC_REPO_REF: ${{ github.event.pull_request.head.repo.full_name }}
FUNC_REPO_BRANCH_REF: ${{ github.head_ref }}
run: ./hack/test-integration-podman.sh

View File

@ -1,50 +0,0 @@
name: Func Unit Test
on: [pull_request]
jobs:
test:
name: Unit Test
strategy:
matrix:
java: [21]
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Install Bash 4 on Mac
if: matrix.os == 'macos-latest'
run: |
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew update
brew install bash
brew install gnu-sed
echo "/usr/local/bin" >> $GITHUB_PATH
echo "$(brew --prefix)/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
- run: git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Core Unit Tests
run: make test
env:
FUNC_REPO_REF: ${{ github.event.pull_request.head.repo.full_name }}
FUNC_REPO_BRANCH_REF: ${{ github.head_ref }}
- name: Template Unit Tests on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
python3 -m venv ${{ github.workspace }}/.venv
. ${{ github.workspace }}/.venv/bin/activate
make test-templates
- name: Template Unit Tests
if: matrix.os != 'ubuntu-latest'
run: make test-templates
- uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
flags: unit-tests

View File

@ -1,31 +0,0 @@
name: Update builder-jammy-full image
on:
schedule:
- cron: '0 * * * *'
jobs:
build-and-push-image:
permissions:
contents: read
packages: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Build and Push
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
docker run -d -p 5000:5000 --name registry registry:2.7
echo '{"insecure-registries" : "localhost:5000" }' | \
sudo tee /etc/docker/daemon.json
mkdir -p "$HOME/.config/containers/"
echo -e '\n[[registry]]\nlocation = "localhost:5000"\ninsecure = true\n' >> \
"$HOME/.config/containers/registries.conf"
skopeo login ghcr.io -u gh-action -p "$GITHUB_TOKEN"
docker login ghcr.io -u gh-action -p "$GITHUB_TOKEN"
make __update-builder

View File

@ -1,26 +0,0 @@
name: Update CA bundle in embedded templates
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */4 * * *'
jobs:
update:
name: Update CA bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install NPM deps.
run: npm install octokit@3.2.1
- name: Create PR
env:
GITHUB_TOKEN: ${{ github.token }}
run: node ./hack/update-ca-bundle.js

View File

@ -1,31 +0,0 @@
name: Update Quarkus Platform in embedded templates
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */4 * * *'
jobs:
update:
name: Update Quarkus Platform
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'temurin'
- name: Install NPM deps.
run: npm install xml2js octokit@3.2.1
- name: Create PR
env:
GITHUB_TOKEN: ${{ github.token }}
run: node ./hack/update-quarkus-platform.js

View File

@ -1,31 +0,0 @@
name: Update Spring Boot Platform in embedded templates
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */4 * * *'
jobs:
update:
name: Update Spring Boot Platform
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: knative/actions/setup-go@main
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'temurin'
- name: Install NPM deps.
run: npm install xml2js octokit@3.2.1 yaml semver
- name: Create PR
env:
GITHUB_TOKEN: ${{ github.token }}
run: node ./hack/update-springboot-platform.js

42
.gitignore vendored
View File

@ -1,40 +1,16 @@
# Project
/func
/func_*
/cmd/func.yaml
/coverage.out
/coverage.txt
/.coverage
/bin
/target
/hack/bin
/.artifacts
/pkg/functions/testdata/migrations/*/.gitignore
/pkg/functions/testdata/default_home/go
/pkg/functions/testdata/default_home/.cache
/pkg/functions/testdata/migrations/*/.gitignore
# Go
/templates/go/cloudevents/go.sum
# JS
node_modules
/templates/typescript/cloudevents/build
/templates/go/events/go.sum
/templates/go/http/go.sum
/templates/typescript/events/build
/templates/typescript/http/build
/coverage.out
/bin
# Python
target
node_modules
__pycache__
/templates/python/cloudevents/.venv
/templates/python/http/.venv
/coverage.out
/bin
# E2E Tests
/e2e/testdata/default_home/go
/e2e/testdata/default_home/.cache
# Editors
.vscode
.idea
# Operating system temporary files
.DS_Store

View File

@ -1,17 +1,19 @@
version: "2"
#
# golangci-lint
#
# For defaults, see:
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
#
#
#
linters:
enable:
- unconvert
- prealloc
- bodyclose
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
issues:
exclude-rules:
- linters:
- staticcheck
# Error Text:
@ -23,15 +25,4 @@ linters:
# Name Type = "value"
# Name2 = "value2"
# )
text: 'SA9004:'
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
text: "SA9004:"

View File

@ -1,3 +0,0 @@
defaultBaseImage: gcr.io/distroless/static:nonroot
baseImageOverrides:
knative.dev/func/cmd/func: docker.io/library/alpine:latest

View File

@ -1,2 +0,0 @@
scan_exclude = [
]

View File

@ -1,16 +0,0 @@
{
"bumpFiles": [{
"filename": "version.txt",
"type": "plain-text"
}],
"types": [
{"type":"feat","section":"Features"},
{"type":"fix","section":"Bug Fixes"},
{"type":"chore","section":"Miscellaneous"},
{"type":"docs","section":"Documentation"},
{"type":"refactor","section":"Miscellaneous"}
],
"skip": {
"tag": true
}
}

View File

@ -1,7 +0,0 @@
# This is the list of func authors for copyright purposes.
#
# This does not necessarily list everyone who has contributed code, since in
# some cases, their employer may be the copyright holder. To see the full list
# of contributors, see the revision history in source control.
Red Hat, Inc.

View File

@ -1,355 +1,7 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [0.26.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.25.0...v0.26.0) (2022-08-26)
### Features
* add OpenShift related Annotations & Labels ([#1106](https://github.com/knative-extensions/kn-plugin-func/issues/1106)) ([b4b4cc3](https://github.com/knative-extensions/kn-plugin-func/commit/b4b4cc34c3b32ad5f00d90ee8244702bef9e3673))
* add runtime icons on OpenShift ([#1116](https://github.com/knative-extensions/kn-plugin-func/issues/1116)) ([a7671e4](https://github.com/knative-extensions/kn-plugin-func/commit/a7671e45a791b88bed464becc79d419ed8eb803c))
* always store namespace in func.yaml and warn if current ns & func.yaml ns is different ([#1118](https://github.com/knative-extensions/kn-plugin-func/issues/1118)) ([8cb7080](https://github.com/knative-extensions/kn-plugin-func/commit/8cb70808f59dc0a4e19fc7d1b2f1ff3389bed4f2))
* build for Linux ARM64 ([#1123](https://github.com/knative-extensions/kn-plugin-func/issues/1123)) ([819b433](https://github.com/knative-extensions/kn-plugin-func/commit/819b433edbb313f4119d88cdf066b48813333a27))
* deploy Tekton task supports optional `image` parameter ([#1140](https://github.com/knative-extensions/kn-plugin-func/issues/1140)) ([65c30ab](https://github.com/knative-extensions/kn-plugin-func/commit/65c30abb39e05eaa6b4ebf9ba25d5cdfc0971cee))
* func deploy accepts image digest in --image ([#1098](https://github.com/knative-extensions/kn-plugin-func/issues/1098)) ([c57af36](https://github.com/knative-extensions/kn-plugin-func/commit/c57af36f7413e670d44f5111acd477e733aea2e4))
* languages list command ([#1114](https://github.com/knative-extensions/kn-plugin-func/issues/1114)) ([d1f935f](https://github.com/knative-extensions/kn-plugin-func/commit/d1f935fde991639c80b369797733e9e48b82805e))
* persist builder value in func.yaml ([#1099](https://github.com/knative-extensions/kn-plugin-func/issues/1099)) ([b1fd9f7](https://github.com/knative-extensions/kn-plugin-func/commit/b1fd9f71b16bd5d83d37cbaed47890775886d807))
* S2I strategy for on cluster build ([#1191](https://github.com/knative-extensions/kn-plugin-func/issues/1191)) ([1112aaa](https://github.com/knative-extensions/kn-plugin-func/commit/1112aaa2fe5fb5aa349251a34c2fccc529498b0d))
* templates list command ([#1134](https://github.com/knative-extensions/kn-plugin-func/issues/1134)) ([2f8d82b](https://github.com/knative-extensions/kn-plugin-func/commit/2f8d82bec283ad29e67bdeaef80f039d87f1a523))
### Bug Fixes
* add languages command to cmd root ([#1127](https://github.com/knative-extensions/kn-plugin-func/issues/1127)) ([59df756](https://github.com/knative-extensions/kn-plugin-func/commit/59df756e49eda6717d82caf8c8f54422fc79fcb0))
* correct error in OpenShift reg.cred.provider ([#1104](https://github.com/knative-extensions/kn-plugin-func/issues/1104)) ([a197f8b](https://github.com/knative-extensions/kn-plugin-func/commit/a197f8b330d6f5fdbc9c16585e605cdf74f9b8b9))
* ensure registry in func.yaml is respected ([#1168](https://github.com/knative-extensions/kn-plugin-func/issues/1168)) ([80657c9](https://github.com/knative-extensions/kn-plugin-func/commit/80657c90e3df49bd4a55c61ada0cbadbdff7a56d))
* image push authorization check ([#1109](https://github.com/knative-extensions/kn-plugin-func/issues/1109)) ([3b198cb](https://github.com/knative-extensions/kn-plugin-func/commit/3b198cb78164e751d293edc6467907cecd626643))
* image push authorization check ([#1130](https://github.com/knative-extensions/kn-plugin-func/issues/1130)) ([36216e7](https://github.com/knative-extensions/kn-plugin-func/commit/36216e7fed34d3cd20aed951a27ed5fdf995bafb))
* performance of template loading ([#1189](https://github.com/knative-extensions/kn-plugin-func/issues/1189)) ([dca11da](https://github.com/knative-extensions/kn-plugin-func/commit/dca11dad5bfefeb05b0d3ea2a88bc4f9b159a2b7))
* stop build progress loop when build completes ([#1133](https://github.com/knative-extensions/kn-plugin-func/issues/1133)) ([cf5be9a](https://github.com/knative-extensions/kn-plugin-func/commit/cf5be9a6161085a92f60d5345b15e8d394d69f0f))
* use 0 group id for func-buildpacks Task ([#1105](https://github.com/knative-extensions/kn-plugin-func/issues/1105)) ([f4537dd](https://github.com/knative-extensions/kn-plugin-func/commit/f4537dd3d5d5130c27ad8c1d1ff0df92569fbc25))
* use creds from creds store first ([#1103](https://github.com/knative-extensions/kn-plugin-func/issues/1103)) ([e1d5229](https://github.com/knative-extensions/kn-plugin-func/commit/e1d522990016f46beff26117f8b80ee16b098402))
* use space prefix for deploy output text ([#1144](https://github.com/knative-extensions/kn-plugin-func/issues/1144)) ([3b8c240](https://github.com/knative-extensions/kn-plugin-func/commit/3b8c24092b0e3c7863715ec9b96c8a86b10af245))
### Documentation
* replace commands.md with generated text file ([#1089](https://github.com/knative-extensions/kn-plugin-func/issues/1089)) ([df022f5](https://github.com/knative-extensions/kn-plugin-func/commit/df022f5c93911687e4356f4c58ccbb5d10b7a895))
### Miscellaneous
* add some milliseconds in client tests ([#1178](https://github.com/knative-extensions/kn-plugin-func/issues/1178)) ([3ac5d46](https://github.com/knative-extensions/kn-plugin-func/commit/3ac5d468bfdbb326375ebc5bc640729b59fec7f2))
* add validation for `builder` ([#1136](https://github.com/knative-extensions/kn-plugin-func/issues/1136)) ([71b0ddd](https://github.com/knative-extensions/kn-plugin-func/commit/71b0dddc556bcc6767354e66b25091eb001d0700))
* adjust codecov configuration ([#1177](https://github.com/knative-extensions/kn-plugin-func/issues/1177)) ([a52b7d5](https://github.com/knative-extensions/kn-plugin-func/commit/a52b7d5ecd895cd65e2844bc5b4d68974d8f3e6f))
* **deps:** bump github.com/containerd/containerd from 1.6.0 to 1.6.6 ([#1112](https://github.com/knative-extensions/kn-plugin-func/issues/1112)) ([7a760fb](https://github.com/knative-extensions/kn-plugin-func/commit/7a760fbf57925104b7d68dbc987183594cf0bc48))
* **deps:** update node/typescript deps in e2e tests ([#1119](https://github.com/knative-extensions/kn-plugin-func/issues/1119)) ([cccb283](https://github.com/knative-extensions/kn-plugin-func/commit/cccb2833d1ad8f77cfb200ef4f43a7e4098af92d))
* update node and typescript dependencies ([#1110](https://github.com/knative-extensions/kn-plugin-func/issues/1110)) ([29f3aad](https://github.com/knative-extensions/kn-plugin-func/commit/29f3aadb47ecd59a0783b2a20643b6963e82af2e))
* update Quarkus platform to 2.11.2.Final ([#1157](https://github.com/knative-extensions/kn-plugin-func/issues/1157)) ([849c2cd](https://github.com/knative-extensions/kn-plugin-func/commit/849c2cd7a187d37693ba963b6cd1fc9a0377ad4d))
* update Quarkus platform version to 2.11.3.Final ([#1187](https://github.com/knative-extensions/kn-plugin-func/issues/1187)) ([b3ced5e](https://github.com/knative-extensions/kn-plugin-func/commit/b3ced5ebd5353568a63bbbc0ecdd986ac52ad706))
* update Quarkus templates to 2.10.3.Final ([#1132](https://github.com/knative-extensions/kn-plugin-func/issues/1132)) ([a906b88](https://github.com/knative-extensions/kn-plugin-func/commit/a906b8866cb97894a676aeada9212df0e76a7ee0))
* update springboot dependencies ([#1183](https://github.com/knative-extensions/kn-plugin-func/issues/1183)) ([e465348](https://github.com/knative-extensions/kn-plugin-func/commit/e4653482101443ce81949c04f2c011b8470302a0))
* use dot as path default rather than absolute path ([#1184](https://github.com/knative-extensions/kn-plugin-func/issues/1184)) ([fecbc4e](https://github.com/knative-extensions/kn-plugin-func/commit/fecbc4ef8bc7a1e9007f8bc912af7d7a2cc9d0fa))
* use lower case *functions* in all CLI outputs ([#1135](https://github.com/knative-extensions/kn-plugin-func/issues/1135)) ([e659256](https://github.com/knative-extensions/kn-plugin-func/commit/e659256005cdc5231759beef2e706466820c129d))
* Use NodeJS for script instead of Shell ([#1170](https://github.com/knative-extensions/kn-plugin-func/issues/1170)) ([62b7232](https://github.com/knative-extensions/kn-plugin-func/commit/62b723263688336c4d5ff85d9652a2cd460d941a))
## [0.24.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.23.0...v0.24.0) (2022-06-01)
### Features
* configurable s2i builder images ([#1024](https://github.com/knative-extensions/kn-plugin-func/issues/1024)) ([096085d](https://github.com/knative-extensions/kn-plugin-func/commit/096085d751f105b5592a09c849fef364b84145b3))
* enable Paketo builders for Python functions ([#979](https://github.com/knative-extensions/kn-plugin-func/issues/979)) ([5af934b](https://github.com/knative-extensions/kn-plugin-func/commit/5af934be8d97b0de676fb7d81e07f7b07a33a9e3))
* expose default builder image logic for in-cluster builds ([#1021](https://github.com/knative-extensions/kn-plugin-func/issues/1021)) ([dc8abf1](https://github.com/knative-extensions/kn-plugin-func/commit/dc8abf179c63582d197e5a9add2006ad0c026ff6))
* improved invoke verbosity ([#1007](https://github.com/knative-extensions/kn-plugin-func/issues/1007)) ([867d4c2](https://github.com/knative-extensions/kn-plugin-func/commit/867d4c26d6dd0b4d06cf6428dc932004e20ca981))
* invoke verbose metadata ([#944](https://github.com/knative-extensions/kn-plugin-func/issues/944)) ([c3c1456](https://github.com/knative-extensions/kn-plugin-func/commit/c3c1456ede7b471a271d0680107657ac0e4f0568))
* make templates consistent across runtimes ([#948](https://github.com/knative-extensions/kn-plugin-func/issues/948)) ([13d4222](https://github.com/knative-extensions/kn-plugin-func/commit/13d4222461c553d5da1c8448713cba9e4a3e828a))
* s2i builder env var interpolation ([#991](https://github.com/knative-extensions/kn-plugin-func/issues/991)) ([1424831](https://github.com/knative-extensions/kn-plugin-func/commit/14248311b1254fe8601b394f064e2fdd92dd4ced))
* s2i builder quarkus support ([#993](https://github.com/knative-extensions/kn-plugin-func/issues/993)) ([397ce65](https://github.com/knative-extensions/kn-plugin-func/commit/397ce65598c1f61d2312c62016ed78453e1299a3))
* s2i builder typescript support ([#957](https://github.com/knative-extensions/kn-plugin-func/issues/957)) ([3be1a77](https://github.com/knative-extensions/kn-plugin-func/commit/3be1a77388647055dc67e3901da323c240dc77f4))
### Bug Fixes
* ignore `is forbidden` errors when deleting function and resources ([#988](https://github.com/knative-extensions/kn-plugin-func/issues/988)) ([ce26a23](https://github.com/knative-extensions/kn-plugin-func/commit/ce26a23352513747cad25b7668def43edff6f0fe))
* ignore node_modules for s2i builds ([#1019](https://github.com/knative-extensions/kn-plugin-func/issues/1019)) ([1d367c6](https://github.com/knative-extensions/kn-plugin-func/commit/1d367c6be50d36e045b475c7ff173001b1eaa3d0))
* read pwd from non-tty input ([#996](https://github.com/knative-extensions/kn-plugin-func/issues/996)) ([e9932cd](https://github.com/knative-extensions/kn-plugin-func/commit/e9932cdf43eb560ff496a98f83766d1c3e1fdc96))
* update various doc links ([#980](https://github.com/knative-extensions/kn-plugin-func/issues/980)) ([bc6383e](https://github.com/knative-extensions/kn-plugin-func/commit/bc6383e55c89349e8e25a79e0f90be1760818e50))
### Documentation
* add a language pack "contract" document ([#918](https://github.com/knative-extensions/kn-plugin-func/issues/918)) ([76c647a](https://github.com/knative-extensions/kn-plugin-func/commit/76c647a1c72f57efa8fad89c6802796d02254bd0))
### Miscellaneous
* add release process and func_darwin_arm64 to the release artifacts ([#945](https://github.com/knative-extensions/kn-plugin-func/issues/945)) ([4e369a0](https://github.com/knative-extensions/kn-plugin-func/commit/4e369a013ac3109bfea06dc730082b22f6d1cf36))
* add version.txt to .gitattributes for style ([#966](https://github.com/knative-extensions/kn-plugin-func/issues/966)) ([23d1188](https://github.com/knative-extensions/kn-plugin-func/commit/23d118831923bf798c5d839c276382f48c048a32))
* clean Repository and Runtimes structs ([#973](https://github.com/knative-extensions/kn-plugin-func/issues/973)) ([e502d55](https://github.com/knative-extensions/kn-plugin-func/commit/e502d554c8aafacc0f4d75bb7f6e1aef57e218fa))
* **deps:** bump faas-js-runtime version ([#1000](https://github.com/knative-extensions/kn-plugin-func/issues/1000)) ([83c081d](https://github.com/knative-extensions/kn-plugin-func/commit/83c081d34a08b4775787c61eb00f9ca39cfcd33d))
* templates ([#961](https://github.com/knative-extensions/kn-plugin-func/issues/961)) ([34cb893](https://github.com/knative-extensions/kn-plugin-func/commit/34cb893545a5f74a120783f66bb3a37d2b283d64))
* update allocate script with latest knative ([#965](https://github.com/knative-extensions/kn-plugin-func/issues/965)) ([4ffb1f9](https://github.com/knative-extensions/kn-plugin-func/commit/4ffb1f9cba3ee1bac248a033ece6e8473965f7a7))
* use paketo builders for all runtimes ([#1001](https://github.com/knative-extensions/kn-plugin-func/issues/1001)) ([31c1d66](https://github.com/knative-extensions/kn-plugin-func/commit/31c1d66eb3f0089541f8219f07722c75c8a82692))
# Change Log
<a name="unreleased"></a>
---
### [0.23.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.23.0...v0.22.0) (2022-04-06)
### Features
* command help text template preprocessing ([#875](https://github.com/knative-extensions/kn-plugin-func/issues/875)) ([2bd5254](https://github.com/knative-extensions/kn-plugin-func/commit/2bd5254f19a14d6aae9bd4a4b59971ef36e96fad))
* invoke verbose metadata ([#944](https://github.com/knative-extensions/kn-plugin-func/issues/944)) ([c3c1456](https://github.com/knative-extensions/kn-plugin-func/commit/c3c1456ede7b471a271d0680107657ac0e4f0568))
* on cluster build doens't require privileged cluster permissions ([#934](https://github.com/knative-extensions/kn-plugin-func/issues/934)) ([e9251f5](https://github.com/knative-extensions/kn-plugin-func/commit/e9251f518cc806768f0221f11e39f04fa4619537))
* s2i builder with preliminary node support ([#923](https://github.com/knative-extensions/kn-plugin-func/issues/923)) ([a91bcc5](https://github.com/knative-extensions/kn-plugin-func/commit/a91bcc5fcfe66948c86ce3e33cf0d28230536f1c)), closes [#921](https://github.com/knative-extensions/kn-plugin-func/issues/921)
### Bug Fixes
* apply updated spring-boot-function dependency ([#936](https://github.com/knative-extensions/kn-plugin-func/issues/936)) ([4a4cebb](https://github.com/knative-extensions/kn-plugin-func/commit/4a4cebb1ea7226e7d7c1dbfb9e3fa8e5ec22c31d))
* bind verbose flag to root ([#884](https://github.com/knative-extensions/kn-plugin-func/issues/884)) ([25524a1](https://github.com/knative-extensions/kn-plugin-func/commit/25524a1f8435cd310b45f283e987eee7a8736ceb))
* full clone of template repos on add ([#904](https://github.com/knative-extensions/kn-plugin-func/issues/904)) ([564a34b](https://github.com/knative-extensions/kn-plugin-func/commit/564a34b3f53381bdd59262dcb78d2953f973c8bb))
* minor typos in docs ([#862](https://github.com/knative-extensions/kn-plugin-func/issues/862)) ([efc3b20](https://github.com/knative-extensions/kn-plugin-func/commit/efc3b208cb5ab76f1eb73801501bcbfc23f16928))
* use full root name for cmd help prefixes ([#873](https://github.com/knative-extensions/kn-plugin-func/issues/873)) ([3f30c91](https://github.com/knative-extensions/kn-plugin-func/commit/3f30c91116344b592bf392e92b63cb845b25428a))
### Miscellaneous
* add Apple M1 build in cross-platform target ([#932](https://github.com/knative-extensions/kn-plugin-func/issues/932)) ([00d5a82](https://github.com/knative-extensions/kn-plugin-func/commit/00d5a8272284ea40ebeefa4f22f12c2d375aadae))
---
## [0.22.0](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.21.2...v0.22.0) (2022-02-22)
### Chore
- Bump Node.js builder image to the latest paketo builder, removing a non-fatal warning that was issued at function startup. (#[8](https://github.com/knative-extensions/kn-plugin-func/runs/5297690460?check_suite_focus=true#step:6:8)26, @lance)
- Update boson builder images to most recent versions (#8[10](https://github.com/knative-extensions/kn-plugin-func/runs/5297690460?check_suite_focus=true#step:6:10), @matejvasek)
### Enhancement
- Adds the --build flag for 'func deploy' to the shell completions (#802, @matejvasek)
### Documentation
- Clarify podman requirements on Linux vs. MacOS and Linux in podman.md guide (#836, @matejvasek)
### Bug or Regression
- Fix a bug where interactive prompt defaults were not being used (#821, @lkingland)
- Fixes a bug during func create when confirm option is used with the go language runtime (#8[15](https://github.com/knative-extensions/kn-plugin-func/runs/5297690460?check_suite_focus=true#step:6:15), @senthilnathan)
- Fixes a bug where the invoke and describe commands could fail if Knative Eventing is not installed on the cluster. (#8[23](https://github.com/knative-extensions/kn-plugin-func/runs/5297690460?check_suite_focus=true#step:6:23), @lance)
- Fixes missing `cloudevent` invocation format for Node.js, SpringBoot and Rust CloudEvent templates (#846, @lance)
### Uncategorized
- Detects when deploying to OpenShift and use internal registry (#8[25](https://github.com/knative-extensions/kn-plugin-func/runs/5297690460?check_suite_focus=true#step:6:25), @matejvasek)
---
## [0.21.2](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.21.1...v0.21.2) (2022-01-28)
## What's Changed
* backport: bug fixes for 0.21 by @lance in https://github.com/knative-extensions/kn-plugin-func/pull/793
**Full Changelog**: https://github.com/knative-extensions/kn-plugin-func/compare/v0.21.1...v0.21.2
---
## [0.21.1](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.21.0...v0.21.1) (2022-01-27)
### Enhancement
- Adds a label `function.knative.dev/name: functionName` to every resouce created for a Function ([#757](https://github.com/knative-extensions/kn-plugin-func/pull/757), [@zroubalik](https://github.com/zroubalik))
- Adds the ability to build a Function on the cluster using Tekton Pipelines. The build on the cluster is enabled by fetching Function source code from a remote Git repository. ([#743](https://github.com/knative-extensions/kn-plugin-func/pull/743), [@zroubalik](https://github.com/zroubalik))
### Bug or Regression
- Changes the springboot function templates to use the base builder instead of the tiny builder. ([#792](https://github.com/knative-extensions/kn-plugin-func/pull/792), [@lance](https://github.com/lance))
**Full Changelog**: https://github.com/knative-extensions/kn-plugin-func/compare/v0.21.0...v0.21.1
---
## [0.21.0](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.20.0...v0.21.0) (2022-01-12)
### Features
* add possibility to disable pushing of image in `deploy` command ([#736](https://www.github.com/knative-extensions/kn-plugin-func/issues/736)) ([4e5a5e8](https://www.github.com/knative-extensions/kn-plugin-func/commit/4e5a5e830799b73f65aba8ee248a52bf1c643acb))
* add possibility to disable pushing of image in `deploy` command ([#739](https://www.github.com/knative-extensions/kn-plugin-func/issues/739)) ([64ba17b](https://www.github.com/knative-extensions/kn-plugin-func/commit/64ba17b4fbe9033e8279fa34aeebdb12edcee25d))
## [0.20.0](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.19.0...v0.20.0) (2021-12-20)
### ⚠ BREAKING CHANGES
* use `function.knative.dev` for Functions related labels (#717)
### Features
* add flag to push image at the end of a successful build ([#681](https://www.github.com/knative-extensions/kn-plugin-func/issues/681)) ([2f24182](https://www.github.com/knative-extensions/kn-plugin-func/commit/2f241824ff3a2664a987fe742aed2f0b56aeb9ab))
* add POD_NAME as an environment variable ([#660](https://www.github.com/knative-extensions/kn-plugin-func/issues/660)) ([64473b7](https://www.github.com/knative-extensions/kn-plugin-func/commit/64473b7197bb5a821b6724a8b914784891b1a828))
* add telemetry to Node.js and TypeScript function templates ([#719](https://www.github.com/knative-extensions/kn-plugin-func/issues/719)) ([d7cfe6e](https://www.github.com/knative-extensions/kn-plugin-func/commit/d7cfe6ead76f15c4bcd34a132d0c17c02a149548))
* allow build to be triggered from run when fn.Image is missing ([#644](https://www.github.com/knative-extensions/kn-plugin-func/issues/644)) ([b190b52](https://www.github.com/knative-extensions/kn-plugin-func/commit/b190b527542bf659f06bf931d94d09542d012c36))
* allow push to cluster internal registries ([#718](https://www.github.com/knative-extensions/kn-plugin-func/issues/718)) ([8d51393](https://www.github.com/knative-extensions/kn-plugin-func/commit/8d51393181adca0c74a4b08cfb2dc2da390f983b))
* automatically start podman service ([#648](https://www.github.com/knative-extensions/kn-plugin-func/issues/648)) ([bfdfb76](https://www.github.com/knative-extensions/kn-plugin-func/commit/bfdfb760cff575146764f9b841a0cafcb31bcd58))
* custom default HTTP transport ([#711](https://www.github.com/knative-extensions/kn-plugin-func/issues/711)) ([a13f897](https://www.github.com/knative-extensions/kn-plugin-func/commit/a13f897fbb996dbfcb2120965745477321087a9c))
* ensure config and repos path exists ([#683](https://www.github.com/knative-extensions/kn-plugin-func/issues/683)) ([db9ad07](https://www.github.com/knative-extensions/kn-plugin-func/commit/db9ad07c7048361946a8c7d45c549323eee44a58))
* function creation timestamp ([#651](https://www.github.com/knative-extensions/kn-plugin-func/issues/651)) ([1bf17ec](https://www.github.com/knative-extensions/kn-plugin-func/commit/1bf17ec976130551da366e75b38f5169b3daed4e))
* function version migrations ([#664](https://www.github.com/knative-extensions/kn-plugin-func/issues/664)) ([ccf0015](https://www.github.com/knative-extensions/kn-plugin-func/commit/ccf00152be0ceba1794267f8e03a09cb32fee514))
* In cluster dialer to proxy TCP connections to unexposed services ([#688](https://www.github.com/knative-extensions/kn-plugin-func/issues/688)) ([98ef5a0](https://www.github.com/knative-extensions/kn-plugin-func/commit/98ef5a00356a5b93ef1a6c581ae8d5ba86ee09e4))
* make SpringBoot template SpringNative by default ([#649](https://www.github.com/knative-extensions/kn-plugin-func/issues/649)) ([c70a21e](https://www.github.com/knative-extensions/kn-plugin-func/commit/c70a21e9a459d726a4118e177835082323698f83))
* use `function.knative.dev` for Functions related labels ([#717](https://www.github.com/knative-extensions/kn-plugin-func/issues/717)) ([feaf8f9](https://www.github.com/knative-extensions/kn-plugin-func/commit/feaf8f91091afc21bcc3e99ed3098c9ff0679883))
### Bug Fixes
* make registry validation work again ([#690](https://www.github.com/knative-extensions/kn-plugin-func/issues/690)) ([10f2cf4](https://www.github.com/knative-extensions/kn-plugin-func/commit/10f2cf44c74884b4585114affd3c05cfba4f7613))
* move integration port to unregistered range ([#701](https://www.github.com/knative-extensions/kn-plugin-func/issues/701)) ([f63af0d](https://www.github.com/knative-extensions/kn-plugin-func/commit/f63af0d34e97a549df51c47f12e22a7558504278))
* remove stray manifest.yaml files ([#628](https://www.github.com/knative-extensions/kn-plugin-func/issues/628)) ([c810efc](https://www.github.com/knative-extensions/kn-plugin-func/commit/c810efc7a4eb0d87f37f3acfafc46c1e4639fdc4))
* remove template manifest from final Function ([#703](https://www.github.com/knative-extensions/kn-plugin-func/issues/703)) ([79ad65d](https://www.github.com/knative-extensions/kn-plugin-func/commit/79ad65ddf42b1bfffacd3f6fe4d606885b1a3766))
* revert hostname alias ([#712](https://www.github.com/knative-extensions/kn-plugin-func/issues/712)) ([07062c1](https://www.github.com/knative-extensions/kn-plugin-func/commit/07062c144aa19e4eb4c4ef27d5c5cb1ebb8eb185))
* schema-generate should point to `function.go` ([#677](https://www.github.com/knative-extensions/kn-plugin-func/issues/677)) ([c7d18c8](https://www.github.com/knative-extensions/kn-plugin-func/commit/c7d18c89edaf0a4fcb5c7b684e5e151c217430c2))
* use default socket path for TCP connections ([#669](https://www.github.com/knative-extensions/kn-plugin-func/issues/669)) ([ee96bef](https://www.github.com/knative-extensions/kn-plugin-func/commit/ee96bef9aea34c9370cf1f871bc4c558ed449a13))
* use specific version of paketo builder ([#670](https://www.github.com/knative-extensions/kn-plugin-func/issues/670)) ([834e8ae](https://www.github.com/knative-extensions/kn-plugin-func/commit/834e8ae46c833d2052171dc8dde23648a1da5112))
## [0.19.0](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.18.0...v0.19.0) (2021-11-03)
### ⚠ BREAKING CHANGES
* rename event templates to 'cloudevents' (#584)
### Features
* add support for manifest.yaml at repo/language/template levels ([#558](https://www.github.com/knative-extensions/kn-plugin-func/issues/558)) ([e319ea3](https://www.github.com/knative-extensions/kn-plugin-func/commit/e319ea3b62150ea2939876cb078ce150b7580bdf))
* allow developers to provide Build Envs to buildpacks in `func.yaml` ([#571](https://www.github.com/knative-extensions/kn-plugin-func/issues/571)) ([114a5fa](https://www.github.com/knative-extensions/kn-plugin-func/commit/114a5faee3ab9ae1ec37ae5c4375c6218405c3b7))
* create cli ([#547](https://www.github.com/knative-extensions/kn-plugin-func/issues/547)) ([4fe9fdc](https://www.github.com/knative-extensions/kn-plugin-func/commit/4fe9fdcab08552814c86d85194c552b591f52cd7))
* Improve build performance ([#569](https://www.github.com/knative-extensions/kn-plugin-func/issues/569)) ([ef7b986](https://www.github.com/knative-extensions/kn-plugin-func/commit/ef7b986900e1eb6afd4203067aeac3dc99a4ede5))
* periodically update progress during build ([#537](https://www.github.com/knative-extensions/kn-plugin-func/issues/537)) ([01689e7](https://www.github.com/knative-extensions/kn-plugin-func/commit/01689e7c131dd79db1e469c3ce54bd011464a6ef))
* rename event templates to 'cloudevents' ([#584](https://www.github.com/knative-extensions/kn-plugin-func/issues/584)) ([68b0904](https://www.github.com/knative-extensions/kn-plugin-func/commit/68b0904b17dee5bc6a5ec71132d35c45d52f4b71))
* Save password after user input ([#560](https://www.github.com/knative-extensions/kn-plugin-func/issues/560)) ([e85a4aa](https://www.github.com/knative-extensions/kn-plugin-func/commit/e85a4aa7f38681eb7554cac818eab7b1ca3bded1))
* ssh connection to remote docker daemon ([#594](https://www.github.com/knative-extensions/kn-plugin-func/issues/594)) ([e1f164d](https://www.github.com/knative-extensions/kn-plugin-func/commit/e1f164d2ca6b0e720ebcf881b8d5fd123dfd0d82))
* validation for registry/namespace to not contain image name ([#601](https://www.github.com/knative-extensions/kn-plugin-func/issues/601)) ([cf9596c](https://www.github.com/knative-extensions/kn-plugin-func/commit/cf9596c83e6eae8229ca61a18fafb5ba4df31d6b))
### Bug Fixes
* ConfigMap/Secret key validation ([#623](https://www.github.com/knative-extensions/kn-plugin-func/issues/623)) ([0ed1e81](https://www.github.com/knative-extensions/kn-plugin-func/commit/0ed1e816920b3e750c0aa0ed25fd1e0308e80a2e))
* Environment values -> Environment variables ([#622](https://www.github.com/knative-extensions/kn-plugin-func/issues/622)) ([ac9de9d](https://www.github.com/knative-extensions/kn-plugin-func/commit/ac9de9dfc68969ecb825c9ee2a92aa9918ae5ea7))
* hide a fmt.Println behind verbose flag ([#538](https://www.github.com/knative-extensions/kn-plugin-func/issues/538)) ([ad4607b](https://www.github.com/knative-extensions/kn-plugin-func/commit/ad4607bd50ae0c41ba0792d46318757089239de4))
* improve error message when invalid function name is used ([#567](https://www.github.com/knative-extensions/kn-plugin-func/issues/567)) ([0e3c676](https://www.github.com/knative-extensions/kn-plugin-func/commit/0e3c6764ef716cf24a3f60676e139d0c61161693))
* registry URL comparison ([#549](https://www.github.com/knative-extensions/kn-plugin-func/issues/549)) ([b10c484](https://www.github.com/knative-extensions/kn-plugin-func/commit/b10c48453cc5817c4c28077be13fc03baee5d818))
* stop the progress ticker after build completes ([#544](https://www.github.com/knative-extensions/kn-plugin-func/issues/544)) ([4f3e5fd](https://www.github.com/knative-extensions/kn-plugin-func/commit/4f3e5fdb7a40a3419d8d731d5a0c916b81af069b))
* update-pkger.sh sed error on osX ([#541](https://www.github.com/knative-extensions/kn-plugin-func/issues/541)) ([25f8b4d](https://www.github.com/knative-extensions/kn-plugin-func/commit/25f8b4d6ead2f47c3ab6541e2bdb5016b4a423aa))
## [0.18.0](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.17.1...v0.18.0) (2021-09-16)
### ⚠ BREAKING CHANGES
* change `describe` command to `info` (#474)
* use key&value for Labels (#472)
### Features
* allow language packs to set function metadata ([#465](https://www.github.com/knative-extensions/kn-plugin-func/issues/465)) ([48f40c3](https://www.github.com/knative-extensions/kn-plugin-func/commit/48f40c35e3a239d09d6a87fc4603ad21db46bc37))
* builders/buildpacks configured in client ([#495](https://www.github.com/knative-extensions/kn-plugin-func/issues/495)) ([668804e](https://www.github.com/knative-extensions/kn-plugin-func/commit/668804e53e76ce153a887289efb2b05f88203a1f))
* change `describe` command to `info` ([#474](https://www.github.com/knative-extensions/kn-plugin-func/issues/474)) ([10a0757](https://www.github.com/knative-extensions/kn-plugin-func/commit/10a07578e9f6ab6bbbb8028633b37e3400fd22bb))
* client effective runtimes list ([#490](https://www.github.com/knative-extensions/kn-plugin-func/issues/490)) ([e0aad6f](https://www.github.com/knative-extensions/kn-plugin-func/commit/e0aad6f936067892e04a463f85ca46689714716c))
* generate json schema for func.yaml ([#460](https://www.github.com/knative-extensions/kn-plugin-func/issues/460)) ([8939f89](https://www.github.com/knative-extensions/kn-plugin-func/commit/8939f89beae7d5b2f66bc18b921ca3059f89e629))
* make func schema if config updated ([#468](https://www.github.com/knative-extensions/kn-plugin-func/issues/468)) ([6ae2157](https://www.github.com/knative-extensions/kn-plugin-func/commit/6ae215754930c8a1e1dc4b5cd0b8ef3d99bb2893))
* move go, typescript and nodejs to paketo builders ([#485](https://www.github.com/knative-extensions/kn-plugin-func/issues/485)) ([a4b15ad](https://www.github.com/knative-extensions/kn-plugin-func/commit/a4b15ad9926112910251a8d74747e2db368c86e9))
* repository and templates client api ([#475](https://www.github.com/knative-extensions/kn-plugin-func/issues/475)) ([3f56a8f](https://www.github.com/knative-extensions/kn-plugin-func/commit/3f56a8fd7a66b923294043bcaa68ad59b1228831))
* repository management cli ([#514](https://www.github.com/knative-extensions/kn-plugin-func/issues/514)) ([ae638c3](https://www.github.com/knative-extensions/kn-plugin-func/commit/ae638c349c46c035bad74645bfc612380c871a85))
* repository management client api ([#467](https://www.github.com/knative-extensions/kn-plugin-func/issues/467)) ([9fd2475](https://www.github.com/knative-extensions/kn-plugin-func/commit/9fd247557ae8ee30cc7c5f0107d80fa72fbe8086))
* use key&value for Labels ([#472](https://www.github.com/knative-extensions/kn-plugin-func/issues/472)) ([5569681](https://www.github.com/knative-extensions/kn-plugin-func/commit/55696811e317a51767e09acab3d4d4e2abc6e982))
### Bug Fixes
* `build` should honor registry specified in `-r` ([#510](https://www.github.com/knative-extensions/kn-plugin-func/issues/510)) ([8aba038](https://www.github.com/knative-extensions/kn-plugin-func/commit/8aba038073f5584133eb3d08ba85289800e2e770))
* `config labels` panic ([#493](https://www.github.com/knative-extensions/kn-plugin-func/issues/493)) ([f2efbe5](https://www.github.com/knative-extensions/kn-plugin-func/commit/f2efbe5b42a6e0af36ecc6be429a630312e0c6e5))
* better cleanup before pkger run ([#479](https://www.github.com/knative-extensions/kn-plugin-func/issues/479)) ([25b1d63](https://www.github.com/knative-extensions/kn-plugin-func/commit/25b1d63b9c1b332e1d59e494af83bdc3a1f576e9))
* control chars on progress listener for Windows OS ([#498](https://www.github.com/knative-extensions/kn-plugin-func/issues/498)) ([1172a85](https://www.github.com/knative-extensions/kn-plugin-func/commit/1172a85c80f834ff3958073bc36ff4a5173c9de6))
* enable healt checks for Quarkus ([#477](https://www.github.com/knative-extensions/kn-plugin-func/issues/477)) ([72a1cf8](https://www.github.com/knative-extensions/kn-plugin-func/commit/72a1cf885e092340295cc6ace3580e7420640cda))
* fast-fail on create if Function already exists ([#496](https://www.github.com/knative-extensions/kn-plugin-func/issues/496)) ([25f7007](https://www.github.com/knative-extensions/kn-plugin-func/commit/25f7007300c020b5a1d336740a2bbc2f546bf3da))
* regenerate pkged.go ([#478](https://www.github.com/knative-extensions/kn-plugin-func/issues/478)) ([c7b3af4](https://www.github.com/knative-extensions/kn-plugin-func/commit/c7b3af41b8cac0b9edfb96d3a01230d2606e320a))
* removal of repositories ([#524](https://www.github.com/knative-extensions/kn-plugin-func/issues/524)) ([90c60b6](https://www.github.com/knative-extensions/kn-plugin-func/commit/90c60b693d6b2dbb2c8edee27a7cf7b6e8d1c399))
* support nested subdirs in remote templates ([#482](https://www.github.com/knative-extensions/kn-plugin-func/issues/482)) ([fcf9e77](https://www.github.com/knative-extensions/kn-plugin-func/commit/fcf9e77cb93808d28d0c60f3a0959fac605771fb))
* use full image names ([#535](https://www.github.com/knative-extensions/kn-plugin-func/issues/535)) ([16ee28c](https://www.github.com/knative-extensions/kn-plugin-func/commit/16ee28c83debcc19092abb250ef20354eca09710))
### [0.17.1](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.17.0...v0.17.1) (2021-08-05)
### Bug Fixes
* hide progress indicator if asking for creds ([#458](https://www.github.com/knative-extensions/kn-plugin-func/issues/458)) ([79e2234](https://www.github.com/knative-extensions/kn-plugin-func/commit/79e2234cbc62319f35b18a9b2a39ca4dffe89d4d))
* use ascii chars in progress indicator on win ([#459](https://www.github.com/knative-extensions/kn-plugin-func/issues/459)) ([6fd42a4](https://www.github.com/knative-extensions/kn-plugin-func/commit/6fd42a421ea58a4e9e1b6b6bff3f97d1da99d349))
## [0.17.0](https://www.github.com/knative-extensions/kn-plugin-func/compare/v0.16.0...v0.17.0) (2021-08-03)
### Features
* Add proper example of configuring Rust functions. ([#436](https://www.github.com/knative-extensions/kn-plugin-func/issues/436)) ([7656c40](https://www.github.com/knative-extensions/kn-plugin-func/commit/7656c4097283ed54b9e5f0472947cff931973365))
* add support for labels in func.yaml ([#373](https://www.github.com/knative-extensions/kn-plugin-func/issues/373)) ([0dba677](https://www.github.com/knative-extensions/kn-plugin-func/commit/0dba67751e5a4c594701d674b44b101a043e9a2c))
* Configure Rust functions ([#430](https://www.github.com/knative-extensions/kn-plugin-func/issues/430)) ([a08b843](https://www.github.com/knative-extensions/kn-plugin-func/commit/a08b843a9c2639d6b237f4248341b35f3bd8b954))
* print emit response output if it's a cloudevent ([#444](https://www.github.com/knative-extensions/kn-plugin-func/issues/444)) ([a25b723](https://www.github.com/knative-extensions/kn-plugin-func/commit/a25b723dbcd50d544566a385441cbdd883017947))
* remote template repositories ([#437](https://www.github.com/knative-extensions/kn-plugin-func/issues/437)) ([9db1a3d](https://www.github.com/knative-extensions/kn-plugin-func/commit/9db1a3d902016d59e60b732de43bdf4be198334f))
### Bug Fixes
* closing stdout ([6f40b29](https://www.github.com/knative-extensions/kn-plugin-func/commit/6f40b29d3e02193c51317a29737c20dc11730c5a))
* do not trust builder when using podman ([#420](https://www.github.com/knative-extensions/kn-plugin-func/issues/420)) ([894f4fe](https://www.github.com/knative-extensions/kn-plugin-func/commit/894f4febda1d7da5d3f47e1003b29b339b1f8cd4))
* fix unit tests for Node.js event templates ([#438](https://www.github.com/knative-extensions/kn-plugin-func/issues/438)) ([d71532a](https://www.github.com/knative-extensions/kn-plugin-func/commit/d71532a070b24ec70dd5b77221e11b53bd300e8d))
* unnecessary template repackaging ([#449](https://www.github.com/knative-extensions/kn-plugin-func/issues/449)) ([435d1ac](https://www.github.com/knative-extensions/kn-plugin-func/commit/435d1ac2a39c4e3abf1a6518b05be3151d132a57))
* update builders version ([#421](https://www.github.com/knative-extensions/kn-plugin-func/issues/421)) ([771a230](https://www.github.com/knative-extensions/kn-plugin-func/commit/771a2307a13d105a188a0fd2c2fa843f3a535277))
## [0.16.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.15.1...v0.16.0) (2021-06-23)
### ⚠ BREAKING CHANGES
* change --trigger and --templates flags
* function signatures implied from trigger
### Features
* `func config envs` - interactive prompt ([#396](https://github.com/knative-extensions/kn-plugin-func/issues/396)) ([83a9ca6](https://github.com/knative-extensions/kn-plugin-func/commit/83a9ca684f1b74458b4804fe0e0efe5e95507077))
* `func config volumes` - interactive prompt ([#391](https://github.com/knative-extensions/kn-plugin-func/issues/391)) ([4ba95b6](https://github.com/knative-extensions/kn-plugin-func/commit/4ba95b69a8926ef56773166951ab8fa577111d37))
* add a URL output type for `func describe` ([#389](https://github.com/knative-extensions/kn-plugin-func/issues/389)) ([947fcaa](https://github.com/knative-extensions/kn-plugin-func/commit/947fcaa968a90efed4b6037cafa19e8fadda1fc7)), closes [#387](https://github.com/knative-extensions/kn-plugin-func/issues/387)
* allow setting autoscaling options to deployed KService ([#374](https://github.com/knative-extensions/kn-plugin-func/issues/374)) ([a937c49](https://github.com/knative-extensions/kn-plugin-func/commit/a937c490b7e1ad31c3596f91c310c3f4560329fd))
* allow setting resource requests/limits ([#386](https://github.com/knative-extensions/kn-plugin-func/issues/386)) ([12c5cda](https://github.com/knative-extensions/kn-plugin-func/commit/12c5cda8e2157a775e9fc0bb14fc051c5119f86a))
* reference ConfigMaps in `envs` and `volumes` sections in config ([#371](https://github.com/knative-extensions/kn-plugin-func/issues/371)) ([1dbb5ae](https://github.com/knative-extensions/kn-plugin-func/commit/1dbb5aecbf73cd77a648eaff5e52c1c3ce282a67))
* reference Secrets in `envs` and `volumes` sections in config ([#369](https://github.com/knative-extensions/kn-plugin-func/issues/369)) ([9d7fd34](https://github.com/knative-extensions/kn-plugin-func/commit/9d7fd346495b119e895747d747c1c0a5bacb988e))
* Rust templates ([#376](https://github.com/knative-extensions/kn-plugin-func/issues/376)) ([4711638](https://github.com/knative-extensions/kn-plugin-func/commit/4711638495692e5b8fc1ccca34000c44afa3832c))
* typed errors for templates use cases ([40f1027](https://github.com/knative-extensions/kn-plugin-func/commit/40f10277a4efc3239bbec7a35586c3eabf3337ee))
### Bug Fixes
* disable selinux labeling ([6e8517c](https://github.com/knative-extensions/kn-plugin-func/commit/6e8517c023fa815c616606640657344785dbe4ff))
* password read on windows ([84f896b](https://github.com/knative-extensions/kn-plugin-func/commit/84f896b3298fffe9c8aeec2706c83b6a0fb48141))
* use credsStore ([88ea081](https://github.com/knative-extensions/kn-plugin-func/commit/88ea081cc0addb644ca4a575735a6dd3393197a2))
### Code Refactoring
* change --trigger and --templates flags ([ce29ff6](https://github.com/knative-extensions/kn-plugin-func/commit/ce29ff6285d68bc008fbf0cfbd956982044104bc))
* function signatures implied from trigger ([b30e883](https://github.com/knative-extensions/kn-plugin-func/commit/b30e883e671477ebfa217df03e6825778e84a3df))
### [0.15.1](https://github.com/knative-extensions/kn-plugin-func/compare/v0.15.0...v0.15.1) (2021-05-27)
### Bug Fixes
* Revert "chore: bump Knative deps to 0.22.0 ([#358](https://github.com/knative-extensions/kn-plugin-func/issues/358))" ([#366](https://github.com/knative-extensions/kn-plugin-func/issues/366)) ([72584ce](https://github.com/knative-extensions/kn-plugin-func/commit/72584ced0dc3af86852f56ce36171ba567481b41))
## [0.15.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.14.0...v0.15.0) (2021-05-26)
## [0.15.0](https://www.github.com/boson-project/func/compare/v0.14.0...v0.15.0) (2021-05-26)
### ⚠ BREAKING CHANGES
@ -358,20 +10,20 @@ All notable changes to this project will be documented in this file. See [standa
### Features
* add 'kn func emit' command ([#332](https://github.com/knative-extensions/kn-plugin-func/issues/332)) ([49594d9](https://github.com/knative-extensions/kn-plugin-func/commit/49594d976627c593ff18e42086199225ddcf5130))
* add typescript templates ([#355](https://github.com/knative-extensions/kn-plugin-func/issues/355)) ([d3eafe2](https://github.com/knative-extensions/kn-plugin-func/commit/d3eafe2a8451ebc28124b913f03c12e9359d5e30))
* add 'kn func emit' command ([#332](https://www.github.com/boson-project/func/issues/332)) ([49594d9](https://www.github.com/boson-project/func/commit/49594d976627c593ff18e42086199225ddcf5130))
* add typescript templates ([#355](https://www.github.com/boson-project/func/issues/355)) ([d3eafe2](https://www.github.com/boson-project/func/commit/d3eafe2a8451ebc28124b913f03c12e9359d5e30))
### Bug Fixes
* minor typos in node template docs ([#351](https://github.com/knative-extensions/kn-plugin-func/issues/351)) ([ea0a75a](https://github.com/knative-extensions/kn-plugin-func/commit/ea0a75a7ccb6d00b8c859ff4cd311ad33fb8dbc3))
* minor typos in node template docs ([#351](https://www.github.com/boson-project/func/issues/351)) ([ea0a75a](https://www.github.com/boson-project/func/commit/ea0a75a7ccb6d00b8c859ff4cd311ad33fb8dbc3))
### src
* **templates:** modify the nodejs event template to accept a cloudevent ([#356](https://github.com/knative-extensions/kn-plugin-func/issues/356)) ([caf0659](https://github.com/knative-extensions/kn-plugin-func/commit/caf0659900a79650bb11877ffaeadbc30be9f922))
* **templates:** modify the nodejs event template to accept a cloudevent ([#356](https://www.github.com/boson-project/func/issues/356)) ([caf0659](https://www.github.com/boson-project/func/commit/caf0659900a79650bb11877ffaeadbc30be9f922))
## [0.14.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.13.0...v0.14.0) (2021-05-12)
## [0.14.0](https://www.github.com/boson-project/func/compare/v0.13.0...v0.14.0) (2021-05-12)
### ⚠ BREAKING CHANGES
@ -380,9 +32,9 @@ All notable changes to this project will be documented in this file. See [standa
### src
* revert bump to go 1.16 and template changes ([#340](https://github.com/knative-extensions/kn-plugin-func/issues/340)) ([2b025df](https://github.com/knative-extensions/kn-plugin-func/commit/2b025df19942e990050ef344784662fe77fd7309))
* revert bump to go 1.16 and template changes ([#340](https://www.github.com/boson-project/func/issues/340)) ([2b025df](https://www.github.com/boson-project/func/commit/2b025df19942e990050ef344784662fe77fd7309))
## [0.13.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.12.1...v0.13.0) (2021-05-12)
## [0.13.0](https://www.github.com/boson-project/func/compare/v0.12.1...v0.13.0) (2021-05-12)
### ⚠ BREAKING CHANGES
@ -391,88 +43,88 @@ All notable changes to this project will be documented in this file. See [standa
### Features
* add support for annotations in func.yaml ([#314](https://github.com/knative-extensions/kn-plugin-func/issues/314)) ([5feb0e2](https://github.com/knative-extensions/kn-plugin-func/commit/5feb0e20f366f8dc46f339257d87419bc852753c))
* add/improve spinner for build and deploy ([#322](https://github.com/knative-extensions/kn-plugin-func/issues/322)) ([857b0fd](https://github.com/knative-extensions/kn-plugin-func/commit/857b0fd19d2a716c804426196e907a3ad31d559e))
* create templates archive on go generate ([63b7f11](https://github.com/knative-extensions/kn-plugin-func/commit/63b7f1147176ce5cfd21c3b74094fcc8154298df))
* function name matches KService name ([#317](https://github.com/knative-extensions/kn-plugin-func/issues/317)) ([541e858](https://github.com/knative-extensions/kn-plugin-func/commit/541e8586f7348fa92ee83f246ef34730b1801b9f))
* positive error when runtimme or template unrecognized ([acc56b0](https://github.com/knative-extensions/kn-plugin-func/commit/acc56b0900113ca68270bd3ac68310864e42b5a7))
* preserve file modes using in-memory tar FS ([7dc772e](https://github.com/knative-extensions/kn-plugin-func/commit/7dc772ec62536fc77b84b16550bf7d2a1f0b6a09))
* support windows paths in embedded templates FS ([c2b2168](https://github.com/knative-extensions/kn-plugin-func/commit/c2b216857bcc1e18555a2e41fa3ad675e75cf1c3))
* usage of local evnvvar in func cfg file ([7f8e595](https://github.com/knative-extensions/kn-plugin-func/commit/7f8e5954a939563486661a98198b22f41eebc195))
* add support for annotations in func.yaml ([#314](https://www.github.com/boson-project/func/issues/314)) ([5feb0e2](https://www.github.com/boson-project/func/commit/5feb0e20f366f8dc46f339257d87419bc852753c))
* add/improve spinner for build and deploy ([#322](https://www.github.com/boson-project/func/issues/322)) ([857b0fd](https://www.github.com/boson-project/func/commit/857b0fd19d2a716c804426196e907a3ad31d559e))
* create templates archive on go generate ([63b7f11](https://www.github.com/boson-project/func/commit/63b7f1147176ce5cfd21c3b74094fcc8154298df))
* function name matches KService name ([#317](https://www.github.com/boson-project/func/issues/317)) ([541e858](https://www.github.com/boson-project/func/commit/541e8586f7348fa92ee83f246ef34730b1801b9f))
* positive error when runtimme or template unrecognized ([acc56b0](https://www.github.com/boson-project/func/commit/acc56b0900113ca68270bd3ac68310864e42b5a7))
* preserve file modes using in-memory tar FS ([7dc772e](https://www.github.com/boson-project/func/commit/7dc772ec62536fc77b84b16550bf7d2a1f0b6a09))
* support windows paths in embedded templates FS ([c2b2168](https://www.github.com/boson-project/func/commit/c2b216857bcc1e18555a2e41fa3ad675e75cf1c3))
* usage of local evnvvar in func cfg file ([7f8e595](https://www.github.com/boson-project/func/commit/7f8e5954a939563486661a98198b22f41eebc195))
### Bug Fixes
* added checks on delete command test for lint ([94e387c](https://github.com/knative-extensions/kn-plugin-func/commit/94e387c9326aed79ede95f36b97da4de97c42dec))
* default for `--builder` flag ([06455f4](https://github.com/knative-extensions/kn-plugin-func/commit/06455f4bac02e8581ae4471e72909ba9fe7dbd4d))
* func delete with explicit name as argument ([#323](https://github.com/knative-extensions/kn-plugin-func/issues/323)) with strict validation ([8ab0ba2](https://github.com/knative-extensions/kn-plugin-func/commit/8ab0ba243ae4c40867a2426b2ca965559a03cd53))
* lint issues ([895872a](https://github.com/knative-extensions/kn-plugin-func/commit/895872aee76b44be739bd0eafb9f2cdcdc137494))
* added checks on delete command test for lint ([94e387c](https://www.github.com/boson-project/func/commit/94e387c9326aed79ede95f36b97da4de97c42dec))
* default for `--builder` flag ([06455f4](https://www.github.com/boson-project/func/commit/06455f4bac02e8581ae4471e72909ba9fe7dbd4d))
* func delete with explicity name as argument ([#323](https://www.github.com/boson-project/func/issues/323)) with strict validation ([8ab0ba2](https://www.github.com/boson-project/func/commit/8ab0ba243ae4c40867a2426b2ca965559a03cd53))
* lint issues ([895872a](https://www.github.com/boson-project/func/commit/895872aee76b44be739bd0eafb9f2cdcdc137494))
### Code Refactoring
* change envVars to env in func.yaml ([#316](https://github.com/knative-extensions/kn-plugin-func/issues/316)) ([89ff286](https://github.com/knative-extensions/kn-plugin-func/commit/89ff286a1f3afae655a2c724a05cb3bc3c281786))
* change envVars to env in func.yaml ([#316](https://www.github.com/boson-project/func/issues/316)) ([89ff286](https://www.github.com/boson-project/func/commit/89ff286a1f3afae655a2c724a05cb3bc3c281786))
### [0.12.1](https://github.com/knative-extensions/kn-plugin-func/compare/v0.12.0...v0.12.1) (2021-04-14)
### [0.12.1](https://www.github.com/boson-project/func/compare/v0.12.0...v0.12.1) (2021-04-14)
### Bug Fixes
* build needs to use legacy jar ([129dc5a](https://github.com/knative-extensions/kn-plugin-func/commit/129dc5a8348dc8e4e14f5891871cf6b50ae35ccc))
* build needs to use legacy jar ([129dc5a](https://www.github.com/boson-project/func/commit/129dc5a8348dc8e4e14f5891871cf6b50ae35ccc))
## [0.12.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.11.0...v0.12.0) (2021-03-30)
## [0.12.0](https://www.github.com/boson-project/func/compare/v0.11.0...v0.12.0) (2021-03-30)
### Features
* add --build (default: true) flag to func deploy ([8a91cac](https://github.com/knative-extensions/kn-plugin-func/commit/8a91cac6cc78b5cf56d5158f3eb03a4076a34ffe))
* basic lifecycle integraiton tests ([8edd0df](https://github.com/knative-extensions/kn-plugin-func/commit/8edd0df836055b33473f9a7774e8ae755f46ac2e))
* integration tests target ([ddf4ab8](https://github.com/knative-extensions/kn-plugin-func/commit/ddf4ab86c46912f78e56a52a14efcf89fd187103))
* local cluster allocation, configuration and teardown ([9c499b6](https://github.com/knative-extensions/kn-plugin-func/commit/9c499b69c4991b86e51127081cee7fb0fc34d554))
* using custom docker daemon (e.g podman) ([6d2d8c6](https://github.com/knative-extensions/kn-plugin-func/commit/6d2d8c63b01e12f6cf277c2cd18c3f7298ce86ab))
* add --build (default: true) flag to func deploy ([8a91cac](https://www.github.com/boson-project/func/commit/8a91cac6cc78b5cf56d5158f3eb03a4076a34ffe))
* basic lifecycle integraiton tests ([8edd0df](https://www.github.com/boson-project/func/commit/8edd0df836055b33473f9a7774e8ae755f46ac2e))
* integration tests target ([ddf4ab8](https://www.github.com/boson-project/func/commit/ddf4ab86c46912f78e56a52a14efcf89fd187103))
* local cluster allocation, configuration and teardown ([9c499b6](https://www.github.com/boson-project/func/commit/9c499b69c4991b86e51127081cee7fb0fc34d554))
* using custom docker daemon (e.g podman) ([6d2d8c6](https://www.github.com/boson-project/func/commit/6d2d8c63b01e12f6cf277c2cd18c3f7298ce86ab))
### Bug Fixes
* `func deploy` uses Docker API, not binary ([dc2fbee](https://github.com/knative-extensions/kn-plugin-func/commit/dc2fbee67f7f2304bece83a9b4d4f051ed19cd61))
* `func run` now uses Docker API, not binary ([db0945e](https://github.com/knative-extensions/kn-plugin-func/commit/db0945ed3ecb9e6e4283a0cb478d39657c6803dc))
* compare service names in integraiton tests ([1551d69](https://github.com/knative-extensions/kn-plugin-func/commit/1551d69b5d287becaafdf3d5b99a6ba8da926fa6))
* exposed port ([7ed2e86](https://github.com/knative-extensions/kn-plugin-func/commit/7ed2e86d9672f285c1def490a3d325ceb9e8471f))
* increase remove timeout to 120s ([80e366b](https://github.com/knative-extensions/kn-plugin-func/commit/80e366b14234c184932d91db4188bdabb0742e7a))
* sprint-boot template ([38fd673](https://github.com/knative-extensions/kn-plugin-func/commit/38fd673fdbef1094558b32910a42fcdff2d8bb0c))
* update pack dependency ([c3c2165](https://github.com/knative-extensions/kn-plugin-func/commit/c3c21657b2bc3cba9e4ba87864d8fe0c5d4e43af))
* `func deploy` uses Docker API, not binary ([dc2fbee](https://www.github.com/boson-project/func/commit/dc2fbee67f7f2304bece83a9b4d4f051ed19cd61))
* `func run` now uses Docker API, not binary ([db0945e](https://www.github.com/boson-project/func/commit/db0945ed3ecb9e6e4283a0cb478d39657c6803dc))
* compare service names in integraiton tests ([1551d69](https://www.github.com/boson-project/func/commit/1551d69b5d287becaafdf3d5b99a6ba8da926fa6))
* exposed port ([7ed2e86](https://www.github.com/boson-project/func/commit/7ed2e86d9672f285c1def490a3d325ceb9e8471f))
* increase remove timeout to 120s ([80e366b](https://www.github.com/boson-project/func/commit/80e366b14234c184932d91db4188bdabb0742e7a))
* sprint-boot template ([38fd673](https://www.github.com/boson-project/func/commit/38fd673fdbef1094558b32910a42fcdff2d8bb0c))
* update pack dependency ([c3c2165](https://www.github.com/boson-project/func/commit/c3c21657b2bc3cba9e4ba87864d8fe0c5d4e43af))
## [0.11.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.10.0...v0.11.0) (2021-01-21)
## [0.11.0](https://www.github.com/boson-project/func/compare/v0.10.0...v0.11.0) (2021-01-21)
### Features
* add --all-namespaces flag to `func list` ([#242](https://github.com/knative-extensions/kn-plugin-func/issues/242)) ([8e72fd2](https://github.com/knative-extensions/kn-plugin-func/commit/8e72fd2eba9f4e6e5d3a0bd366215025ba1d1004))
* add --all-namespaces flag to `func list` ([#242](https://www.github.com/boson-project/func/issues/242)) ([8e72fd2](https://www.github.com/boson-project/func/commit/8e72fd2eba9f4e6e5d3a0bd366215025ba1d1004))
### Bug Fixes
* change --format flag to --output for list and describe commands ([#248](https://github.com/knative-extensions/kn-plugin-func/issues/248)) ([6470d9e](https://github.com/knative-extensions/kn-plugin-func/commit/6470d9e57462bc8d3a30583cf146d4f466e2d5f7))
* correct fn signatures in Go Events template ([#246](https://github.com/knative-extensions/kn-plugin-func/issues/246)) ([5502492](https://github.com/knative-extensions/kn-plugin-func/commit/55024921c26e044f83187cbd5510375d8702c6d9))
* correcting broken merge ([#252](https://github.com/knative-extensions/kn-plugin-func/issues/252)) ([8d1f5b8](https://github.com/knative-extensions/kn-plugin-func/commit/8d1f5b833d86fa959e3386db73f7e1b07bdd6dfd))
* fix the help text for the describe function ([#243](https://github.com/knative-extensions/kn-plugin-func/issues/243)) ([5a3a0d6](https://github.com/knative-extensions/kn-plugin-func/commit/5a3a0d6bdab4d01292c4c8f6011a3b67cadb8ef6))
* print "No functions found in [ns] namespace" for kn func list ([#240](https://github.com/knative-extensions/kn-plugin-func/issues/240)) ([61ea8d4](https://github.com/knative-extensions/kn-plugin-func/commit/61ea8d4fc6e841f0f10151244f10131862bf181c))
* set envVars when creating a function ([#250](https://github.com/knative-extensions/kn-plugin-func/issues/250)) ([f0be048](https://github.com/knative-extensions/kn-plugin-func/commit/f0be048c841be22fcd0d448fdecc0da33b8c77be))
* change --format flag to --output for list and describe commands ([#248](https://www.github.com/boson-project/func/issues/248)) ([6470d9e](https://www.github.com/boson-project/func/commit/6470d9e57462bc8d3a30583cf146d4f466e2d5f7))
* correct fn signatures in Go Events template ([#246](https://www.github.com/boson-project/func/issues/246)) ([5502492](https://www.github.com/boson-project/func/commit/55024921c26e044f83187cbd5510375d8702c6d9))
* correcting broken merge ([#252](https://www.github.com/boson-project/func/issues/252)) ([8d1f5b8](https://www.github.com/boson-project/func/commit/8d1f5b833d86fa959e3386db73f7e1b07bdd6dfd))
* fix the help text for the describe function ([#243](https://www.github.com/boson-project/func/issues/243)) ([5a3a0d6](https://www.github.com/boson-project/func/commit/5a3a0d6bdab4d01292c4c8f6011a3b67cadb8ef6))
* print "No functions found in [ns] namespace" for kn func list ([#240](https://www.github.com/boson-project/func/issues/240)) ([61ea8d4](https://www.github.com/boson-project/func/commit/61ea8d4fc6e841f0f10151244f10131862bf181c))
* set envVars when creating a function ([#250](https://www.github.com/boson-project/func/issues/250)) ([f0be048](https://www.github.com/boson-project/func/commit/f0be048c841be22fcd0d448fdecc0da33b8c77be))
## [0.10.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.9.0...v0.10.0) (2020-12-08)
## [0.10.0](https://www.github.com/boson-project/faas/compare/v0.9.0...v0.10.0) (2020-12-08)
### Features
* add spring cloud function runtime and templates ([#231](https://github.com/knative-extensions/kn-plugin-func/issues/231)) ([557361a](https://github.com/knative-extensions/kn-plugin-func/commit/557361a37446953dc613ae30f59913f1924dedd3))
* add spring cloud function runtime and templates ([#231](https://www.github.com/boson-project/faas/issues/231)) ([557361a](https://www.github.com/boson-project/faas/commit/557361a37446953dc613ae30f59913f1924dedd3))
### Bug Fixes
* Fix plugin version output ([#233](https://github.com/knative-extensions/kn-plugin-func/issues/233)) ([8a30ba1](https://github.com/knative-extensions/kn-plugin-func/commit/8a30ba193da6097a141332212cbd64e5a1a708e8))
* use image name for run command ([#238](https://github.com/knative-extensions/kn-plugin-func/issues/238)) ([985906b](https://github.com/knative-extensions/kn-plugin-func/commit/985906b0e1f692f94fc84e3e796893192d17bd4c))
* Fix plugin version output ([#233](https://www.github.com/boson-project/faas/issues/233)) ([8a30ba1](https://www.github.com/boson-project/faas/commit/8a30ba193da6097a141332212cbd64e5a1a708e8))
* use image name for run command ([#238](https://www.github.com/boson-project/faas/issues/238)) ([985906b](https://www.github.com/boson-project/faas/commit/985906b0e1f692f94fc84e3e796893192d17bd4c))
## [0.9.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.8.0...v0.9.0) (2020-11-06)
## [0.9.0](https://www.github.com/boson-project/faas/compare/v0.8.0...v0.9.0) (2020-11-06)
### ⚠ BREAKING CHANGES
@ -482,19 +134,19 @@ All notable changes to this project will be documented in this file. See [standa
### Features
* Better output of build/deploy/delete commands ([#206](https://github.com/knative-extensions/kn-plugin-func/issues/206)) ([ddbb95b](https://github.com/knative-extensions/kn-plugin-func/commit/ddbb95b075a383fb1847be2c75fd2c216870c7f8))
* change default runtime to Node.js HTTP ([#198](https://github.com/knative-extensions/kn-plugin-func/issues/198)) ([61cb56a](https://github.com/knative-extensions/kn-plugin-func/commit/61cb56aec3461e9f9b35282435dbc884999be2b3))
* list command - improved output ([#205](https://github.com/knative-extensions/kn-plugin-func/issues/205)) ([29ca077](https://github.com/knative-extensions/kn-plugin-func/commit/29ca07768ca455debb7992ebbf09b2db2058f56d))
* remove create cli subcommand ([#180](https://github.com/knative-extensions/kn-plugin-func/issues/180)) ([57e1236](https://github.com/knative-extensions/kn-plugin-func/commit/57e12362af18f48624a9c303c070846e1645e08d))
* rename faas to function ([#210](https://github.com/knative-extensions/kn-plugin-func/issues/210)) ([cd57692](https://github.com/knative-extensions/kn-plugin-func/commit/cd57692c9d04fecb918abf4f15cd37d45592cf82))
* Better output of build/deploy/delete commands ([#206](https://www.github.com/boson-project/faas/issues/206)) ([ddbb95b](https://www.github.com/boson-project/faas/commit/ddbb95b075a383fb1847be2c75fd2c216870c7f8))
* change default runtime to Node.js HTTP ([#198](https://www.github.com/boson-project/faas/issues/198)) ([61cb56a](https://www.github.com/boson-project/faas/commit/61cb56aec3461e9f9b35282435dbc884999be2b3))
* list command - improved output ([#205](https://www.github.com/boson-project/faas/issues/205)) ([29ca077](https://www.github.com/boson-project/faas/commit/29ca07768ca455debb7992ebbf09b2db2058f56d))
* remove create cli subcommand ([#180](https://www.github.com/boson-project/faas/issues/180)) ([57e1236](https://www.github.com/boson-project/faas/commit/57e12362af18f48624a9c303c070846e1645e08d))
* rename faas to function ([#210](https://www.github.com/boson-project/faas/issues/210)) ([cd57692](https://www.github.com/boson-project/faas/commit/cd57692c9d04fecb918abf4f15cd37d45592cf82))
### Bug Fixes
* `delete` and `deploy sub-commands respects func.yaml conf ([d562498](https://github.com/knative-extensions/kn-plugin-func/commit/d5624980d5f31f98bc27e803ae94311491d4d078))
* return JSON in Node.js event template ([#211](https://github.com/knative-extensions/kn-plugin-func/issues/211)) ([beb838f](https://github.com/knative-extensions/kn-plugin-func/commit/beb838ff43d04c7ccec63a26fbe2af9fb167ae1a))
* `delete` and `deploy sub-commands respects func.yaml conf ([d562498](https://www.github.com/boson-project/faas/commit/d5624980d5f31f98bc27e803ae94311491d4d078))
* return JSON in Node.js event template ([#211](https://www.github.com/boson-project/faas/issues/211)) ([beb838f](https://www.github.com/boson-project/faas/commit/beb838ff43d04c7ccec63a26fbe2af9fb167ae1a))
## [0.8.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.7.0...v0.8.0) (2020-10-20)
## [0.8.0](https://www.github.com/boson-project/faas/compare/v0.7.0...v0.8.0) (2020-10-20)
### ⚠ BREAKING CHANGES
@ -504,48 +156,48 @@ All notable changes to this project will be documented in this file. See [standa
### Features
* add health probes to node & go services ([#174](https://github.com/knative-extensions/kn-plugin-func/issues/174)) ([95c1eb5](https://github.com/knative-extensions/kn-plugin-func/commit/95c1eb5e59335cfee84ce536d086bd394268c81c))
* introduce CloudEvent data as first parameter for event functions ([#172](https://github.com/knative-extensions/kn-plugin-func/issues/172)) ([7451194](https://github.com/knative-extensions/kn-plugin-func/commit/74511948cefc368d898ad05b911fded74d44b759))
* user can set envvars ([5182487](https://github.com/knative-extensions/kn-plugin-func/commit/5182487df218685867fda10c3d1983b4c035c08a))
* **kn:** Enable faas to be integrated as plugin to kn ([#155](https://github.com/knative-extensions/kn-plugin-func/issues/155)) ([85a5f47](https://github.com/knative-extensions/kn-plugin-func/commit/85a5f475eb32269b9cced05fe36dc90f8befd000))
* ability for users to specify custom builders ([#147](https://github.com/knative-extensions/kn-plugin-func/issues/147)) ([c2b4a30](https://github.com/knative-extensions/kn-plugin-func/commit/c2b4a304bd3fa7d020c71db9f4d79c80c98d86d3))
* combine deploy and update commands ([#152](https://github.com/knative-extensions/kn-plugin-func/issues/152)) ([d5839ea](https://github.com/knative-extensions/kn-plugin-func/commit/d5839ea6c1e84e843ad643cc0611a82e2e6d2399))
* fish completion ([d822303](https://github.com/knative-extensions/kn-plugin-func/commit/d82230353d3d437e8f35e7f9ce3569988d765b42))
* add health probes to node & go services ([#174](https://www.github.com/boson-project/faas/issues/174)) ([95c1eb5](https://www.github.com/boson-project/faas/commit/95c1eb5e59335cfee84ce536d086bd394268c81c))
* introduce CloudEvent data as first parameter for event functions ([#172](https://www.github.com/boson-project/faas/issues/172)) ([7451194](https://www.github.com/boson-project/faas/commit/74511948cefc368d898ad05b911fded74d44b759))
* user can set envvars ([5182487](https://www.github.com/boson-project/faas/commit/5182487df218685867fda10c3d1983b4c035c08a))
* **kn:** Enable faas to be integrated as plugin to kn ([#155](https://www.github.com/boson-project/faas/issues/155)) ([85a5f47](https://www.github.com/boson-project/faas/commit/85a5f475eb32269b9cced05fe36dc90f8befd000))
* ability for users to specify custom builders ([#147](https://www.github.com/boson-project/faas/issues/147)) ([c2b4a30](https://www.github.com/boson-project/faas/commit/c2b4a304bd3fa7d020c71db9f4d79c80c98d86d3))
* combine deploy and update commands ([#152](https://www.github.com/boson-project/faas/issues/152)) ([d5839ea](https://www.github.com/boson-project/faas/commit/d5839ea6c1e84e843ad643cc0611a82e2e6d2399))
* fish completion ([d822303](https://www.github.com/boson-project/faas/commit/d82230353d3d437e8f35e7f9ce3569988d765b42))
### Bug Fixes
* examples in readme ([5591e7f](https://github.com/knative-extensions/kn-plugin-func/commit/5591e7fa2ca9584f03bf8d065778cd120ea9054f))
* image parsing ([6a621a5](https://github.com/knative-extensions/kn-plugin-func/commit/6a621a5186ffffec79e6f34c34681cc37eeaa0bd))
* regenerate pkger files ([#183](https://github.com/knative-extensions/kn-plugin-func/issues/183)) ([1d14a8c](https://github.com/knative-extensions/kn-plugin-func/commit/1d14a8c10156098d66ef691f84ecce1bd25a6d88))
* root cmd init ([ec5327d](https://github.com/knative-extensions/kn-plugin-func/commit/ec5327d5201b57d6a33bcc7314332686582b676f))
* stop using manually edited completion ([bf9b048](https://github.com/knative-extensions/kn-plugin-func/commit/bf9b04881333fed6038251fa4de92368771840d9))
* update quarkus templates ([ffc6a12](https://github.com/knative-extensions/kn-plugin-func/commit/ffc6a123e469968865fef1ccb5f8d84a443baccb))
* update to Knative 0.17 ([#145](https://github.com/knative-extensions/kn-plugin-func/issues/145)) ([5fe7052](https://github.com/knative-extensions/kn-plugin-func/commit/5fe70526e531e283c6704d9526e3cdd7ef64f9e1))
* examples in readme ([5591e7f](https://www.github.com/boson-project/faas/commit/5591e7fa2ca9584f03bf8d065778cd120ea9054f))
* image parsing ([6a621a5](https://www.github.com/boson-project/faas/commit/6a621a5186ffffec79e6f34c34681cc37eeaa0bd))
* regenerate pkger files ([#183](https://www.github.com/boson-project/faas/issues/183)) ([1d14a8c](https://www.github.com/boson-project/faas/commit/1d14a8c10156098d66ef691f84ecce1bd25a6d88))
* root cmd init ([ec5327d](https://www.github.com/boson-project/faas/commit/ec5327d5201b57d6a33bcc7314332686582b676f))
* stop using manually edited completion ([bf9b048](https://www.github.com/boson-project/faas/commit/bf9b04881333fed6038251fa4de92368771840d9))
* update quarkus templates ([ffc6a12](https://www.github.com/boson-project/faas/commit/ffc6a123e469968865fef1ccb5f8d84a443baccb))
* update to Knative 0.17 ([#145](https://www.github.com/boson-project/faas/issues/145)) ([5fe7052](https://www.github.com/boson-project/faas/commit/5fe70526e531e283c6704d9526e3cdd7ef64f9e1))
### src
* change all references of "repository" to "registry" for images ([#156](https://github.com/knative-extensions/kn-plugin-func/issues/156)) ([e425c8f](https://github.com/knative-extensions/kn-plugin-func/commit/e425c8f08183b333e56d5d3cfe74fc9e85a6c903))
* change all references of "repository" to "registry" for images ([#156](https://www.github.com/boson-project/faas/issues/156)) ([e425c8f](https://www.github.com/boson-project/faas/commit/e425c8f08183b333e56d5d3cfe74fc9e85a6c903))
## [0.7.0](https://github.com/knative-extensions/kn-plugin-func/compare/v0.6.2...v0.7.0) (2020-09-24)
## [0.7.0](https://www.github.com/boson-project/faas/compare/v0.6.2...v0.7.0) (2020-09-24)
### Features
* add local debugging to node.js templates ([#132](https://github.com/knative-extensions/kn-plugin-func/issues/132)) ([1b0bb15](https://github.com/knative-extensions/kn-plugin-func/commit/1b0bb15147889bb55ff33de1dc132cb0370d1da6))
* decouple function name from function domain ([#127](https://github.com/knative-extensions/kn-plugin-func/issues/127)) ([0258626](https://github.com/knative-extensions/kn-plugin-func/commit/025862689ec8dc460a1ef6f4402151c18a072ba3))
* default to no confirmation prompts for CLI commands ([566d8f9](https://github.com/knative-extensions/kn-plugin-func/commit/566d8f9255d532e88e72d5bce122bebaee88bc81))
* set builder images in templates and .faas.yaml ([#136](https://github.com/knative-extensions/kn-plugin-func/issues/136)) ([d6e131f](https://github.com/knative-extensions/kn-plugin-func/commit/d6e131f9153c20bd3edbf1441060610987fa5693))
* **ci/cd:** add release-please for automated release management ([8a60c5e](https://github.com/knative-extensions/kn-plugin-func/commit/8a60c5e0c44d28d2ff085e56299217e05e408df8))
* add local debugging to node.js templates ([#132](https://www.github.com/boson-project/faas/issues/132)) ([1b0bb15](https://www.github.com/boson-project/faas/commit/1b0bb15147889bb55ff33de1dc132cb0370d1da6))
* decouple function name from function domain ([#127](https://www.github.com/boson-project/faas/issues/127)) ([0258626](https://www.github.com/boson-project/faas/commit/025862689ec8dc460a1ef6f4402151c18a072ba3))
* default to no confirmation prompts for CLI commands ([566d8f9](https://www.github.com/boson-project/faas/commit/566d8f9255d532e88e72d5bce122bebaee88bc81))
* set builder images in templates and .faas.yaml ([#136](https://www.github.com/boson-project/faas/issues/136)) ([d6e131f](https://www.github.com/boson-project/faas/commit/d6e131f9153c20bd3edbf1441060610987fa5693))
* **ci/cd:** add release-please for automated release management ([8a60c5e](https://www.github.com/boson-project/faas/commit/8a60c5e0c44d28d2ff085e56299217e05e408df8))
### Bug Fixes
* correct value for config path and robustify ([#130](https://github.com/knative-extensions/kn-plugin-func/issues/130)) ([fae27da](https://github.com/knative-extensions/kn-plugin-func/commit/fae27dabc97c78cd98be400d296da6fc2fbeba65))
* delete command ([284b77f](https://github.com/knative-extensions/kn-plugin-func/commit/284b77f7ef6524195da958850131190399470375))
* describe works without Eventing ([6c16e65](https://github.com/knative-extensions/kn-plugin-func/commit/6c16e65d60543458f0b70c010d672cb4d45f6279))
* sync package-lock.json ([#137](https://github.com/knative-extensions/kn-plugin-func/issues/137)) ([02309a2](https://github.com/knative-extensions/kn-plugin-func/commit/02309a24a1d8779fb69e4f67fa4f7faea705b2ba))
* correct value for config path and robustify ([#130](https://www.github.com/boson-project/faas/issues/130)) ([fae27da](https://www.github.com/boson-project/faas/commit/fae27dabc97c78cd98be400d296da6fc2fbeba65))
* delete command ([284b77f](https://www.github.com/boson-project/faas/commit/284b77f7ef6524195da958850131190399470375))
* describe works without Eventing ([6c16e65](https://www.github.com/boson-project/faas/commit/6c16e65d60543458f0b70c010d672cb4d45f6279))
* sync package-lock.json ([#137](https://www.github.com/boson-project/faas/issues/137)) ([02309a2](https://www.github.com/boson-project/faas/commit/02309a24a1d8779fb69e4f67fa4f7faea705b2ba))
## [Unreleased]
@ -565,7 +217,7 @@ All notable changes to this project will be documented in this file. See [standa
### Chore
- update quarkus version to 1.7.2.Final
- use organization-level secrets for image deployment
- **actions:** add binary uploads to develop branch CI ([#104](https://github.com/knative-extensions/kn-plugin-func/issues/104))
- **actions:** add binary uploads to develop branch CI ([#104](https://github.com/boson-project/faas/issues/104))
### Docs
- initial Go template READMEs
@ -573,13 +225,13 @@ All notable changes to this project will be documented in this file. See [standa
### Fix
- build releases from main branch only
- remove references to unused binaries appsody, kn, kubectl
- image override ([#88](https://github.com/knative-extensions/kn-plugin-func/issues/88))
- image override ([#88](https://github.com/boson-project/faas/issues/88))
### Release
- v0.6.1
### Templates
- **node:** make node templates use npx [@redhat](https://github.com/redhat)/faas-js-runtime ([#99](https://github.com/knative-extensions/kn-plugin-func/issues/99))
- **node:** make node templates use npx [@redhat](https://github.com/redhat)/faas-js-runtime ([#99](https://github.com/boson-project/faas/issues/99))
<a name="v0.6.0"></a>
@ -590,7 +242,7 @@ All notable changes to this project will be documented in this file. See [standa
### Docs
- fix function typos
- setting up remote access to kind clusters
- wireguard configuration for OS X
- wireguard configuraiton for OS X
- Kind cluster provisioning and TLS
- separate repository and system docs
- getting started with kubernetes, reorganization.
@ -602,8 +254,8 @@ All notable changes to this project will be documented in this file. See [standa
- consolidate knative client config construction
- cli usability enhancements and API simplification
- the `list` sub-command uses namespace
- version command respects verbose flag ([#61](https://github.com/knative-extensions/kn-plugin-func/issues/61))
- add init/build/deploy commands and customizable namespace ([#65](https://github.com/knative-extensions/kn-plugin-func/issues/65))
- version command respects verbose flag ([#61](https://github.com/boson-project/faas/issues/61))
- add init/build/deploy commands and customizable namespace ([#65](https://github.com/boson-project/faas/issues/65))
- JSON output for the `list` sub-command
### Fix
@ -618,13 +270,13 @@ All notable changes to this project will be documented in this file. See [standa
- v0.6.0
### Test
- add test targets for go and quarkus templates ([#72](https://github.com/knative-extensions/kn-plugin-func/issues/72))
- add test targets for go and quarkus templates ([#72](https://github.com/boson-project/faas/issues/72))
<a name="v0.5.0"></a>
## [v0.5.0] - 2020-07-31
### Actions
- add CHANGELOG.md and a release target to Makefile ([#45](https://github.com/knative-extensions/kn-plugin-func/issues/45))
- add CHANGELOG.md and a release target to Makefile ([#45](https://github.com/boson-project/faas/issues/45))
### Build
- reduce build verbosity for cross-platform compilations
@ -727,17 +379,17 @@ All notable changes to this project will be documented in this file. See [standa
- add kn-based implementation
[Unreleased]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.6.2...HEAD
[v0.6.2]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.6.1...v0.6.2
[v0.6.1]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.6.0...v0.6.1
[v0.6.0]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.5.0...v0.6.0
[v0.5.0]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.3.0...v0.4.0
[v0.3.0]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.2.2...v0.3.0
[v0.2.2]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.2.1...v0.2.2
[v0.2.1]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.2.0...v0.2.1
[v0.2.0]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.1.0...v0.2.0
[v0.1.0]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.0.19...v0.1.0
[v0.0.19]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.0.18...v0.0.19
[v0.0.18]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.0.17...v0.0.18
[v0.0.17]: https://github.com/knative-extensions/kn-plugin-func/compare/v0.0.16...v0.0.17
[Unreleased]: https://github.com/boson-project/faas/compare/v0.6.2...HEAD
[v0.6.2]: https://github.com/boson-project/faas/compare/v0.6.1...v0.6.2
[v0.6.1]: https://github.com/boson-project/faas/compare/v0.6.0...v0.6.1
[v0.6.0]: https://github.com/boson-project/faas/compare/v0.5.0...v0.6.0
[v0.5.0]: https://github.com/boson-project/faas/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/boson-project/faas/compare/v0.3.0...v0.4.0
[v0.3.0]: https://github.com/boson-project/faas/compare/v0.2.2...v0.3.0
[v0.2.2]: https://github.com/boson-project/faas/compare/v0.2.1...v0.2.2
[v0.2.1]: https://github.com/boson-project/faas/compare/v0.2.0...v0.2.1
[v0.2.0]: https://github.com/boson-project/faas/compare/v0.1.0...v0.2.0
[v0.1.0]: https://github.com/boson-project/faas/compare/v0.0.19...v0.1.0
[v0.0.19]: https://github.com/boson-project/faas/compare/v0.0.18...v0.0.19
[v0.0.18]: https://github.com/boson-project/faas/compare/v0.0.17...v0.0.18
[v0.0.17]: https://github.com/boson-project/faas/compare/v0.0.16...v0.0.17

View File

@ -1,3 +0,0 @@
## Knative Community Code of Conduct
The [Knative Community Code of Conduct](https://github.com/knative/community/blob/main/CODE-OF-CONDUCT.md) is defined in the [Knative community repository](https://github.com/knative/community).

3
CODEOWNERS Normal file
View File

@ -0,0 +1,3 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence
* @lance @lkingland @matejvasek @zroubalik

View File

@ -1,18 +0,0 @@
FROM scratch
ARG TARGETARCH
ARG FUNC_UTIL_BINARY=func-util-$TARGETARCH
ENV PATH=/
COPY $FUNC_UTIL_BINARY /func-util
ADD func-util-symlinks.tgz /
LABEL \
org.opencontainers.image.description="Knative Func Utils Image" \
org.opencontainers.image.source="https://github.com/knative/func" \
org.opencontainers.image.vendor="https://github.com/knative/func" \
org.opencontainers.image.url="https://github.com/knative/func/pkgs/container/func-utils"
USER 0:0

367
Makefile
View File

@ -1,323 +1,104 @@
# ##
#
# Run 'make help' for a summary
#
# ##
# Binaries
REPO := quay.io/boson/func
BIN := func
BIN_DARWIN_AMD64 ?= $(BIN)_darwin_amd64
BIN_DARWIN_ARM64 ?= $(BIN)_darwin_arm64
BIN_LINUX_AMD64 ?= $(BIN)_linux_amd64
BIN_LINUX_ARM64 ?= $(BIN)_linux_arm64
BIN_LINUX_PPC64LE ?= $(BIN)_linux_ppc64le
BIN_LINUX_S390X ?= $(BIN)_linux_s390x
BIN_WINDOWS ?= $(BIN)_windows_amd64.exe
# Utilities
BIN_GOLANGCI_LINT ?= "$(PWD)/bin/golangci-lint"
PKGER?=pkger
# Version
# A verbose version is built into the binary including a date stamp, git commit
# hash and the version tag of the current commit (semver) if it exists.
# If the current commit does not have a semver tag, 'tip' is used, unless there
# is a TAG environment variable. Precedence is git tag, environment variable, 'tip'
DARWIN=$(BIN)_darwin_amd64
LINUX=$(BIN)_linux_amd64
WINDOWS=$(BIN)_windows_amd64.exe
CODE := $(shell find . -name '*.go')
DATE := $(shell date -u +"%Y%m%dT%H%M%SZ")
HASH := $(shell git rev-parse --short HEAD 2>/dev/null)
VTAG := $(shell git tag --points-at HEAD | head -1)
VTAG := $(shell [ -z $(VTAG) ] && echo $(ETAG) || echo $(VTAG))
VERS ?= $(shell git describe --tags --match 'v*')
KVER ?= $(shell git describe --tags --match 'knative-*')
VTAG := $(shell git tag --points-at HEAD)
# a VERS environment variable takes precedence over git tags
# and is necessary with release-please-action which tags asynchronously
# unless explicitly, synchronously tagging as is done in ci.yaml
VERS ?= $(shell [ -z $(VTAG) ] && echo 'tip' || echo $(VTAG) )
LDFLAGS := -X knative.dev/func/pkg/version.Vers=$(VERS) -X knative.dev/func/pkg/version.Kver=$(KVER) -X knative.dev/func/pkg/version.Hash=$(HASH)
TEMPLATE_DIRS=$(shell find templates -type d)
TEMPLATE_FILES=$(shell find templates -type f -name '*')
TEMPLATE_PACKAGE=pkged.go
FUNC_UTILS_IMG ?= ghcr.io/knative/func-utils:v2
LDFLAGS += -X knative.dev/func/pkg/k8s.SocatImage=$(FUNC_UTILS_IMG)
LDFLAGS += -X knative.dev/func/pkg/k8s.TarImage=$(FUNC_UTILS_IMG)
LDFLAGS += -X knative.dev/func/pkg/pipelines/tekton.FuncUtilImage=$(FUNC_UTILS_IMG)
build: all
all: $(TEMPLATE_PACKAGE) $(BIN)
GOFLAGS := "-ldflags=$(LDFLAGS)"
export GOFLAGS
$(TEMPLATE_PACKAGE): templates $(TEMPLATE_DIRS) $(TEMPLATE_FILES)
# ensure no cached dependencies are added to the binary
rm -rf templates/node/events/node_modules
rm -rf templates/node/http/node_modules
rm -rf templates/python/events/__pycache__
rm -rf templates/python/http/__pycache__
rm -rf templates/typescript/events/node_modules
rm -rf templates/typescript/http/node_modules
# to install pkger: go get github.com/markbates/pkger/cmd/pkger
$(PKGER)
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
cross-platform: $(TEMPLATE_PACKAGE) $(DARWIN) $(LINUX) $(WINDOWS)
# Default Targets
.PHONY: all
all: build docs
@echo '🎉 Build process completed!'
darwin: $(DARWIN) ## Build for Darwin (macOS)
# Help Text
# Headings: lines with `##$` comment prefix
# Targets: printed if their line includes a `##` comment
.PHONY: help
help:
@echo 'Usage: make <OPTIONS> ... <TARGETS>'
@echo ''
@echo 'Available targets are:'
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
linux: $(LINUX) ## Build for Linux
windows: $(WINDOWS) ## Build for Windows
###############
##@ Development
###############
$(BIN): $(CODE) ## Build using environment defaults
env CGO_ENABLED=0 go build -ldflags "-X main.date=$(DATE) -X main.vers=$(VERS) -X main.hash=$(HASH)" ./cmd/$(BIN)
.PHONY: build
build: $(BIN) ## (default) Build binary for current OS
$(DARWIN):
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $(DARWIN) -ldflags "-X main.date=$(DATE) -X main.vers=$(VERS) -X main.hash=$(HASH)" ./cmd/$(BIN)
.PHONY: $(BIN)
$(BIN): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 go build ./cmd/$(BIN)
$(LINUX):
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(LINUX) -ldflags "-X main.date=$(DATE) -X main.vers=$(VERS) -X main.hash=$(HASH)" ./cmd/$(BIN)
.PHONY: test
test: generate/zz_filesystem_generated.go ## Run core unit tests
go test -race -cover -coverprofile=coverage.txt ./...
$(WINDOWS):
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $(WINDOWS) -ldflags "-X main.date=$(DATE) -X main.vers=$(VERS) -X main.hash=$(HASH)" ./cmd/$(BIN)
.PHONY: check
check: $(BIN_GOLANGCI_LINT) ## Check code quality (lint)
$(BIN_GOLANGCI_LINT) run --timeout 300s
cd test && $(BIN_GOLANGCI_LINT) run --timeout 300s
test: test-binary test-node test-python test-quarkus test-go test-typescript
$(BIN_GOLANGCI_LINT):
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v2.0.2
test-binary:
go test -race -cover -coverprofile=coverage.out ./...
.PHONY: generate/zz_filesystem_generated.go
generate/zz_filesystem_generated.go: clean_templates
go generate pkg/functions/templates_embedded.go
.PHONY: clean_templates
clean_templates:
# Removing temporary template files
@rm -rf templates/**/.DS_Store
@rm -rf templates/node/cloudevents/node_modules
@rm -rf templates/node/http/node_modules
@rm -rf templates/python/cloudevents/.venv
@rm -rf templates/python/cloudevents/.pytest_cache
@rm -rf templates/python/cloudevents/function/__pycache__
@rm -rf templates/python/cloudevents/tests/__pycache__
@rm -rf templates/python/http/.venv
@rm -rf templates/python/http/.pytest_cache
@rm -rf templates/python/http/function/__pycache__
@rm -rf templates/python/http/tests/__pycache__
@rm -rf templates/quarkus/cloudevents/target
@rm -rf templates/quarkus/http/target
@rm -rf templates/rust/cloudevents/target
@rm -rf templates/rust/http/target
@rm -rf templates/springboot/cloudevents/target
@rm -rf templates/springboot/http/target
@rm -rf templates/typescript/cloudevents/build
@rm -rf templates/typescript/cloudevents/node_modules
@rm -rf templates/typescript/http/build
@rm -rf templates/typescript/http/node_modules
.PHONY: clean
clean: clean_templates ## Remove generated artifacts such as binaries and schemas
rm -f $(BIN) $(BIN_WINDOWS) $(BIN_LINUX) $(BIN_DARWIN_AMD64) $(BIN_DARWIN_ARM64)
rm -f $(BIN_GOLANGCI_LINT)
rm -f schema/func_yaml-schema.json
rm -f coverage.txt
.PHONY: docs
docs:
# Generating command reference doc
KUBECONFIG="$(shell mktemp)" go run docs/generator/main.go
#############
##@ Prow Integration
#############
.PHONY: presubmit-unit-tests
presubmit-unit-tests: ## Run prow presubmit unit tests locally
docker run --platform linux/amd64 -it --rm -v$(MAKEFILE_DIR):/src/ us-docker.pkg.dev/knative-tests/images/prow-tests:v20230616-086ddd644 sh -c 'cd /src && runner.sh ./test/presubmit-tests.sh --unit-tests'
#############
##@ Templates
#############
.PHONY: check-embedded-fs
check-embedded-fs: ## Check the embedded templates FS
go test -run "^\QTestFileSystems\E$$/^\Qembedded\E$$" ./pkg/filesystem
# TODO: add linters for other templates
.PHONY: check-templates
check-templates: check-go check-rust ## Run template source code checks
.PHONY: check-go
check-go: ## Check Go templates' source
cd templates/go/scaffolding/instanced-http && go vet ./... && $(BIN_GOLANGCI_LINT) run
cd templates/go/scaffolding/instanced-cloudevents && go vet && $(BIN_GOLANGCI_LINT) run
cd templates/go/scaffolding/static-http && go vet ./... && $(BIN_GOLANGCI_LINT) run
cd templates/go/scaffolding/static-cloudevents && go vet ./... && $(BIN_GOLANGCI_LINT) run
.PHONY: check-rust
check-rust: ## Check Rust templates' source
cd templates/rust/cloudevents && cargo clippy && cargo clean
cd templates/rust/http && cargo clippy && cargo clean
.PHONY: test-templates
test-templates: test-go test-node test-python test-quarkus test-springboot test-rust test-typescript ## Run all template tests
.PHONY: test-go
test-go: ## Test Go templates
cd templates/go/cloudevents && go mod tidy && go test
cd templates/go/http && go mod tidy && go test
.PHONY: test-node
test-node: ## Test Node templates
cd templates/node/cloudevents && npm ci && npm test && rm -rf node_modules
test-node:
cd templates/node/events && npm ci && npm test && rm -rf node_modules
cd templates/node/http && npm ci && npm test && rm -rf node_modules
.PHONY: test-python
test-python: ## Test Python templates and Scaffolding
test/test_python.sh
.PHONY: test-quarkus
test-quarkus: ## Test Quarkus templates
cd templates/quarkus/cloudevents && ./mvnw -q test && ./mvnw clean && rm .mvn/wrapper/maven-wrapper.jar
cd templates/quarkus/http && ./mvnw -q test && ./mvnw clean && rm .mvn/wrapper/maven-wrapper.jar
.PHONY: test-springboot
test-springboot: ## Test Spring Boot templates
cd templates/springboot/cloudevents && ./mvnw -q test && ./mvnw clean && rm .mvn/wrapper/maven-wrapper.jar
cd templates/springboot/http && ./mvnw -q test && ./mvnw clean && rm .mvn/wrapper/maven-wrapper.jar
.PHONY: test-rust
test-rust: ## Test Rust templates
cd templates/rust/cloudevents && cargo -q test && cargo clean
cd templates/rust/http && cargo -q test && cargo clean
.PHONY: test-typescript
test-typescript: ## Test Typescript templates
cd templates/typescript/cloudevents && npm ci && npm test && rm -rf node_modules build
test-typescript:
cd templates/typescript/events && npm ci && npm test && rm -rf node_modules build
cd templates/typescript/http && npm ci && npm test && rm -rf node_modules build
###############
##@ Scaffolding
###############
test-python:
cd templates/python/events && pip3 install -r requirements.txt && python3 test_func.py
cd templates/python/http && python3 test_func.py
# Pulls runtimes then rebuilds the embedded filesystem
.PHONY: update-runtimes
update-runtimes: update-runtime-go generate/zz_filesystem_generated.go ## Update Scaffolding Runtimes
test-quarkus:
cd templates/quarkus/events && mvn test && mvn clean
cd templates/quarkus/http && mvn test && mvn clean
.PHONY: update-runtime-go
update-runtime-go:
cd templates/go/scaffolding/instanced-http && go get -u knative.dev/func-go/http
cd templates/go/scaffolding/static-http && go get -u knative.dev/func-go/http
cd templates/go/scaffolding/instanced-cloudevents && go get -u knative.dev/func-go/cloudevents
cd templates/go/scaffolding/static-cloudevents && go get -u knative.dev/func-go/cloudevents
test-go:
cd templates/go/events && go test
cd templates/go/http && go test
test-integration:
go test -tags integration ./...
.PHONY: certs
certs: templates/certs/ca-certificates.crt ## Update root certificates
bin/golangci-lint:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v1.40.1
.PHONY: templates/certs/ca-certificates.crt
templates/certs/ca-certificates.crt:
# Updating root certificates
curl --output templates/certs/ca-certificates.crt https://curl.se/ca/cacert.pem
check: bin/golangci-lint
./bin/golangci-lint run --timeout 300s
###################
##@ Extended Testing (cluster required)
###################
release: build test
go get -u github.com/git-chglog/git-chglog/cmd/git-chglog
git-chglog --next-tag $(VTAG) -o CHANGELOG.md
git commit -am "release: $(VTAG)"
git tag $(VTAG)
.PHONY: test-integration
test-integration: ## Run integration tests using an available cluster.
go test -tags integration -timeout 30m --coverprofile=coverage.txt ./... -v
cluster: ## Set up a local cluster for integraiton tests.
# Creating KinD cluster `kind`.
# Delete with ./hack/delete.sh
./hack/allocate.sh && ./hack/configure.sh
.PHONY: func-instrumented
func-instrumented: # func binary instrumented with coverage reporting
env CGO_ENABLED=1 go build -cover -o func ./cmd/$(BIN)
.PHONY: test-e2e
test-e2e: func-instrumented ## Run end-to-end tests using an available cluster.
./test/e2e_extended_tests.sh
.PHONY: test-e2e-runtime
test-e2e-runtime: func-instrumented ## Run end-to-end lifecycle tests using an available cluster for a single runtime.
./test/e2e_lifecycle_tests.sh $(runtime)
.PHONY: test-e2e-on-cluster
test-e2e-on-cluster: func-instrumented ## Run end-to-end on-cluster build tests using an available cluster.
./test/e2e_oncluster_tests.sh
######################
##@ Release Artifacts
######################
.PHONY: cross-platform
cross-platform: darwin-arm64 darwin-amd64 linux-amd64 linux-arm64 linux-ppc64le linux-s390x windows ## Build all distributable (cross-platform) binaries
.PHONY: darwin-arm64
darwin-arm64: $(BIN_DARWIN_ARM64) ## Build for mac M1
$(BIN_DARWIN_ARM64): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o $(BIN_DARWIN_ARM64) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
.PHONY: darwn-amd64
darwin-amd64: $(BIN_DARWIN_AMD64) ## Build for Darwin (macOS)
$(BIN_DARWIN_AMD64): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $(BIN_DARWIN_AMD64) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
.PHONY: linux-amd64
linux-amd64: $(BIN_LINUX_AMD64) ## Build for Linux amd64
$(BIN_LINUX_AMD64): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(BIN_LINUX_AMD64) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
.PHONY: linux-arm64
linux-arm64: $(BIN_LINUX_ARM64) ## Build for Linux arm64
$(BIN_LINUX_ARM64): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(BIN_LINUX_ARM64) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
.PHONY: linux-ppc64le
linux-ppc64le: $(BIN_LINUX_PPC64LE) ## Build for Linux ppc64le
$(BIN_LINUX_PPC64LE): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le go build -o $(BIN_LINUX_PPC64LE) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
.PHONY: linux-s390x
linux-s390x: $(BIN_LINUX_S390X) ## Build for Linux s390x
$(BIN_LINUX_S390X): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=linux GOARCH=s390x go build -o $(BIN_LINUX_S390X) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
.PHONY: windows
windows: $(BIN_WINDOWS) ## Build for Windows
$(BIN_WINDOWS): generate/zz_filesystem_generated.go
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $(BIN_WINDOWS) -trimpath -ldflags "$(LDFLAGS) -w -s" ./cmd/$(BIN)
######################
##@ Schemas
######################
.PHONY: schema-generate
schema-generate: schema/func_yaml-schema.json ## Generate func.yaml schema
schema/func_yaml-schema.json: pkg/functions/function.go pkg/functions/function_*.go
go run schema/generator/main.go
.PHONY: schema-check
schema-check: ## Check that func.yaml schema is up-to-date
mv schema/func_yaml-schema.json schema/func_yaml-schema-previous.json
make schema-generate
diff schema/func_yaml-schema.json schema/func_yaml-schema-previous.json ||\
(echo "\n\nFunction config schema 'schema/func_yaml-schema.json' is obsolete, please run 'make schema-generate'.\n\n"; rm -rf schema/func_yaml-schema-previous.json; exit 1)
rm -rf schema/func_yaml-schema-previous.json
######################
##@ Hack scripting
######################
.PHONY: hack-generate-components
hack-generate-components: ## Regenerate components in hack/ dir
cd hack && go run ./cmd/components
.PHONY: test-hack
test-hack:
cd hack && go test ./... -v
## This is used by workflows
.PHONY: update-builder
__update-builder: # Used in automation
cd hack && go run ./cmd/update-builder
clean:
rm -f $(BIN) $(WINDOWS) $(LINUX) $(DARWIN)
-rm -f coverage.out

13
OWNERS
View File

@ -1,13 +0,0 @@
# The OWNERS file is used by prow to automatically merge approved PRs.
approvers:
- technical-oversight-committee
- knative-release-leads
- client-writers
- func-writers
- functions-wg-leads
reviewers:
- client-writers
- func-reviewers

View File

@ -1,126 +0,0 @@
# This file is auto-generated from peribolos.
# Do not modify this file, instead modify peribolos/knative.yaml
aliases:
client-reviewers: []
client-wg-leads:
- dsimansk
client-writers:
- dsimansk
docs-reviewers:
- nainaz
- skonto
docs-writers:
- skonto
eventing-reviewers:
- Leo6Leo
- aslom
- cali0707
- creydr
eventing-wg-leads:
- creydr
- pierDipi
eventing-writers:
- Leo6Leo
- aliok
- cali0707
- creydr
- matzew
- pierDipi
func-reviewers:
- jrangelramos
- nainaz
func-writers:
- gauron99
- jrangelramos
- lkingland
- matejvasek
- matzew
- salaboy
functions-wg-leads:
- lkingland
- salaboy
knative-admin:
- aliok
- arsenetar
- cardil
- dprotaso
- dsimansk
- evankanderson
- gauron99
- knative-automation
- knative-prow-releaser-robot
- knative-prow-robot
- knative-prow-updater-robot
- knative-test-reporter-robot
- matzew
- skonto
- upodroid
knative-release-leads:
- dprotaso
- dsimansk
- gauron99
- skonto
knative-robots:
- knative-automation
- knative-prow-releaser-robot
- knative-prow-robot
- knative-prow-updater-robot
- knative-test-reporter-robot
operations-reviewers:
- aliok
- houshengbo
- matzew
operations-wg-leads:
- houshengbo
operations-writers:
- aliok
- houshengbo
- matzew
productivity-leads:
- cardil
- upodroid
productivity-reviewers:
- evankanderson
- mgencur
productivity-wg-leads:
- cardil
- upodroid
productivity-writers:
- cardil
- upodroid
security-wg-leads:
- davidhadas
- evankanderson
security-writers:
- davidhadas
- evankanderson
serving-approvers:
- dsimansk
- skonto
serving-reviewers:
- skonto
serving-triage:
- skonto
serving-wg-leads:
- dprotaso
serving-writers:
- dprotaso
- dsimansk
- skonto
steering-committee:
- aliok
- arsenetar
- dprotaso
- evankanderson
- matzew
ux-wg-leads:
- Leo6Leo
- cali0707
- mmejia02
- zainabhusain227
ux-writers:
- Leo6Leo
- cali0707
- mmejia02
- zainabhusain227

View File

@ -1,31 +1,17 @@
# Func
# Boson Function CLI
[![CI Status](https://github.com/knative/func/actions/workflows/ci.yaml/badge.svg)](https://github.com/knative/func/actions/workflows/ci.yaml)
[![Client API Documentation](https://pkg.go.dev/badge/knative.dev/func?utm_source=godoc)](https://pkg.go.dev/knative.dev/func)
[![Issues](https://img.shields.io/github/issues/knative/func.svg)](https://github.com/knative/func/issues)
[![License](https://img.shields.io/github/license/knative/func)](https://github.com/knative/func/blob/main/LICENSE)
[![Releases](https://img.shields.io/github/v/release/knative/func.svg?label=Release)](https://github.com/knative/func/releases)
[![codecov](https://codecov.io/gh/knative/func/branch/main/graph/badge.svg)](https://codecov.io/gh/knative/func)
[![Main Build Status](https://github.com/boson-project/func/workflows/Main/badge.svg?branch=main)](https://github.com/boson-project/func/actions?query=workflow%3AMain+branch%3Amain)
[![Develop Build Status](https://github.com/boson-project/func/workflows/Develop/badge.svg?branch=develop&label=develop)](https://github.com/boson-project/func/actions?query=workflow%3ADevelop+branch%3Adevelop)
[![Client API Documentation](https://godoc.org/github.com/boson-project/func?status.svg)](http://godoc.org/github.com/boson-project/func)
[![GitHub Issues](https://img.shields.io/github/issues/boson-project/func.svg)](https://github.com/boson-project/func/issues)
[![License](https://img.shields.io/github/license/boson-project/func)](https://github.com/boson-project/func/blob/main/LICENSE)
[![Release](https://img.shields.io/github/release/boson-project/func.svg?label=Release)](https://github.com/boson-project/func/releases)
`func` is a Client Library and CLI enabling the development and deployment of Functions.
`func` is a Client Library and CLI for enabling the development of implicitly deployed, platform agnostic code.
[Try the QuickStart](https://knative.dev/docs/getting-started/about-knative-functions/)
[Read the Documentation](https://knative.dev/docs/functions/)
## Roadmap
We use GitHub issues and project to track our roadmap. Please see our roadmap [here](https://github.com/orgs/knative/projects/49).
## Knative Function demos and examples
- [Knative Functions in action: Amsterdam City Data App](https://github.com/zroubalik/knative-functions-ams-data-demo/)
[Read the Documentation](docs/README.md)
## Contributing
We are always looking for contributions from the Function Developer community. For more information on how to participate, see the [Contribuiting Guide](docs/CONTRIBUTING.md).
For a list of all help wanted issues in Knative, take a look at [CLOTRIBUTOR](https://clotributor.dev/search?project=knative&page=1).
We are always looking for contributions from the Function Developer community. For more information on how to participate, see the [Contributor's Guide](docs/contributors_guide.md)
The `func` Working Group meets @ 10:00 US Eastern every Tuesday, we'd love to have you! For more information, see the invitation on the [Knative Team Calendar](https://calendar.google.com/calendar/u/0/embed?src=knative.team_9q83bg07qs5b9rrslp5jor4l6s@group.calendar.google.com).
## Roadmap
Our project roadmap can be found: https://github.com/orgs/knative/projects/49

View File

@ -1,7 +0,0 @@
# Knative Security Policy
We're extremely grateful for security researchers and users that report vulnerabilities to the Knative Open Source Community. All reports are thoroughly investigated by a set of community volunteers.
To make a report, please email the private security@knative.team list with the security details and the details expected for all Knative bug reports.
See [Knative Security and Disclosure Information](https://knative.dev/docs/reference/security/) for more details.

102
buildpacks/builder.go Normal file
View File

@ -0,0 +1,102 @@
package buildpacks
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"runtime"
"github.com/buildpacks/pack"
"github.com/buildpacks/pack/logging"
bosonFunc "github.com/boson-project/func"
)
//Builder holds the configuration that will be passed to
//Buildpack builder
type Builder struct {
Verbose bool
}
//NewBuilder builds the new Builder configuration
func NewBuilder() *Builder {
return &Builder{}
}
//RuntimeToBuildpack holds the mapping between the Runtime and its corresponding
//Buildpack builder to use
var RuntimeToBuildpack = map[string]string{
"quarkus": "quay.io/boson/faas-quarkus-builder",
"node": "quay.io/boson/faas-nodejs-builder",
"go": "quay.io/boson/faas-go-builder",
"springboot": "quay.io/boson/faas-springboot-builder",
"python": "quay.io/boson/faas-python-builder",
"typescript": "quay.io/boson/faas-nodejs-builder",
}
// Build the Function at path.
func (builder *Builder) Build(ctx context.Context, f bosonFunc.Function) (err error) {
// Use the builder found in the Function configuration file
// If one isn't found, use the defaults
var packBuilder string
if f.Builder != "" {
packBuilder = f.Builder
pb, ok := f.BuilderMap[packBuilder]
if ok {
packBuilder = pb
}
} else {
packBuilder = RuntimeToBuildpack[f.Runtime]
if packBuilder == "" {
return errors.New(fmt.Sprint("unsupported runtime: ", f.Runtime))
}
}
// Build options for the pack client.
var network string
if runtime.GOOS == "linux" {
network = "host"
}
packOpts := pack.BuildOptions{
AppPath: f.Root,
Image: f.Image,
Builder: packBuilder,
DockerHost: os.Getenv("DOCKER_HOST"),
ContainerConfig: struct {
Network string
Volumes []string
}{Network: network, Volumes: nil},
}
// log output is either STDOUt or kept in a buffer to be printed on error.
var logWriter io.Writer
if builder.Verbose {
logWriter = os.Stdout
} else {
logWriter = &bytes.Buffer{}
}
// Client with a logger which is enabled if in Verbose mode.
packClient, err := pack.NewClient(pack.WithLogger(logging.New(logWriter)))
if err != nil {
return
}
// Build based using the given builder.
if err = packClient.Build(ctx, packOpts); err != nil {
if ctx.Err() != nil {
// received SIGINT
return
} else if !builder.Verbose {
// If the builder was not showing logs, embed the full logs in the error.
err = fmt.Errorf("%v\noutput: %s\n", err, logWriter.(*bytes.Buffer).String())
}
}
return
}

599
client.go Normal file
View File

@ -0,0 +1,599 @@
package function
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
const (
DefaultRegistry = "docker.io"
DefaultRuntime = "node"
DefaultTrigger = "http"
)
// Client for managing Function instances.
type Client struct {
verbose bool // print verbose logs
builder Builder // Builds a runnable image from Function source
pusher Pusher // Pushes the image assocaited with a Function.
deployer Deployer // Deploys or Updates a Function
runner Runner // Runs the Function locally
remover Remover // Removes remote services
lister Lister // Lists remote services
describer Describer
dnsProvider DNSProvider // Provider of DNS services
templates string // path to extensible templates
registry string // default registry for OCI image tags
progressListener ProgressListener // progress listener
emitter Emitter // Emits CloudEvents to functions
}
// ErrNotBuilt indicates the Function has not yet been built.
var ErrNotBuilt = errors.New("not built")
// Builder of Function source to runnable image.
type Builder interface {
// Build a Function project with source located at path.
Build(context.Context, Function) error
}
// Pusher of Function image to a registry.
type Pusher interface {
// Push the image of the Function.
// Returns Image Digest - SHA256 hash of the produced image
Push(ctx context.Context, f Function) (string, error)
}
type Status int
const (
Failed Status = iota
Deployed
Updated
)
type DeploymentResult struct {
Status Status
URL string
}
// Deployer of Function source to running status.
type Deployer interface {
// Deploy a Function of given name, using given backing image.
Deploy(context.Context, Function) (DeploymentResult, error)
}
// Runner runs the Function locally.
type Runner interface {
// Run the Function locally.
Run(context.Context, Function) error
}
// Remover of deployed services.
type Remover interface {
// Remove the Function from remote.
Remove(ctx context.Context, name string) error
}
// Lister of deployed services.
type Lister interface {
// List the Functions currently deployed.
List(ctx context.Context) ([]ListItem, error)
}
type ListItem struct {
Name string `json:"name" yaml:"name"`
Namespace string `json:"namespace" yaml:"namespace"`
Runtime string `json:"runtime" yaml:"runtime"`
URL string `json:"url" yaml:"url"`
Ready string `json:"ready" yaml:"ready"`
}
// ProgressListener is notified of task progress.
type ProgressListener interface {
// SetTotal steps of the given task.
SetTotal(int)
// Increment to the next step with the given message.
Increment(message string)
// Complete signals completion, which is expected to be somewhat different than a step increment.
Complete(message string)
// Done signals a cessation of progress updates. Should be called in a defer statement to ensure
// the progress listener can stop any outstanding tasks such as synchronous user updates.
Done()
}
// Describer of Functions' remote deployed aspect.
type Describer interface {
// Describe the running state of the service as reported by the underlyng platform.
Describe(ctx context.Context, name string) (description Description, err error)
}
type Description struct {
Name string `json:"name" yaml:"name"`
Image string `json:"image" yaml:"image"`
Namespace string `json:"namespace" yaml:"namespace"`
Routes []string `json:"routes" yaml:"routes"`
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
}
type Subscription struct {
Source string `json:"source" yaml:"source"`
Type string `json:"type" yaml:"type"`
Broker string `json:"broker" yaml:"broker"`
}
// DNSProvider exposes DNS services necessary for serving the Function.
type DNSProvider interface {
// Provide the given name by routing requests to address.
Provide(Function) error
}
// Emit CloudEvents to functions
type Emitter interface {
Emit(ctx context.Context, endpoint string) error
}
// New client for Function management.
func New(options ...Option) *Client {
// Instantiate client with static defaults.
c := &Client{
builder: &noopBuilder{output: os.Stdout},
pusher: &noopPusher{output: os.Stdout},
deployer: &noopDeployer{output: os.Stdout},
runner: &noopRunner{output: os.Stdout},
remover: &noopRemover{output: os.Stdout},
lister: &noopLister{output: os.Stdout},
dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &noopProgressListener{},
emitter: &noopEmitter{},
}
// Apply passed options, which take ultimate precidence.
for _, o := range options {
o(c)
}
return c
}
// Option defines a Function which when passed to the Client constructor optionally
// mutates private members at time of instantiation.
type Option func(*Client)
// WithVerbose toggles verbose logging.
func WithVerbose(v bool) Option {
return func(c *Client) {
c.verbose = v
}
}
// WithBuilder provides the concrete implementation of a builder.
func WithBuilder(d Builder) Option {
return func(c *Client) {
c.builder = d
}
}
// WithPusher provides the concrete implementation of a pusher.
func WithPusher(d Pusher) Option {
return func(c *Client) {
c.pusher = d
}
}
// WithDeployer provides the concrete implementation of a deployer.
func WithDeployer(d Deployer) Option {
return func(c *Client) {
c.deployer = d
}
}
// WithRunner provides the concrete implementation of a deployer.
func WithRunner(r Runner) Option {
return func(c *Client) {
c.runner = r
}
}
// WithRemover provides the concrete implementation of a remover.
func WithRemover(r Remover) Option {
return func(c *Client) {
c.remover = r
}
}
// WithLister provides the concrete implementation of a lister.
func WithLister(l Lister) Option {
return func(c *Client) {
c.lister = l
}
}
// WithDescriber provides a concrete implementation of a Function describer.
func WithDescriber(describer Describer) Option {
return func(c *Client) {
c.describer = describer
}
}
// WithProgressListener provides a concrete implementation of a listener to
// be notified of progress updates.
func WithProgressListener(p ProgressListener) Option {
return func(c *Client) {
c.progressListener = p
}
}
// WithDNSProvider proivdes a DNS provider implementation for registering the
// effective DNS name which is either explicitly set via WithName or is derived
// from the root path.
func WithDNSProvider(provider DNSProvider) Option {
return func(c *Client) {
c.dnsProvider = provider
}
}
// WithTemplates sets the location to use for extensible templates.
// Extensible templates are additional templates that exist on disk and are
// not built into the binary.
func WithTemplates(templates string) Option {
return func(c *Client) {
c.templates = templates
}
}
// WithRegistry sets the default registry which is consulted when an image name/tag
// is not explocitly provided. Can be fully qualified, including the registry
// (ex: 'quay.io/myname') or simply the namespace 'myname' which indicates the
// the use of the default registry.
func WithRegistry(registry string) Option {
return func(c *Client) {
c.registry = registry
}
}
// WithEmitter sets a CloudEvent emitter on the client which is capable of sending
// a CloudEvent to an arbitrary function endpoint
func WithEmitter(e Emitter) Option {
return func(c *Client) {
c.emitter = e
}
}
// New Function.
// Use Create, Build and Deploy independently for lower level control.
func (c *Client) New(ctx context.Context, cfg Function) (err error) {
c.progressListener.SetTotal(3)
defer c.progressListener.Done()
// Create local template
err = c.Create(cfg)
if err != nil {
return
}
// Load the now-initialized Function.
f, err := NewFunction(cfg.Root)
if err != nil {
return
}
// Build the now-initialized Function
c.progressListener.Increment("Building container image")
if err = c.Build(ctx, f.Root); err != nil {
return
}
// Deploy the initialized Function, returning its publicly
// addressible name for possible registration.
c.progressListener.Increment("Deploying Function to cluster")
if err = c.Deploy(ctx, f.Root); err != nil {
return
}
// Create an external route to the Function
c.progressListener.Increment("Creating route to Function")
if err = c.Route(f.Root); err != nil {
return
}
c.progressListener.Complete("Done")
// TODO: use the knative client during deployment such that the actual final
// route can be returned from the deployment step, passed to the DNS Router
// for routing actual traffic, and returned here.
if c.verbose {
fmt.Printf("https://%v/\n", f.Name)
}
return
}
// Create a new Function project locally using the settings provided on a
// Function object.
func (c *Client) Create(cfg Function) (err error) {
// Create project root directory, if it doesn't already exist
if err = os.MkdirAll(cfg.Root, 0755); err != nil {
return
}
// Create Function of the given root path.
f, err := NewFunction(cfg.Root)
if err != nil {
return
}
// Assert the specified root is free of visible files and contentious
// hidden files (the ConfigFile, which indicates it is already initialized)
if err = assertEmptyRoot(f.Root); err != nil {
return
}
// Map requested fields to the newly created function.
f.Image = cfg.Image
f.Name = cfg.Name
// Assert runtime was provided, or default.
f.Runtime = cfg.Runtime
if f.Runtime == "" {
f.Runtime = DefaultRuntime
}
// Assert trigger was provided, or default.
f.Trigger = cfg.Trigger
if f.Trigger == "" {
f.Trigger = DefaultTrigger
}
// Write out a template.
w := templateWriter{templates: c.templates, verbose: c.verbose}
if err = w.Write(f.Runtime, f.Trigger, f.Root); err != nil {
return
}
// Check if template specifies a builder image. If so, add to configuration
builderFilePath := filepath.Join(f.Root, ".builders.yaml")
if builderConfig, err := ioutil.ReadFile(builderFilePath); err == nil {
// A .builder file was found. Read the default builder and set in the config file
// TODO: A command line flag could be used to specify non-default builders
builders := make(map[string]string)
if err := yaml.Unmarshal(builderConfig, builders); err == nil {
f.Builder = builders["default"]
if c.verbose {
fmt.Printf("Builder: %s\n", f.Builder)
}
f.BuilderMap = builders
}
// Remove the builders.yaml file so the user is not confused by a
// configuration file that is only used for project creation/initialization
if err := os.Remove(builderFilePath); err != nil {
if c.verbose {
fmt.Printf("Cannot remove %v. %v\n", builderFilePath, err)
}
}
}
// Write out the config.
if err = writeConfig(f); err != nil {
return
}
// TODO: Create a status structure and return it for clients to use
// for output, such as from the CLI.
if c.verbose {
fmt.Println("Function project created")
}
return
}
// Build the Function at path. Errors if the Function is either unloadable or does
// not contain a populated Image.
func (c *Client) Build(ctx context.Context, path string) (err error) {
c.progressListener.Increment("Building function image")
f, err := NewFunction(path)
if err != nil {
return
}
// Derive Image from the path (precedence is given to extant config)
if f.Image, err = DerivedImage(path, c.registry); err != nil {
return
}
if err = c.builder.Build(ctx, f); err != nil {
return
}
// Write out config, which will now contain a populated image tag
// if it had not already
if err = writeConfig(f); err != nil {
return
}
// TODO: create a statu structure and return it here for optional
// use by the cli for user echo (rather than rely on verbose mode here)
message := fmt.Sprintf("🙌 Function image built: %v", f.Image)
c.progressListener.Increment(message)
return
}
// Deploy the Function at path. Errors if the Function has not been
// initialized with an image tag.
func (c *Client) Deploy(ctx context.Context, path string) (err error) {
f, err := NewFunction(path)
if err != nil {
return
}
// Functions must be built (have an associated image) before being deployed.
// Note that externally built images may be specified in the func.yaml
if !f.Built() {
return ErrNotBuilt
}
// Push the image for the named service to the configured registry
c.progressListener.Increment("Pushing function image to the registry")
imageDigest, err := c.pusher.Push(ctx, f)
if err != nil {
return
}
// Store the produced image Digest in the config
f.ImageDigest = imageDigest
if err = writeConfig(f); err != nil {
return
}
// Deploy a new or Update the previously-deployed Function
c.progressListener.Increment("Deploying function to the cluster")
result, err := c.deployer.Deploy(ctx, f)
if result.Status == Deployed {
c.progressListener.Increment(fmt.Sprintf("Function deployed at URL: %v", result.URL))
} else if result.Status == Updated {
c.progressListener.Increment(fmt.Sprintf("Function updated at URL: %v", result.URL))
}
return err
}
func (c *Client) Route(path string) (err error) {
// Ensure that the allocated final address is enabled with the
// configured DNS provider.
// NOTE:
// DNS and TLS are provisioned by Knative Serving + cert-manager,
// but DNS subdomain CNAME to the Kourier Load Balancer is
// still manual, and the initial cluster config to suppot the TLD
// is still manual.
f, err := NewFunction(path)
if err != nil {
return
}
return c.dnsProvider.Provide(f)
}
// Run the Function whose code resides at root.
func (c *Client) Run(ctx context.Context, root string) error {
// Create an instance of a Function representation at the given root.
f, err := NewFunction(root)
if err != nil {
return err
}
if !f.Initialized() {
// TODO: this needs a test.
return fmt.Errorf("the given path '%v' does not contain an initialized Function. Please create one at this path in order to run.", root)
}
// delegate to concrete implementation of runner entirely.
return c.runner.Run(ctx, f)
}
// List currently deployed Functions.
func (c *Client) List(ctx context.Context) ([]ListItem, error) {
// delegate to concrete implementation of lister entirely.
return c.lister.List(ctx)
}
// Describe a Function. Name takes precidence. If no name is provided,
// the Function defined at root is used.
func (c *Client) Describe(ctx context.Context, name, root string) (d Description, err error) {
// If name is provided, it takes precidence.
// Otherwise load the Function defined at root.
if name != "" {
return c.describer.Describe(ctx, name)
}
f, err := NewFunction(root)
if err != nil {
return d, err
}
if !f.Initialized() {
return d, fmt.Errorf("%v is not initialized", f.Name)
}
return c.describer.Describe(ctx, f.Name)
}
// Remove a Function. Name takes precidence. If no name is provided,
// the Function defined at root is used if it exists.
func (c *Client) Remove(ctx context.Context, cfg Function) error {
// If name is provided, it takes precidence.
// Otherwise load the Function deined at root.
if cfg.Name != "" {
return c.remover.Remove(ctx, cfg.Name)
}
f, err := NewFunction(cfg.Root)
if err != nil {
return err
}
if !f.Initialized() {
return fmt.Errorf("Function at %v can not be removed unless initialized. Try removing by name.", f.Root)
}
return c.remover.Remove(ctx, f.Name)
}
// Emit a CloudEvent to a function endpoint
func (c *Client) Emit(ctx context.Context, endpoint string) error {
return c.emitter.Emit(ctx, endpoint)
}
// Manual implementations (noops) of required interfaces.
// In practice, the user of this client package (for example the CLI) will
// provide a concrete implementation for all of the interfaces. For testing or
// development, however, it is usefule that they are defaulted to noops and
// provded only when necessary. Unit tests for the concrete implementations
// serve to keep the core logic here separate from the imperitive.
// -----------------------------------------------------
type noopBuilder struct{ output io.Writer }
func (n *noopBuilder) Build(ctx context.Context, _ Function) error { return nil }
type noopPusher struct{ output io.Writer }
func (n *noopPusher) Push(ctx context.Context, f Function) (string, error) { return "", nil }
type noopDeployer struct{ output io.Writer }
func (n *noopDeployer) Deploy(ctx context.Context, _ Function) (DeploymentResult, error) {
return DeploymentResult{}, nil
}
type noopRunner struct{ output io.Writer }
func (n *noopRunner) Run(_ context.Context, _ Function) error { return nil }
type noopRemover struct{ output io.Writer }
func (n *noopRemover) Remove(context.Context, string) error { return nil }
type noopLister struct{ output io.Writer }
func (n *noopLister) List(context.Context) ([]ListItem, error) { return []ListItem{}, nil }
type noopDNSProvider struct{ output io.Writer }
func (n *noopDNSProvider) Provide(_ Function) error { return nil }
type noopProgressListener struct{}
func (p *noopProgressListener) SetTotal(i int) {}
func (p *noopProgressListener) Increment(m string) {}
func (p *noopProgressListener) Complete(m string) {}
func (p *noopProgressListener) Done() {}
type noopEmitter struct{}
func (p *noopEmitter) Emit(ctx context.Context, endpoint string) error { return nil }

293
client_int_test.go Normal file
View File

@ -0,0 +1,293 @@
// +build integration
package function_test
import (
"context"
"os"
"reflect"
"testing"
"time"
boson "github.com/boson-project/func"
"github.com/boson-project/func/buildpacks"
"github.com/boson-project/func/docker"
"github.com/boson-project/func/knative"
)
/*
NOTE: Running integration tests locally requires a configured test cluster.
Test failures may require manual removal of dangling resources.
## Integration Cluster
These integration tests require a properly configured cluster,
such as that which is setup and configured in CI (see .github/workflows).
A local KinD cluster can be started via:
./hack/allocate.sh && ./hack/configure.sh
## Integration Testing
These tests can be run via the make target:
make test-integration
or manually by specifying the tag
go test -v -tags integration ./...
## Teardown and Cleanup
Tests should clean up after themselves. In the event of failures, one may
need to manually remove files:
rm -rf ./testdata/example.com
The test cluster is not automatically removed, as it can be reused. To remove:
./hack/delete.sh
*/
const (
// DefaultRegistry must contain both the registry host and
// registry namespace at this time. This will likely be
// split and defaulted to the forthcoming in-cluster registry.
DefaultRegistry = "localhost:5000/func"
// DefaultNamespace for the underlying deployments. Must be the same
// as is set up and configured (see hack/configure.sh)
DefaultNamespace = "func"
)
func TestList(t *testing.T) {
verbose := true
// Assemble
lister, err := knative.NewLister(DefaultNamespace)
if err != nil {
t.Fatal(err)
}
client := boson.New(
boson.WithLister(lister),
boson.WithVerbose(verbose))
// Act
names, err := client.List(context.Background())
if err != nil {
t.Fatal(err)
}
// Assert
if len(names) != 0 {
t.Fatalf("Expected no Functions, got %v", names)
}
}
// TestNew creates
func TestNew(t *testing.T) {
defer within(t, "testdata/example.com/testnew")()
verbose := true
client := newClient(verbose)
// Act
if err := client.New(context.Background(), boson.Function{Name: "testnew", Root: ".", Runtime: "go"}); err != nil {
t.Fatal(err)
}
defer del(t, client, "testnew")
// Assert
items, err := client.List(context.Background())
names := []string{}
for _, item := range items {
names = append(names, item.Name)
}
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(names, []string{"testnew"}) {
t.Fatalf("Expected function list ['testnew'], got %v", names)
}
}
// TestDeploy updates
func TestDeploy(t *testing.T) {
defer within(t, "testdata/example.com/deploy")()
verbose := true
client := newClient(verbose)
if err := client.New(context.Background(), boson.Function{Name: "deploy", Root: ".", Runtime: "go"}); err != nil {
t.Fatal(err)
}
defer del(t, client, "deploy")
if err := client.Deploy(context.Background(), "."); err != nil {
t.Fatal(err)
}
}
// TestRemove deletes
func TestRemove(t *testing.T) {
defer within(t, "testdata/example.com/remove")()
verbose := true
client := newClient(verbose)
if err := client.New(context.Background(), boson.Function{Name: "remove", Root: ".", Runtime: "go"}); err != nil {
t.Fatal(err)
}
waitFor(t, client, "remove")
if err := client.Remove(context.Background(), boson.Function{Name: "remove"}); err != nil {
t.Fatal(err)
}
names, err := client.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(names) != 0 {
t.Fatalf("Expected empty Functions list, got %v", names)
}
}
// ***********
// Helpers
// ***********
// newClient creates an instance of the func client whose concrete impls
// match those created by the kn func plugin CLI.
func newClient(verbose bool) *boson.Client {
builder := buildpacks.NewBuilder()
builder.Verbose = verbose
pusher, err := docker.NewPusher()
if err != nil {
panic(err)
}
pusher.Verbose = verbose
deployer, err := knative.NewDeployer(DefaultNamespace)
if err != nil {
panic(err) // TODO: remove error from deployer constructor
}
deployer.Verbose = verbose
remover, err := knative.NewRemover(DefaultNamespace)
if err != nil {
panic(err) // TODO: remove error from remover constructor
}
remover.Verbose = verbose
lister, err := knative.NewLister(DefaultNamespace)
if err != nil {
panic(err) // TODO: remove error from lister constructor
}
lister.Verbose = verbose
return boson.New(
boson.WithRegistry(DefaultRegistry),
boson.WithVerbose(verbose),
boson.WithBuilder(builder),
boson.WithPusher(pusher),
boson.WithDeployer(deployer),
boson.WithRemover(remover),
boson.WithLister(lister),
)
}
// Del cleans up after a test by removing a function by name.
// (test fails if the named function does not exist)
//
// Intended to be run in a defer statement immediately after creation, del
// works around the asynchronicity of the underlying platform's creation
// step by polling the provider until the names function becomes available
// (or the test times out), before firing off a deletion request.
// Of course, ideally this would be replaced by the use of a synchronous
// method, or at a minimum a way to register a callback/listener for the
// creation event. This is what we have for now, and the show must go on.
func del(t *testing.T, c *boson.Client, name string) {
t.Helper()
waitFor(t, c, name)
if err := c.Remove(context.Background(), boson.Function{Name: name}); err != nil {
t.Fatal(err)
}
}
// waitFor the named Function to become available in List output.
// TODO: the API should be synchronous, but that depends first on
// Create returning the derived name such that we can bake polling in.
// Ideally the Boson provider's Creaet would be made syncrhonous.
func waitFor(t *testing.T, c *boson.Client, name string) {
t.Helper()
var pollInterval = 2 * time.Second
for { // ever (i.e. defer to global test timeout)
nn, err := c.List(context.Background())
if err != nil {
t.Fatal(err)
}
for _, n := range nn {
if n.Name == name {
return
}
}
time.Sleep(pollInterval)
}
}
// Create the given directory, CD to it, and return a function which can be
// run in a defer statement to return to the original directory and cleanup.
// Note must be executed, not deferred itself
// NO: defer within(t, "somedir")
// YES: defer within(t, "somedir")()
func within(t *testing.T, root string) func() {
t.Helper()
cwd := pwd(t)
mkdir(t, root)
cd(t, root)
return func() {
cd(t, cwd)
rm(t, root)
}
}
func pwd(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return dir
}
func mkdir(t *testing.T, dir string) {
t.Helper()
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
}
func cd(t *testing.T, dir string) {
t.Helper()
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
}
func rm(t *testing.T, dir string) {
t.Helper()
if err := os.RemoveAll(dir); err != nil {
t.Fatal(err)
}
}
func touch(file string) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
f, err := os.Create(file)
if err != nil {
panic(err)
}
defer f.Close()
}
t := time.Now().Local()
if err := os.Chtimes(file, t, t); err != nil {
panic(err)
}
}

734
client_test.go Normal file
View File

@ -0,0 +1,734 @@
// +build !integration
package function_test
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/mock"
)
// TestRegistry for calculating destination image during tests.
// Will be optional once we support in-cluster container registries
// by default. See TestRegistryRequired for details.
const TestRegistry = "quay.io/alice"
// TestNew Function completes without error using defaults and zero values.
// New is the superset of creating a new fully deployed Function, and
// thus implicitly tests Create, Build and Deploy, which are exposed
// by the client API for those who prefer manual transmissions.
func TestNew(t *testing.T) {
root := "testdata/example.com/testCreate" // Root from which to run the test
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// New Client
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
// New Function using Client
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
}
// TestTemplateWrites ensures a template is written.
func TestTemplateWrites(t *testing.T) {
root := "testdata/example.com/testCreateWrites"
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.Create(bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Assert file was written
if _, err := os.Stat(filepath.Join(root, bosonFunc.ConfigFile)); os.IsNotExist(err) {
t.Fatalf("Initialize did not result in '%v' being written to '%v'", bosonFunc.ConfigFile, root)
}
}
// TestExtantAborts ensures that a directory which contains an extant
// Function does not reinitialize
func TestExtantAborts(t *testing.T) {
root := "testdata/example.com/testCreateInitializedAborts"
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// New once
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// New again should fail as already initialized
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err == nil {
t.Fatal("error expected initilizing a path already containing an initialized Function")
}
}
// TestNonemptyDirectoryAborts ensures that a directory which contains any
// visible files aborts.
func TestNonemptyDirectoryAborts(t *testing.T) {
root := "testdata/example.com/testCreateNonemptyDirectoryAborts" // contains only a single visible file.
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// An unexpected, non-hidden file.
_, err := os.Create(root + "/file.txt")
if err != nil {
t.Fatal(err)
}
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err == nil {
t.Fatal("error expected initilizing a Function in a nonempty directory")
}
}
// TestHiddenFilesIgnored ensures that initializing in a directory that
// only contains hidden files does not error, protecting against the naieve
// implementation of aborting initialization if any files exist, which would
// break functions tracked in source control (.git), or when used in
// conjunction with other tools (.envrc, etc)
func TestHiddenFilesIgnored(t *testing.T) {
// Create a directory for the Function
root := "testdata/example.com/testCreateHiddenFilesIgnored"
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create a hidden file that should be ignored.
hiddenFile := filepath.Join(root, ".envrc")
if err := ioutil.WriteFile(hiddenFile, []byte{}, 0644); err != nil {
t.Fatal(err)
}
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
}
// TestDefaultRuntime ensures that the default runtime is applied to new
// Functions and persisted.
func TestDefaultRuntime(t *testing.T) {
// Create a root for the new Function
root := "testdata/example.com/testCreateDefaultRuntime"
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create a new function at root with all defaults.
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Load the function
f, err := bosonFunc.NewFunction(root)
if err != nil {
t.Fatal(err)
}
// Ensure it has defaulted runtime
if f.Runtime != bosonFunc.DefaultRuntime {
t.Fatal("The default runtime was not applied or persisted.")
}
}
// TestDefaultTemplate ensures that the default template is
// applied when not provided.
func TestDefaultTrigger(t *testing.T) {
// TODO: need to either expose accessor for introspection, or compare
// the files written to those in the embedded repisotory?
}
// TestExtensibleTemplates templates. Ensures that templates are extensible
// using a custom path to a template repository on disk. Custom repository
// location is not defined herein but expected to be provided because, for
// example, a CLI may want to use XDG_CONFIG_HOME. Assuming a repository path
// $FUNC_TEMPLATES, a Go template named 'json' which is provided in the
// repository 'boson-experimental', would be expected to be in the location:
// $FUNC_TEMPLATES/boson-experimental/go/json
// See the CLI for full details, but a standard default location is
// $HOME/.config/templates/boson-experimental/go/json
func TestExtensibleTemplates(t *testing.T) {
// Create a directory for the new Function
root := "testdata/example.com/testExtensibleTemplates"
if err := os.MkdirAll(root, 0744); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create a new client with a path to the extensible templates
client := bosonFunc.New(
bosonFunc.WithTemplates("testdata/templates"),
bosonFunc.WithRegistry(TestRegistry))
// Create a Function specifying a template, 'json' that only exists in the extensible set
if err := client.New(context.Background(), bosonFunc.Function{Root: root, Trigger: "boson-experimental/json"}); err != nil {
t.Fatal(err)
}
// Ensure that a file from that only exists in that template set was actually written 'json.js'
if _, err := os.Stat(filepath.Join(root, "json.js")); os.IsNotExist(err) {
t.Fatalf("Initializing a custom did not result in json.js being written to '%v'", root)
} else if err != nil {
t.Fatal(err)
}
}
// TestUnsupportedRuntime generates an error.
func TestUnsupportedRuntime(t *testing.T) {
// Create a directory for the Function
root := "testdata/example.com/testUnsupportedRuntime"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
// create a Function call witn an unsupported runtime should bubble
// the error generated by the underlying initializer.
if err := client.New(context.Background(), bosonFunc.Function{Root: root, Runtime: "invalid"}); err == nil {
t.Fatal("unsupported runtime did not generate error")
}
}
// TestNamed ensures that an explicitly passed name is used in leau of the
// path derived name when provided, and persists through instantiations.
func TestNamed(t *testing.T) {
// Explicit name to use
name := "service.example.com"
// Path which would derive to testWithHame.example.com were it not for the
// explicitly provided name.
root := "testdata/example.com/testWithName"
// Create a root directory for the Function
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.New(context.Background(), bosonFunc.Function{Root: root, Name: name}); err != nil {
t.Fatal(err)
}
f, err := bosonFunc.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Name != name {
t.Fatalf("expected name '%v' got '%v", name, f.Name)
}
}
// TestRegistryRequired ensures that a registry is required, and is
// prepended with the DefaultRegistry if a single token.
// Registry is the namespace at the container image registry.
// If not prepended with the registry, it will be defaulted:
// Examples: "docker.io/alice"
// "quay.io/bob"
// "charlie" (becomes [DefaultRegistry]/charlie
// At this time a registry namespace is required as we rely on a third-party
// registry in all cases. When we support in-cluster container registries,
// this configuration parameter will become optional.
func TestRegistryRequired(t *testing.T) {
// Create a root for the Function
root := "testdata/example.com/testRegistry"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
client := bosonFunc.New()
var err error
if err = client.New(context.Background(), bosonFunc.Function{Root: root}); err == nil {
t.Fatal("did not receive expected error creating a Function without specifying Registry")
}
fmt.Println(err)
}
// TestDeriveImage ensures that the full image (tag) of the resultant OCI
// container is populated based of a derivation using configured registry
// plus the service name.
func TestDeriveImage(t *testing.T) {
// Create the root Function directory
root := "testdata/example.com/testDeriveImage"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create the function which calculates fields such as name and image.
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Load the function with the now-populated fields.
f, err := bosonFunc.NewFunction(root)
if err != nil {
t.Fatal(err)
}
// In form: [Default Registry]/[Registry Namespace]/[Service Name]:latest
expected := TestRegistry + "/" + f.Name + ":latest"
if f.Image != expected {
t.Fatalf("expected image '%v' got '%v'", expected, f.Image)
}
}
// TestDeriveImageDefaultRegistry ensures that a Registry which does not have
// a registry prefix has the DefaultRegistry prepended.
// For example "alice" becomes "docker.io/alice"
func TestDeriveImageDefaultRegistry(t *testing.T) {
// Create the root Function directory
root := "testdata/example.com/testDeriveImageDefaultRegistry"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create the function which calculates fields such as name and image.
// Rather than use TestRegistry, use a single-token name and expect
// the DefaultRegistry to be prepended.
client := bosonFunc.New(bosonFunc.WithRegistry("alice"))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Load the function with the now-populated fields.
f, err := bosonFunc.NewFunction(root)
if err != nil {
t.Fatal(err)
}
// Expected image is [DefaultRegistry]/[namespace]/[servicename]:latest
expected := bosonFunc.DefaultRegistry + "/alice/" + f.Name + ":latest"
if f.Image != expected {
t.Fatalf("expected image '%v' got '%v'", expected, f.Image)
}
}
// TestDelegation ensures that Create invokes each of the individual
// subcomponents via delegation through Build, Push and
// Deploy (and confirms expected fields calculated).
func TestNewDelegates(t *testing.T) {
var (
root = "testdata/example.com/testCreateDelegates" // .. in which to initialize
expectedName = "testCreateDelegates" // expected to be derived
expectedImage = "quay.io/alice/testCreateDelegates:latest"
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployer()
)
// Create a directory for the new Function
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create a client with mocks for each of the subcomponents.
client := bosonFunc.New(
bosonFunc.WithRegistry(TestRegistry),
bosonFunc.WithBuilder(builder), // builds an image
bosonFunc.WithPusher(pusher), // pushes images to a registry
bosonFunc.WithDeployer(deployer), // deploys images as a running service
)
// Register Function delegates on the mocks which validate assertions
// -------------
// The builder should be invoked with a path to a Function project's source
// An example image name is returned.
builder.BuildFn = func(f bosonFunc.Function) error {
expectedPath, err := filepath.Abs(root)
if err != nil {
t.Fatal(err)
}
if expectedPath != f.Root {
t.Fatalf("builder expected path %v, got '%v'", expectedPath, f.Root)
}
return nil
}
pusher.PushFn = func(f bosonFunc.Function) (string, error) {
if f.Image != expectedImage {
t.Fatalf("pusher expected image '%v', got '%v'", expectedImage, f.Image)
}
return "", nil
}
deployer.DeployFn = func(f bosonFunc.Function) error {
if f.Name != expectedName {
t.Fatalf("deployer expected name '%v', got '%v'", expectedName, f.Name)
}
if f.Image != expectedImage {
t.Fatalf("deployer expected image '%v', got '%v'", expectedImage, f.Image)
}
return nil
}
// Invocation
// -------------
// Invoke the creation, triggering the Function delegates, and
// perform follow-up assertions that the Functions were indeed invoked.
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Confirm that each delegate was invoked.
if !builder.BuildInvoked {
t.Fatal("builder was not invoked")
}
if !pusher.PushInvoked {
t.Fatal("pusher was not invoked")
}
if !deployer.DeployInvoked {
t.Fatal("deployer was not invoked")
}
}
// TestRun ensures that the runner is invoked with the absolute path requested.
func TestRun(t *testing.T) {
// Create the root Function directory
root := "testdata/example.com/testRun"
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Create a client with the mock runner and the new test Function
runner := mock.NewRunner()
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry), bosonFunc.WithRunner(runner))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Run the newly created function
if err := client.Run(context.Background(), root); err != nil {
t.Fatal(err)
}
// Assert the runner was invoked, and with the expected root.
if !runner.RunInvoked {
t.Fatal("run did not invoke the runner")
}
absRoot, err := filepath.Abs(root)
if err != nil {
t.Fatal(err)
}
if runner.RootRequested != absRoot {
t.Fatalf("expected path '%v', got '%v'", absRoot, runner.RootRequested)
}
}
// TestUpdate ensures that the deployer properly invokes the build/push/deploy
// process, erroring if run on a directory uncreated.
func TestUpdate(t *testing.T) {
var (
root = "testdata/example.com/testUpdate"
expectedName = "testUpdate"
expectedImage = "quay.io/alice/testUpdate:latest"
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployer()
)
// Create the root Function directory
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// A client with mocks whose implementaton will validate input.
client := bosonFunc.New(
bosonFunc.WithRegistry(TestRegistry),
bosonFunc.WithBuilder(builder),
bosonFunc.WithPusher(pusher),
bosonFunc.WithDeployer(deployer))
// create the new Function which will be updated
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Builder whose implementation verifies the expected root
builder.BuildFn = func(f bosonFunc.Function) error {
rootPath, err := filepath.Abs(root)
if err != nil {
t.Fatal(err)
}
if f.Root != rootPath {
t.Fatalf("builder expected path %v, got '%v'", rootPath, f.Root)
}
return nil
}
// Pusher whose implementaiton verifies the expected image
pusher.PushFn = func(f bosonFunc.Function) (string, error) {
if f.Image != expectedImage {
t.Fatalf("pusher expected image '%v', got '%v'", expectedImage, f.Image)
}
// image of given name wouold be pushed to the configured registry.
return "", nil
}
// Update whose implementaiton verifed the expected name and image
deployer.DeployFn = func(f bosonFunc.Function) error {
if f.Name != expectedName {
t.Fatalf("updater expected name '%v', got '%v'", expectedName, f.Name)
}
if f.Image != expectedImage {
t.Fatalf("updater expected image '%v', got '%v'", expectedImage, f.Image)
}
return nil
}
// Invoke the creation, triggering the Function delegates, and
// perform follow-up assertions that the Functions were indeed invoked.
if err := client.Deploy(context.Background(), root); err != nil {
t.Fatal(err)
}
if !builder.BuildInvoked {
t.Fatal("builder was not invoked")
}
if !pusher.PushInvoked {
t.Fatal("pusher was not invoked")
}
if !deployer.DeployInvoked {
t.Fatal("deployer was not invoked")
}
}
// TestRemoveByPath ensures that the remover is invoked to remove
// the Function with the name of the function at the provided root.
func TestRemoveByPath(t *testing.T) {
var (
root = "testdata/example.com/testRemoveByPath"
expectedName = "testRemoveByPath"
remover = mock.NewRemover()
)
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
client := bosonFunc.New(
bosonFunc.WithRegistry(TestRegistry),
bosonFunc.WithRemover(remover))
if err := client.New(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
remover.RemoveFn = func(name string) error {
if name != expectedName {
t.Fatalf("Expected to remove '%v', got '%v'", expectedName, name)
}
return nil
}
if err := client.Remove(context.Background(), bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
if !remover.RemoveInvoked {
t.Fatal("remover was not invoked")
}
}
// TestRemoveByName ensures that the remover is invoked to remove the function
// of the name provided, with precidence over a provided root path.
func TestRemoveByName(t *testing.T) {
var (
root = "testdata/example.com/testRemoveByPath"
expectedName = "explicitName.example.com"
remover = mock.NewRemover()
)
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
client := bosonFunc.New(
bosonFunc.WithRegistry(TestRegistry),
bosonFunc.WithRemover(remover))
if err := client.Create(bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
remover.RemoveFn = func(name string) error {
if name != expectedName {
t.Fatalf("Expected to remove '%v', got '%v'", expectedName, name)
}
return nil
}
// Run remove with only a name
if err := client.Remove(context.Background(), bosonFunc.Function{Name: expectedName}); err != nil {
t.Fatal(err)
}
// Run remove with a name and a root, which should be ignored in favor of the name.
if err := client.Remove(context.Background(), bosonFunc.Function{Name: expectedName, Root: root}); err != nil {
t.Fatal(err)
}
if !remover.RemoveInvoked {
t.Fatal("remover was not invoked")
}
}
// TestRemoveUninitializedFails ensures that attempting to remove a Function
// by path only (no name) fails unless the Function has been initialized. I.e.
// the name will not be derived from path and the Function removed by this
// derived name; which could be unexpected and destructive.
func TestRemoveUninitializedFails(t *testing.T) {
var (
root = "testdata/example.com/testRemoveUninitializedFails"
remover = mock.NewRemover()
)
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// remover fails if invoked
remover.RemoveFn = func(name string) error {
return fmt.Errorf("remove invoked for unitialized Function %v", name)
}
// Instantiate the client with the failing remover.
client := bosonFunc.New(
bosonFunc.WithRegistry(TestRegistry),
bosonFunc.WithRemover(remover))
// Attempt to remove by path (uninitialized), expecting an error.
if err := client.Remove(context.Background(), bosonFunc.Function{Root: root}); err == nil {
t.Fatalf("did not received expeced error removing an uninitialized func")
}
}
// TestList merely ensures that the client invokes the configured lister.
func TestList(t *testing.T) {
lister := mock.NewLister()
client := bosonFunc.New(bosonFunc.WithLister(lister)) // lists deployed Functions.
if _, err := client.List(context.Background()); err != nil {
t.Fatal(err)
}
if !lister.ListInvoked {
t.Fatal("list did not invoke lister implementation")
}
}
// TestListOutsideRoot ensures that a call to a Function (in this case list)
// that is not contextually dependent on being associated with a Function,
// can be run from anywhere, thus ensuring that the client itself makes
// a distinction between Function-scoped methods and not.
func TestListOutsideRoot(t *testing.T) {
lister := mock.NewLister()
// Instantiate in the current working directory, with no name.
client := bosonFunc.New(bosonFunc.WithLister(lister))
if _, err := client.List(context.Background()); err != nil {
t.Fatal(err)
}
if !lister.ListInvoked {
t.Fatal("list did not invoke lister implementation")
}
}
// TestDeployUnbuilt ensures that a call to deploy a Function which was not
// fully created (ie. was only initialized, not actually built and deploys)
// yields an expected, and informative, error.
func TestDeployUnbuilt(t *testing.T) {
root := "testdata/example.com/testDeploy" // Root from which to run the test
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// New Client
client := bosonFunc.New(bosonFunc.WithRegistry(TestRegistry))
// Initialize (half-create) a new Function at root
if err := client.Create(bosonFunc.Function{Root: root}); err != nil {
t.Fatal(err)
}
// Now try to deploy it. Ie. without having run the necessary build step.
err := client.Deploy(context.Background(), root)
if err == nil {
t.Fatal("did not receive an error attempting to deploy an unbuilt Function")
}
if !errors.Is(err, bosonFunc.ErrNotBuilt) {
t.Fatalf("did not receive expected error type. Expected ErrNotBuilt, got %T", err)
}
}
func TestEmit(t *testing.T) {
sink := "http://testy.mctestface.com"
emitter := mock.NewEmitter()
// Ensure sink passthrough from client
emitter.EmitFn = func(s string) error {
if s != sink {
t.Fatalf("Unexpected sink %v\n", s)
}
return nil
}
// Instantiate in the current working directory, with no name.
client := bosonFunc.New(bosonFunc.WithEmitter(emitter))
if err := client.Emit(context.Background(), sink); err != nil {
t.Fatal(err)
}
if !emitter.EmitInvoked {
t.Fatal("Client did not invoke emitter.Emit()")
}
}
// TODO: The tests which confirm an error is generated do not currently test
// that the expected error is received; just that any error is generated.
// This should be replaced with typed errors or at a minimum code prefixes
// on the string to avoid tests passing for unrelated errors.

66
cloudevents/emitter.go Normal file
View File

@ -0,0 +1,66 @@
package cloudevents
import (
"context"
"fmt"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/cloudevents/sdk-go/v2/client"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol/http"
"github.com/cloudevents/sdk-go/v2/types"
"github.com/google/uuid"
)
const (
DefaultSource = "/boson/fn"
DefaultType = "boson.fn"
)
type Emitter struct {
Endpoint string
Source string
Type string
Id string
Data string
ContentType string
}
func NewEmitter() *Emitter {
return &Emitter{
Source: DefaultSource,
Type: DefaultType,
Id: uuid.NewString(),
Data: "",
ContentType: event.TextPlain,
}
}
func (e *Emitter) Emit(ctx context.Context, endpoint string) (err error) {
c, err := newClient(endpoint)
if err != nil {
return
}
evt := event.Event{
Context: event.EventContextV1{
Type: e.Type,
Source: *types.ParseURIRef(e.Source),
ID: e.Id,
}.AsV1(),
}
if err = evt.SetData(e.ContentType, e.Data); err != nil {
return
}
if result := c.Send(ctx, evt); cloudevents.IsUndelivered(result) {
return fmt.Errorf(result.Error())
}
return nil
}
func newClient(target string) (c client.Client, err error) {
p, err := http.New(http.WithTarget(target))
if err != nil {
return
}
return client.New(p)
}

140
cloudevents/emitter_test.go Normal file
View File

@ -0,0 +1,140 @@
package cloudevents
import (
"context"
"fmt"
"testing"
"time"
"github.com/cloudevents/sdk-go/v2/client"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol/http"
"github.com/google/go-cmp/cmp"
)
func makeClient(t *testing.T) (c client.Client, p *http.Protocol) {
p, err := http.New()
if err != nil {
t.Fatal(err)
}
c, err = client.New(p)
if err != nil {
t.Errorf("failed to make client %s", err.Error())
}
return
}
func receiveEvents(t *testing.T, ctx context.Context, events chan<- event.Event) (p *http.Protocol) {
c, p := makeClient(t)
go func() {
err := c.StartReceiver(ctx, func(ctx context.Context, event event.Event) error {
go func() {
events <- event
}()
return nil
})
if err != nil {
t.Errorf("failed to start receiver %s", err.Error())
}
}()
time.Sleep(1 * time.Second) // let the server start
return
}
func TestEmitterDefaults(t *testing.T) {
events := make(chan event.Event)
ctx, cancel := context.WithCancel(context.Background())
// start a cloudevent client that receives events
// and sends them to a channel
p := receiveEvents(t, ctx, events)
emitter := NewEmitter()
if err := emitter.Emit(ctx, fmt.Sprintf("http://localhost:%v", p.GetListeningPort())); err != nil {
t.Fatalf("Error emitting event: %v\n", err)
}
// received event
got := <-events
cancel() // stop the client
time.Sleep(1 * time.Second) // let the server stop
if got.Source() != "/boson/fn" {
t.Fatal("Expected /boson/fn as default source")
}
if got.Type() != "boson.fn" {
t.Fatal("Expected boson.fn as default type")
}
}
func TestEmitter(t *testing.T) {
testCases := map[string]struct {
cesource string
cetype string
ceid string
cedata string
}{
"with-source": {
cesource: "/my/source",
},
"with-type": {
cetype: "my.type",
},
"with-id": {
ceid: "11223344",
},
"with-data": {
cedata: "Some event data",
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
events := make(chan event.Event)
ctx, cancel := context.WithCancel(context.Background())
// start a cloudevent client that receives events
// and sends them to a channel
p := receiveEvents(t, ctx, events)
emitter := NewEmitter()
if tc.cesource != "" {
emitter.Source = tc.cesource
}
if tc.cetype != "" {
emitter.Type = tc.cetype
}
if tc.ceid != "" {
emitter.Id = tc.ceid
}
if tc.cedata != "" {
emitter.Data = tc.cedata
}
if err := emitter.Emit(ctx, fmt.Sprintf("http://localhost:%v", p.GetListeningPort())); err != nil {
t.Fatalf("Error emitting event: %v\n", err)
}
// received event
got := <-events
cancel() // stop the client
time.Sleep(100 * time.Millisecond) // let the server stop
if tc.cesource != "" && got.Source() != tc.cesource {
t.Fatalf("%s: Expected %s as source, got %s", n, tc.cesource, got.Source())
}
if tc.cetype != "" && got.Type() != tc.cetype {
t.Fatalf("%s: Expected %s as type, got %s", n, tc.cetype, got.Type())
}
if tc.ceid != "" && got.ID() != tc.ceid {
t.Fatalf("%s: Expected %s as id, got %s", n, tc.ceid, got.ID())
}
if tc.cedata != "" {
if diff := cmp.Diff(tc.cedata, string(got.Data())); diff != "" {
t.Errorf("Unexpected difference (-want, +got): %v", diff)
}
}
})
}
}

View File

@ -1,436 +1,169 @@
package cmd
import (
"context"
"errors"
"fmt"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/builders"
pack "knative.dev/func/pkg/builders/buildpacks"
"knative.dev/func/pkg/builders/s2i"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/oci"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/buildpacks"
"github.com/boson-project/func/progress"
"github.com/boson-project/func/prompt"
)
func NewBuildCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
func init() {
root.AddCommand(buildCmd)
buildCmd.Flags().StringP("builder", "b", "", "Buildpack builder, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml for subsequent builds.")
buildCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
buildCmd.Flags().StringP("image", "i", "", "Full image name in the orm [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE")
buildCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
buildCmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
err := buildCmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList)
if err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
}
var buildCmd = &cobra.Command{
Use: "build",
Short: "Build a function container",
Long: `
NAME
{{rootCmdUse}} build - Build a function container locally without deploying
Short: "Build a function project as a container image",
Long: `Build a function project as a container image
SYNOPSIS
{{rootCmdUse}} build [-r|--registry] [--builder] [--builder-image]
[--push] [--username] [--password] [--token]
[--platform] [-p|--path] [-c|--confirm] [-v|--verbose]
[--build-timestamp] [--registry-insecure]
This command builds the function project in the current directory or in the directory
specified by --path. The result will be a container image that is pushed to a registry.
The func.yaml file is read to determine the image name and registry.
If the project has not already been built, either --registry or --image must be provided
and the image name is stored in the configuration file.
`,
Example: `
# Build from the local directory, using the given registry as target.
# The full image name will be determined automatically based on the
# project directory name
kn func build --registry quay.io/myuser
DESCRIPTION
# Build from the local directory, specifying the full image name
kn func build --image quay.io/myuser/myfunc
Builds a function's container image and optionally pushes it to the
configured container registry.
By default building is handled automatically when deploying (see the deploy
subcommand). However, sometimes it is useful to build a function container
outside of this normal deployment process, for example for testing or during
composition when integrating with other systems. Additionally, the container
can be pushed to the configured registry using the --push option.
When building a function for the first time, either a registry or explicit
image name is required. Subsequent builds will reuse these option values.
EXAMPLES
o Build a function container using the given registry.
The full image name will be calculated using the registry and function name.
$ {{rootCmdUse}} build --registry registry.example.com/alice
o Build a function container using an explicit image name, ignoring registry
and function name.
$ {{rootCmdUse}} build --image registry.example.com/alice/f:latest
o Rebuild a function using prior values to determine container name.
$ {{rootCmdUse}} build
o Build a function specifying the Source-to-Image (S2I) builder
$ {{rootCmdUse}} build --builder=s2i
o Build a function specifying the Pack builder with a custom Buildpack
builder image.
$ {{rootCmdUse}} build --builder=pack --builder-image=cnbs/sample-builder:bionic
# Re-build, picking up a previously supplied image name from a local func.yml
kn func build
# Build with a custom buildpack builder
kn func build --builder cnbs/sample-builder:bionic
`,
SuggestFor: []string{"biuld", "buidl", "built"},
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm",
"push", "builder-image", "base-image", "platform", "verbose",
"build-timestamp", "registry-insecure", "username", "password", "token"),
RunE: func(cmd *cobra.Command, args []string) error {
return runBuild(cmd, args, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Function Context
f, _ := fn.NewFunction(effectivePath())
if f.Initialized() {
cfg = cfg.Apply(f) // defined values on f take precedence over cfg defaults
}
// Flags
//
// NOTE on flag defaults:
// Use the config value when available, as this will include global static
// defaults, user settings and the value from the function with context.
// Use the function struct for flag flags which are not globally configurable
//
// Globally-Configurable Flags:
// Options whose value may be defined globally may also exist on the
// contextually relevant function; sets are flattened above via cfg.Apply(f)
cmd.Flags().StringP("builder", "b", cfg.Builder,
fmt.Sprintf("Builder to use when creating the function's container. Currently supported builders are %s. ($FUNC_BUILDER)", KnownBuilders()))
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 ($FUNC_REGISTRY_INSECURE)")
// Function-Context Flags:
// Options whose value is available on the function with context only
// (persisted but not globally configurable)
builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().StringP("builder-image", "", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("base-image", "", f.Build.BaseImage,
"Override the base image for your function (host builder only)")
cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE)")
// Static Flags:
// Options which are either empty or have static defaults only (not
// globally configurable nor persisted with the function)
cmd.Flags().BoolP("push", "u", false,
"Attempt to push the function image to the configured registry after being successfully built")
cmd.Flags().StringP("platform", "", "",
"Optionally specify a target platform, for example \"linux/amd64\" when using the s2i build strategy")
cmd.Flags().StringP("username", "", "",
"Username to use when pushing to the registry.")
cmd.Flags().StringP("password", "", "",
"Password to use when pushing to the registry.")
cmd.Flags().StringP("token", "", "",
"Token to use when pushing to the registry.")
cmd.Flags().BoolP("build-timestamp", "", false, "Use the actual time as the created time for the docker image. This is only useful for buildpacks builder.")
// Temporarily Hidden Basic Auth Flags
// Username, Password and Token flags, which plumb through basic auth, are
// currently only available on the "host" builder.
_ = cmd.Flags().MarkHidden("username")
_ = cmd.Flags().MarkHidden("password")
_ = cmd.Flags().MarkHidden("token")
// Oft-shared flags:
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
// Tab Completion
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
if err := cmd.RegisterFlagCompletionFunc("builder-image", CompleteBuilderImageList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
return cmd
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm"),
RunE: runBuild,
}
func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) {
var (
cfg buildConfig
f fn.Function
)
if cfg, err = newBuildConfig().Prompt(); err != nil { // gather values into a single instruction set
return
}
if err = cfg.Validate(); err != nil { // Perform any pre-validation
return
}
if f, err = fn.NewFunction(cfg.Path); err != nil { // Read in the Function
return
}
if !f.Initialized() {
return fn.NewErrNotInitialized(f.Root)
}
f = cfg.Configure(f) // Returns an f updated with values from the config (flags, envs, etc)
func runBuild(cmd *cobra.Command, _ []string) (err error) {
config := newBuildConfig().Prompt()
cmd.SetContext(cfg.WithValues(cmd.Context())) // Some optional settings are passed via context
// Client
clientOptions, err := cfg.clientOptions()
function, err := functionWithOverrides(config.Path, functionOverrides{Builder: config.Builder, Image: config.Image})
if err != nil {
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, clientOptions...)
defer done()
// Build
buildOptions, err := cfg.buildOptions() // build-specific options from the finalized cfg
// Check if the Function has been initialized
if !function.Initialized() {
return fmt.Errorf("the given path '%v' does not contain an initialized function. Please create one at this path before deploying", config.Path)
}
// If the Function does not yet have an image name and one was not provided on the command line
if function.Image == "" {
// AND a --registry was not provided, then we need to
// prompt for a registry from which we can derive an image name.
if config.Registry == "" {
fmt.Print("A registry for function images is required (e.g. 'quay.io/boson').\n\n")
config.Registry = prompt.ForString("Registry for function images", "")
if config.Registry == "" {
return fmt.Errorf("unable to determine function image name")
}
}
// We have the registry, so let's use it to derive the Function image name
config.Image = deriveImage(config.Image, config.Registry, config.Path)
function.Image = config.Image
}
// All set, let's write changes in the config to the disk
err = function.WriteConfig()
if err != nil {
return
}
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return
}
if cfg.Push {
if f, _, err = client.Push(cmd.Context(), f); err != nil {
return
}
}
if err = f.Write(); err != nil {
return
}
// Stamp is a performance optimization: treat the function as being built
// (cached) unless the fs changes.
return f.Stamp()
}
// WithValues returns a context populated with values from the build config
// which are provided to the system via the context.
func (c buildConfig) WithValues(ctx context.Context) context.Context {
// Push
ctx = context.WithValue(ctx, fn.PushUsernameKey{}, c.Username)
ctx = context.WithValue(ctx, fn.PushPasswordKey{}, c.Password)
ctx = context.WithValue(ctx, fn.PushTokenKey{}, c.Token)
return ctx
builder := buildpacks.NewBuilder()
builder.Verbose = config.Verbose
listener := progress.New()
listener.Verbose = config.Verbose
defer listener.Done()
context := cmd.Context()
go func() {
<-context.Done()
listener.Done()
}()
client := bosonFunc.New(
bosonFunc.WithVerbose(config.Verbose),
bosonFunc.WithRegistry(config.Registry), // for deriving image name when --image not provided explicitly.
bosonFunc.WithBuilder(builder),
bosonFunc.WithProgressListener(listener))
return client.Build(context, config.Path)
}
type buildConfig struct {
// Globals (builder, confirm, registry, verbose)
config.Global
// BuilderImage is the image (name or mapping) to use for building. Usually
// set automatically.
BuilderImage string
// Image name in full, including registry, repo and tag (overrides
// image name derivation based on registry and function name)
// image name derivation based on Registry and Function Name)
Image string
// BaseImage is an image to build a function upon (host builder only)
// TODO: gauron99 -- make option to add a path to dockerfile ?
BaseImage string
// Path of the function implementation on local disk. Defaults to current
// Path of the Function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// Platform ofr resultant image (s2i builder only)
Platform string
// Push the resulting image to the registry after building.
Push bool
// Username when specifying optional basic auth.
Username string
// Registry at which interstitial build artifacts should be kept.
// This setting is ignored if Image is specified, which includes the full
Registry string
// Password when using optional basic auth. Should be provided along
// with Username.
Password string
// Verbose logging.
Verbose bool
// Token when performing basic auth using a bearer token. Should be
// exclusive with Username and Password.
Token string
// Build with the current timestamp as the created time for docker image.
// This is only useful for buildpacks builder.
WithTimestamp bool
// Confirm: confirm values arrived upon from environment plus flags plus defaults,
// with interactive prompting (only applicable when attached to a TTY).
Confirm bool
Builder string
}
// newBuildConfig gathers options into a single build request.
func newBuildConfig() buildConfig {
return buildConfig{
Global: config.Global{
Builder: viper.GetString("builder"),
Confirm: viper.GetBool("confirm"),
Registry: registry(), // deferred defaulting
Verbose: viper.GetBool("verbose"),
RegistryInsecure: viper.GetBool("registry-insecure"),
},
BuilderImage: viper.GetString("builder-image"),
BaseImage: viper.GetString("base-image"),
Image: viper.GetString("image"),
Path: viper.GetString("path"),
Platform: viper.GetString("platform"),
Push: viper.GetBool("push"),
Username: viper.GetString("username"),
Password: viper.GetString("password"),
Token: viper.GetString("token"),
WithTimestamp: viper.GetBool("build-timestamp"),
Registry: viper.GetString("registry"),
Verbose: viper.GetBool("verbose"), // defined on root
Confirm: viper.GetBool("confirm"),
Builder: viper.GetString("builder"),
}
}
// Configure the given function. Updates a function struct with all
// configurable values. Note that buildConfig already includes function's
// current values, as they were passed through via flag defaults, so overwriting
// is a noop.
func (c buildConfig) Configure(f fn.Function) fn.Function {
f = c.Global.Configure(f)
if f.Build.Builder != "" && c.BuilderImage != "" {
f.Build.BuilderImages[f.Build.Builder] = c.BuilderImage
}
f.Image = c.Image
f.Build.BaseImage = c.BaseImage
// Path, Platform and Push are not part of a function's state.
return f
}
// Prompt the user with value of config members, allowing for interactive changes.
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
// all prompts) was set (default).
func (c buildConfig) Prompt() (buildConfig, error) {
if !interactiveTerminal() {
return c, nil
func (c buildConfig) Prompt() buildConfig {
imageName := deriveImage(c.Image, c.Registry, c.Path)
if !interactiveTerminal() || !c.Confirm {
return c
}
// If there is no registry nor explicit image name defined, the
// Registry prompt is shown whether or not we are in confirm mode.
// Otherwise, it is only showin if in confirm mode
// NOTE: the default in this latter situation will ignore the current function
// value and will always use the value from the config (flag or env variable).
// This is not strictly correct and will be fixed when Global Config: Function
// Context is available (PR#1416)
f, err := fn.NewFunction(c.Path)
if err != nil {
return c, err
return buildConfig{
Path: prompt.ForString("Path to project directory", c.Path),
Image: prompt.ForString("Full image name (e.g. quay.io/boson/node-sample)", imageName, prompt.WithRequired(true)),
Verbose: c.Verbose,
// Registry not prompted for as it would be confusing when combined with explicit image. Instead it is
// inferred by the derived default for Image, which uses Registry for derivation.
}
if (f.Registry == "" && c.Registry == "" && c.Image == "") || c.Confirm {
fmt.Println("A registry for function images is required. For example, 'docker.io/tigerteam'.")
err := survey.AskOne(
&survey.Input{Message: "Registry for function images:", Default: c.Registry},
&c.Registry,
survey.WithValidator(NewRegistryValidator(c.Path)))
if err != nil {
return c, fn.ErrRegistryRequired
}
fmt.Println("Note: building a function the first time will take longer than subsequent builds")
}
// Remainder of prompts are optional and only shown if in --confirm mode
if !c.Confirm {
return c, nil
}
// Image Name Override
// Calculate a better image name message which shows the value of the final
// image name as it will be calculated if an explicit image name is not used.
qs := []*survey.Question{
{
Name: "image",
Prompt: &survey.Input{
Message: "Optionally specify an exact image name to use (e.g. quay.io/boson/node-sample:latest)",
},
},
{
Name: "path",
Prompt: &survey.Input{
Message: "Project path:",
Default: c.Path,
},
},
}
//
// TODO(lkingland): add confirmation prompts for other config members here
//
err = survey.Ask(qs, &c)
return c, err
}
// Validate the config passes an initial consistency check
func (c buildConfig) Validate() (err error) {
// Builder value must refer to a known builder short name
if err = ValidateBuilder(c.Builder); err != nil {
return
}
// Platform is only supported with the S2I builder at this time
if c.Platform != "" && c.Builder != builders.S2I {
err = errors.New("only S2I builds currently support specifying platform")
return
}
// BaseImage is only supported with the host builder
if c.BaseImage != "" && c.Builder != "host" {
err = errors.New("only host builds support specifying the base image")
}
return
}
// clientOptions returns options suitable for instantiating a client based on
// the current state of the build config object.
// This will be unnecessary and refactored away when the host-based OCI
// builder and pusher are the default implementations and the Pack and S2I
// constructors simplified.
//
// TODO: Platform is currently only used by the S2I builder. This should be
// a multi-valued argument which passes through to the "host" builder (which
// supports multi-arch/platform images), and throw an error if either trying
// to specify a platform for buildpacks, or trying to specify more than one
// for S2I.
//
// TODO: As a further optimization, it might be ideal to only build the
// image necessary for the target cluster, since the end product of a function
// deployment is not the contiainer, but rather the running service.
func (c buildConfig) clientOptions() ([]fn.Option, error) {
o := []fn.Option{fn.WithRegistry(c.Registry)}
switch c.Builder {
case builders.Host:
o = append(o,
fn.WithBuilder(oci.NewBuilder(builders.Host, c.Verbose)),
fn.WithPusher(oci.NewPusher(c.RegistryInsecure, false, c.Verbose)))
case builders.Pack:
o = append(o,
fn.WithBuilder(pack.NewBuilder(
pack.WithName(builders.Pack),
pack.WithTimestamp(c.WithTimestamp),
pack.WithVerbose(c.Verbose))))
case builders.S2I:
o = append(o,
fn.WithBuilder(s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithVerbose(c.Verbose))))
default:
return o, builders.ErrUnknownBuilder{Name: c.Builder, Known: KnownBuilders()}
}
return o, nil
}
// buildOptions returns options for use with the client.Build request
func (c buildConfig) buildOptions() (oo []fn.BuildOption, err error) {
oo = []fn.BuildOption{}
// Platforms
//
// TODO: upgrade --platform to a multi-value field. The individual builder
// implementations are responsible for bubbling an error if they do
// not support this. Pack supports none, S2I supports one, host builder
// supports multi.
if c.Platform != "" {
parts := strings.Split(c.Platform, "/")
if len(parts) != 2 {
return oo, fmt.Errorf("the value for --patform must be in the form [OS]/[Architecture]. eg \"linux/amd64\"")
}
oo = append(oo, fn.BuildWithPlatforms([]fn.Platform{{OS: parts[0], Architecture: parts[1]}}))
}
return
}

View File

@ -1,160 +0,0 @@
package cmd
import (
"errors"
"testing"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)
// TestBuild_BuilderPersists ensures that the builder chosen is read from
// the function by default, and is able to be overridden by flags/env vars.
func TestBuild_BuilderPersists(t *testing.T) {
testBuilderPersists(NewBuildCmd, t)
}
// TestBuild_ValidateBuilder ensures that the validation function correctly
// identifies valid and invalid builder short names.
func TestBuild_BuilderValidated(t *testing.T) {
testBuilderValidated(NewBuildCmd, t)
}
// TestBuild_ConfigApplied ensures that the build command applies config
// settings at each level (static, global, function, envs, flags).
func TestBuild_ConfigApplied(t *testing.T) {
testConfigApplied(NewBuildCmd, t)
}
// TestBuild_ConfigPrecedence ensures that the correct precidence for config
// are applied: static < global < function context < envs < flags
func TestBuild_ConfigPrecedence(t *testing.T) {
testConfigPrecedence(NewBuildCmd, t)
}
// TestBuild_Default ensures that running build on a valid default Function
// (only required options populated; all else default) completes successfully.
func TestBuild_Default(t *testing.T) {
testDefault(NewBuildCmd, t)
}
// TestBuild_FunctionContext ensures that the function contectually relevant
// to the current command execution is loaded and used for flag defaults by
// spot-checking the builder setting.
func TestBuild_FunctionContext(t *testing.T) {
testFunctionContext(NewBuildCmd, t)
}
// TestBuild_ImageFlag ensures that the image flag is used when specified.
func TestBuild_ImageFlag(t *testing.T) {
testImageFlag(NewBuildCmd, t)
}
// TestBuild_ImageAndRegistry ensures that image is derived when --registry
// is provided without --image; that --image is used if provided; that when
// both are provided, they are both passed to the builder; and that these
// values are persisted.
func TestBuild_ImageAndRegistry(t *testing.T) {
testImageAndRegistry(NewBuildCmd, t)
}
// TestBuild_InvalidRegistry ensures that providing an invalid registry
// fails with the expected error.
func TestBuild_InvalidRegistry(t *testing.T) {
testInvalidRegistry(NewBuildCmd, t)
}
// TestBuild_Registry ensures that a function's registry member is kept in
// sync with the image tag.
// During normal operation (using the client API) a function's state on disk
// will be in a valid state, but it is possible that a function could be
// manually edited, necessitating some kind of consistency recovery (as
// preferable to just an error).
func TestBuild_Registry(t *testing.T) {
testRegistry(NewBuildCmd, t)
}
// TestBuild_RegistryLoads ensures that a function with a defined registry
// will use this when recalculating .Image on build when no --image is
// explicitly provided.
func TestBuild_RegistryLoads(t *testing.T) {
testRegistryLoads(NewBuildCmd, t)
}
// TestBuild_RegistryOrImageRequired ensures that when no registry or image are
// provided (or exist on the function already), and the client has not been
// instantiated with a default registry, an ErrRegistryRequired is received.
func TestBuild_RegistryOrImageRequired(t *testing.T) {
testRegistryOrImageRequired(NewBuildCmd, t)
}
// TestBuild_Authentication ensures that Token and Username/Password auth
// propagate to pushers which support them.
func TestBuild_Authentication(t *testing.T) {
testAuthentication(NewBuildCmd, t)
}
// TestBuild_BaseImage ensures that base image is used only with the right
// builders and propagates into f.Build.BaseImage
func TestBuild_BaseImage(t *testing.T) {
testBaseImage(NewBuildCmd, t)
}
// TestBuild_Push ensures that the build command properly pushes and respects
// the --push flag.
// - Push triggered after a successful build
// - Push not triggered after an unsuccessful build
// - Push can be disabled
func TestBuild_Push(t *testing.T) {
root := FromTempDirectory(t)
f := fn.Function{
Root: root,
Name: "myfunc",
Runtime: "go",
Registry: "example.com/alice",
}
if _, err := fn.New().Init(f); err != nil {
t.Fatal(err)
}
var (
builder = mock.NewBuilder()
pusher = mock.NewPusher()
)
cmd := NewBuildCmd(NewTestClient(fn.WithRegistry(TestRegistry), fn.WithBuilder(builder), fn.WithPusher(pusher)))
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Assert there was no push
if pusher.PushInvoked {
t.Fatal("push should not be invoked by default")
}
// Execute with push enabled
cmd.SetArgs([]string{"--push"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Assert there was a push
if !pusher.PushInvoked {
t.Fatal("push should be invoked when requested and a successful build")
}
// Execute with push enabled but with a failed build
builder.BuildFn = func(f fn.Function) error {
return errors.New("mock error")
}
pusher.PushInvoked = false
_ = cmd.Execute() // expected error
// Assert push was not invoked when the build failed
if pusher.PushInvoked {
t.Fatal("push should not be invoked on a failed build")
}
}

View File

@ -1,150 +0,0 @@
package cmd
import (
"fmt"
"net/http"
"os"
"knative.dev/func/cmd/prompt"
"knative.dev/func/pkg/builders/buildpacks"
"knative.dev/func/pkg/config"
"knative.dev/func/pkg/docker"
"knative.dev/func/pkg/docker/creds"
fn "knative.dev/func/pkg/functions"
fnhttp "knative.dev/func/pkg/http"
"knative.dev/func/pkg/k8s"
"knative.dev/func/pkg/knative"
"knative.dev/func/pkg/pipelines/tekton"
)
// ClientConfig settings for use with NewClient
// These are the minimum settings necessary to create the default client
// instance which has most subsystems initialized.
type ClientConfig struct {
// Verbose logging. By default, logging output is kept to the bare minimum.
// Use this flag to configure verbose logging throughout.
Verbose bool
// Allow insecure server connections when using SSL
InsecureSkipVerify bool
}
// ClientFactory defines a constructor which assists in the creation of a Client
// for use by commands.
// See the NewClient constructor which is the fully populated ClientFactory used
// by commands by default.
// See NewClientFactory which constructs a minimal ClientFactory for use
// during testing.
type ClientFactory func(ClientConfig, ...fn.Option) (*fn.Client, func())
// NewTestClient returns a client factory which will ignore options used,
// instead using those provided when creating the factory. This allows
// for tests to create an entirely default client but with N mocks.
func NewTestClient(options ...fn.Option) ClientFactory {
return func(_ ClientConfig, _ ...fn.Option) (*fn.Client, func()) {
return fn.New(options...), func() {}
}
}
// NewClient constructs an fn.Client with the majority of
// the concrete implementations set. Provide additional Options to this constructor
// to override or augment as needed, or override the ClientFactory passed to
// commands entirely to mock for testing. Note the returned cleanup function.
// 'Namespace' is optional. If not provided (see DefaultNamespace commentary),
// the currently configured is used.
// 'Verbose' indicates the system should write out a higher amount of logging.
func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) {
var (
t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies
c = newCredentialsProvider(config.Dir(), t) // for accessing registries
d = newKnativeDeployer(cfg.Verbose)
pp = newTektonPipelinesProvider(c, cfg.Verbose)
o = []fn.Option{ // standard (shared) options for all commands
fn.WithVerbose(cfg.Verbose),
fn.WithTransport(t),
fn.WithRepositoriesPath(config.RepositoriesPath()),
fn.WithBuilder(buildpacks.NewBuilder(buildpacks.WithVerbose(cfg.Verbose))),
fn.WithRemover(knative.NewRemover(cfg.Verbose)),
fn.WithDescriber(knative.NewDescriber(cfg.Verbose)),
fn.WithLister(knative.NewLister(cfg.Verbose)),
fn.WithDeployer(d),
fn.WithPipelinesProvider(pp),
fn.WithPusher(docker.NewPusher(
docker.WithCredentialsProvider(c),
docker.WithTransport(t),
docker.WithVerbose(cfg.Verbose))),
}
)
// Client is constructed with standard options plus any additional options
// which either augment or override the defaults.
client := fn.New(append(o, options...)...)
// A deferrable cleanup function which is used to perform any cleanup, such
// as closing the transport
cleanup := func() {
if err := t.Close(); err != nil {
fmt.Fprintf(os.Stderr, "error closing http transport. %v", err)
}
}
return client, cleanup
}
// newTransport returns a transport with cluster-flavor-specific variations
// which take advantage of additional features offered by cluster variants.
func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser {
return fnhttp.NewRoundTripper(fnhttp.WithInsecureSkipVerify(insecureSkipVerify), fnhttp.WithOpenShiftServiceCA())
}
// newCredentialsProvider returns a credentials provider which possibly
// has cluster-flavor specific additional credential loaders to take advantage
// of features or configuration nuances of cluster variants.
func newCredentialsProvider(configPath string, t http.RoundTripper) docker.CredentialsProvider {
options := []creds.Opt{
creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin, os.Stdout, os.Stderr)),
creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()),
creds.WithTransport(t),
creds.WithAdditionalCredentialLoaders(k8s.GetOpenShiftDockerCredentialLoaders()...),
}
// Other cluster variants can be supported here
return creds.NewCredentialsProvider(configPath, options...)
}
func newTektonPipelinesProvider(creds docker.CredentialsProvider, verbose bool) *tekton.PipelinesProvider {
options := []tekton.Opt{
tekton.WithCredentialsProvider(creds),
tekton.WithVerbose(verbose),
tekton.WithPipelineDecorator(deployDecorator{}),
}
return tekton.NewPipelinesProvider(options...)
}
func newKnativeDeployer(verbose bool) fn.Deployer {
options := []knative.DeployerOpt{
knative.WithDeployerVerbose(verbose),
knative.WithDeployerDecorator(deployDecorator{}),
}
return knative.NewDeployer(options...)
}
type deployDecorator struct {
oshDec k8s.OpenshiftMetadataDecorator
}
func (d deployDecorator) UpdateAnnotations(function fn.Function, annotations map[string]string) map[string]string {
if k8s.IsOpenShift() {
return d.oshDec.UpdateAnnotations(function, annotations)
}
return annotations
}
func (d deployDecorator) UpdateLabels(function fn.Function, labels map[string]string) map[string]string {
if k8s.IsOpenShift() {
return d.oshDec.UpdateLabels(function, labels)
}
return labels
}

View File

@ -1,55 +0,0 @@
package cmd
import (
"context"
"testing"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
)
// Test_NewTestClient ensures that the convenience method for
// constructing a mocked client for testing properly considers options:
// options provided to the factory constructor are considered exaustive,
// such that the test can force the user of the factory to use specific mocks.
// In other words, options provided when invoking the factory (such as by
// a command implementation) are ignored.
func Test_NewTestClient(t *testing.T) {
var (
remover = mock.NewRemover()
describer = mock.NewDescriber()
)
// Factory constructor options which should be used when invoking later
clientFn := NewTestClient(fn.WithRemover(remover))
// Factory should ignore options provided when invoking
client, _ := clientFn(ClientConfig{}, fn.WithDescriber(describer))
// Trigger an invocation of the mocks by running the associated client
// methods which depend on them
err := client.Remove(context.Background(), "myfunc", "myns", fn.Function{}, true)
if err != nil {
t.Fatal(err)
}
_, err = client.Describe(context.Background(), "myfunc", "myns", fn.Function{})
if err != nil {
t.Fatal(err)
}
// Ensure the first set of options, held on the factory (the mock remover)
// is invoked.
if !remover.RemoveInvoked {
t.Fatalf("factory (outer) options not carried through to final client instance")
}
// Ensure the second set of options, provided when constructing the client
// using the factory, are ignored.
if describer.DescribeInvoked {
t.Fatalf("test client factory should ignore options when invoked.")
}
// This ensures that the NewTestClient function, when provided a set
// of optional implementations (mocks) will override any which are set
// by commands, allowing tests to "force" a command to use the mocked
// implementations.
}

View File

@ -7,10 +7,14 @@ import (
"github.com/spf13/cobra"
)
func NewCompletionCmd() *cobra.Command {
return &cobra.Command{
func init() {
root.AddCommand(completionCmd)
}
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion <bash|zsh|fish>",
Short: "Output functions shell completion code",
Short: "Generate completion scripts for bash, fish and zsh",
Long: `To load completion run
For zsh:
@ -32,17 +36,15 @@ source <(func completion bash)
}
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
err = root.GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
err = root.GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
err = root.GenFishCompletion(os.Stdout, true)
default:
err = errors.New("unknown shell, only bash, zsh and fish are supported")
}
return err
},
}
}

View File

@ -2,22 +2,24 @@ package cmd
import (
"encoding/json"
"fmt"
"os"
"os/user"
"path"
"strings"
"github.com/spf13/cobra"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/knative"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/buildpacks"
"github.com/boson-project/func/knative"
)
func CompleteFunctionList(cmd *cobra.Command, args []string, toComplete string) (strings []string, directive cobra.ShellCompDirective) {
lister := knative.NewLister(false)
list, err := lister.List(cmd.Context(), "")
lister, err := knative.NewLister("")
if err != nil {
directive = cobra.ShellCompDirectiveError
return
}
list, err := lister.List(cmd.Context())
if err != nil {
directive = cobra.ShellCompDirectiveError
return
@ -29,50 +31,14 @@ func CompleteFunctionList(cmd *cobra.Command, args []string, toComplete string)
directive = cobra.ShellCompDirectiveDefault
return
}
func CompleteRuntimeList(cmd *cobra.Command, args []string, toComplete string, client *fn.Client) (matches []string, directive cobra.ShellCompDirective) {
runtimes, err := client.Runtimes()
if err != nil {
fmt.Fprintf(os.Stderr, "error listing runtimes for flag completion: %v", err)
return
func CompleteRuntimeList(cmd *cobra.Command, args []string, toComplete string) (strings []string, directive cobra.ShellCompDirective) {
strings = []string{}
for lang := range buildpacks.RuntimeToBuildpack {
strings = append(strings, lang)
}
for _, runtime := range runtimes {
if strings.HasPrefix(runtime, toComplete) {
matches = append(matches, runtime)
}
}
return
}
func CompleteTemplateList(cmd *cobra.Command, args []string, toComplete string, client *fn.Client) (matches []string, directive cobra.ShellCompDirective) {
directive = cobra.ShellCompDirectiveError
lang, err := cmd.Flags().GetString("language")
if err != nil {
fmt.Fprintf(os.Stderr, "cannot list templates: %v", err)
return
}
if lang == "" {
fmt.Fprintln(os.Stderr, "cannot list templates: language not specified")
return
}
templates, err := client.Templates().List(lang)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot list templates: %v", err)
return
}
directive = cobra.ShellCompDirectiveDefault
for _, t := range templates {
if strings.HasPrefix(t, toComplete) {
matches = append(matches, t)
}
}
return
}
func CompleteOutputFormatList(cmd *cobra.Command, args []string, toComplete string) (strings []string, directive cobra.ShellCompDirective) {
directive = cobra.ShellCompDirectiveDefault
strings = []string{"plain", "yaml", "xml", "json"}
@ -107,13 +73,13 @@ func CompleteRegistryList(cmd *cobra.Command, args []string, toComplete string)
return
}
func CompleteBuilderImageList(cmd *cobra.Command, args []string, complete string) (builderImages []string, directive cobra.ShellCompDirective) {
func CompleteBuilderList(cmd *cobra.Command, args []string, complete string) (strings []string, directive cobra.ShellCompDirective) {
directive = cobra.ShellCompDirectiveError
var (
err error
path string
f fn.Function
f bosonFunc.Function
)
path, err = cmd.Flags().GetString("path")
@ -121,40 +87,16 @@ func CompleteBuilderImageList(cmd *cobra.Command, args []string, complete string
return
}
f, err = fn.NewFunction(path)
f, err = bosonFunc.NewFunction(path)
if err != nil {
return
}
builderImages = make([]string, 0, len(f.Build.BuilderImages))
for name := range f.Build.BuilderImages {
if len(complete) == 0 {
builderImages = append(builderImages, name)
continue
}
if strings.HasPrefix(name, complete) {
builderImages = append(builderImages, name)
}
}
directive = cobra.ShellCompDirectiveNoFileComp
return
}
func CompleteBuilderList(cmd *cobra.Command, args []string, complete string) (matches []string, d cobra.ShellCompDirective) {
d = cobra.ShellCompDirectiveNoFileComp
matches = []string{}
if len(complete) == 0 {
matches = KnownBuilders()
return
}
for _, b := range KnownBuilders() {
if strings.HasPrefix(b, complete) {
matches = append(matches, b)
}
strings = make([]string, 0, len(f.BuilderMap))
for name := range f.BuilderMap {
strings = append(strings, name)
}
directive = cobra.ShellCompDirectiveDefault
return
}

View File

@ -1,175 +0,0 @@
package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
)
type functionLoader interface {
Load(path string) (fn.Function, error)
}
type functionSaver interface {
Save(f fn.Function) error
}
type functionLoaderSaver interface {
functionLoader
functionSaver
}
type standardLoaderSaver struct{}
func (s standardLoaderSaver) Load(path string) (fn.Function, error) {
f, err := fn.NewFunction(path)
if err != nil {
return fn.Function{}, fmt.Errorf("failed to create new function (path: %q): %w", path, err)
}
if !f.Initialized() {
return fn.Function{}, fn.NewErrNotInitialized(f.Root)
}
return f, nil
}
func (s standardLoaderSaver) Save(f fn.Function) error {
return f.Write()
}
var defaultLoaderSaver standardLoaderSaver
func NewConfigCmd(loadSaver functionLoaderSaver, newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Configure a function",
Long: `Configure a function
Interactive prompt that allows configuration of Git configuration, Volume mounts, Environment
variables, and Labels for a function project present in the current directory
or from the directory specified with --path.
`,
SuggestFor: []string{"cfg", "cofnig"},
PreRunE: bindEnv("path", "verbose"),
RunE: runConfigCmd,
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
cmd.AddCommand(NewConfigGitCmd(newClient))
cmd.AddCommand(NewConfigLabelsCmd(loadSaver))
cmd.AddCommand(NewConfigEnvsCmd(loadSaver))
cmd.AddCommand(NewConfigVolumesCmd())
return cmd
}
func runConfigCmd(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(defaultLoaderSaver)
if err != nil {
return
}
var qs = []*survey.Question{
{
Name: "selectedConfig",
Prompt: &survey.Select{
Message: "What do you want to configure?",
Options: []string{"Git", "Environment variables", "Volumes", "Labels"},
Default: "Git",
},
},
{
Name: "selectedOperation",
Prompt: &survey.Select{
Message: "What operation do you want to perform?",
Options: []string{"Add", "Remove", "List"},
Default: "List",
},
},
}
answers := struct {
SelectedConfig string
SelectedOperation string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
return
}
switch answers.SelectedOperation {
case "Add":
switch answers.SelectedConfig {
case "Volumes":
err = runAddVolumesPrompt(cmd.Context(), function)
case "Environment variables":
err = runAddEnvsPrompt(cmd.Context(), function)
case "Labels":
err = runAddLabelsPrompt(cmd.Context(), function, defaultLoaderSaver)
case "Git":
err = runConfigGitSetCmd(cmd, NewClient)
}
case "Remove":
switch answers.SelectedConfig {
case "Volumes":
err = runRemoveVolumesPrompt(function)
case "Environment variables":
err = runRemoveEnvsPrompt(function)
case "Labels":
err = runRemoveLabelsPrompt(function, defaultLoaderSaver)
case "Git":
err = runConfigGitRemoveCmd(cmd, NewClient)
}
case "List":
switch answers.SelectedConfig {
case "Volumes":
listVolumes(function)
case "Environment variables":
err = listEnvs(function, cmd.OutOrStdout(), Human)
case "Labels":
err = listLabels(function, cmd.OutOrStdout(), Human)
case "Git":
err = runConfigGitCmd(cmd, NewClient)
}
}
return
}
// CLI Configuration (parameters)
// ------------------------------
type configCmdConfig struct {
Path string
Verbose bool
}
func newConfigCmdConfig() configCmdConfig {
return configCmdConfig{
Path: viper.GetString("path"),
Verbose: viper.GetBool("verbose"),
}
}
func initConfigCommand(loader functionLoader) (fn.Function, error) {
config := newConfigCmdConfig()
function, err := loader.Load(config.Path)
if err != nil {
return fn.Function{}, err
}
return function, nil
}

View File

@ -1,533 +0,0 @@
package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s"
"knative.dev/func/pkg/utils"
)
func NewConfigEnvsCmd(loadSaver functionLoaderSaver) *cobra.Command {
cmd := &cobra.Command{
Use: "envs",
Short: "List and manage configured environment variable for a function",
Long: `List and manage configured environment variable for a function
Prints configured Environment variable for a function project present in
the current directory or from the directory specified with --path.
`,
Aliases: []string{"env"},
SuggestFor: []string{"ensv"},
PreRunE: bindEnv("path", "output", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(loadSaver)
if err != nil {
return
}
return listEnvs(function, cmd.OutOrStdout(), Format(viper.GetString("output")))
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
cmd.Flags().StringP("output", "o", "human", "Output format (human|json) ($FUNC_OUTPUT)")
configEnvsAddCmd := NewConfigEnvsAddCmd(loadSaver)
configEnvsRemoveCmd := NewConfigEnvsRemoveCmd(loadSaver)
addPathFlag(cmd)
addPathFlag(configEnvsAddCmd)
addPathFlag(configEnvsRemoveCmd)
addVerboseFlag(cmd, cfg.Verbose)
addVerboseFlag(configEnvsAddCmd, cfg.Verbose)
addVerboseFlag(configEnvsRemoveCmd, cfg.Verbose)
cmd.AddCommand(configEnvsAddCmd)
cmd.AddCommand(configEnvsRemoveCmd)
return cmd
}
func NewConfigEnvsAddCmd(loadSaver functionLoaderSaver) *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Add environment variable to the function configuration",
Long: `Add environment variable to the function configuration.
If environment variable is not set explicitly by flag, interactive prompt is used.
The environment variable can be set directly from a value,
from an environment variable on the local machine or from Secrets and ConfigMaps.
It is also possible to import all keys as environment variables from a Secret or ConfigMap.`,
Example: `# set environment variable directly
{{rootCmdUse}} config envs add --name=VARNAME --value=myValue
# set environment variable from local env $LOC_ENV
{{rootCmdUse}} config envs add --name=VARNAME --value='{{"{{"}} env:LOC_ENV {{"}}"}}'
set environment variable from a secret
{{rootCmdUse}} config envs add --name=VARNAME --value='{{"{{"}} secret:secretName:key {{"}}"}}'
# set all key as environment variables from a secret
{{rootCmdUse}} config envs add --value='{{"{{"}} secret:secretName {{"}}"}}'
# set environment variable from a configMap
{{rootCmdUse}} config envs add --name=VARNAME --value='{{"{{"}} configMap:confMapName:key {{"}}"}}'
# set all key as environment variables from a configMap
{{rootCmdUse}} config envs add --value='{{"{{"}} configMap:confMapName {{"}}"}}'`,
SuggestFor: []string{"ad", "create", "insert", "append"},
PreRunE: bindEnv("path", "name", "value", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(loadSaver)
if err != nil {
return
}
var np *string
var vp *string
if cmd.Flags().Changed("name") {
s, e := cmd.Flags().GetString("name")
if e != nil {
return e
}
np = &s
}
if cmd.Flags().Changed("value") {
s, e := cmd.Flags().GetString("value")
if e != nil {
return e
}
vp = &s
}
if np != nil || vp != nil {
if np != nil {
if err := utils.ValidateEnvVarName(*np); err != nil {
return err
}
}
function.Run.Envs = append(function.Run.Envs, fn.Env{Name: np, Value: vp})
return loadSaver.Save(function)
}
return runAddEnvsPrompt(cmd.Context(), function)
},
}
cmd.Flags().StringP("name", "", "", "Name of the environment variable.")
cmd.Flags().StringP("value", "", "", "Value of the environment variable.")
return cmd
}
func NewConfigEnvsRemoveCmd(loadSaver functionLoaderSaver) *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Remove environment variable from the function configuration",
Long: `Remove environment variable from the function configuration
Interactive prompt to remove Environment variables from the function project
in the current directory or from the directory specified with --path.
`,
Aliases: []string{"rm"},
SuggestFor: []string{"del", "delete", "rmeove"},
PreRunE: bindEnv("path", "name", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(loadSaver)
if err != nil {
return
}
var name string
if cmd.Flags().Changed("name") {
s, e := cmd.Flags().GetString("name")
if e != nil {
return e
}
name = s
}
if name != "" {
envs := []fn.Env{}
for _, v := range function.Run.Envs {
if *v.Name != name {
envs = append(envs, v)
}
}
function.Run.Envs = envs
return loadSaver.Save(function)
}
return runRemoveEnvsPrompt(function)
},
}
cmd.Flags().StringP("name", "", "", "Name of the environment variable.")
return cmd
}
func listEnvs(f fn.Function, w io.Writer, outputFormat Format) error {
switch outputFormat {
case Human:
if len(f.Run.Envs) == 0 {
_, err := fmt.Fprintln(w, "There aren't any configured Environment variables")
return err
}
fmt.Println("Configured Environment variables:")
for _, e := range f.Run.Envs {
_, err := fmt.Fprintln(w, " - ", e.String())
if err != nil {
return err
}
}
return nil
case JSON:
enc := json.NewEncoder(w)
return enc.Encode(f.Run.Envs)
default:
return fmt.Errorf("bad format: %v", outputFormat)
}
}
func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) {
insertToIndex := 0
// SECTION - if there are some envs already set, let choose the position of the new entry
if len(f.Run.Envs) > 0 {
options := []string{}
for _, e := range f.Run.Envs {
options = append(options, fmt.Sprintf("Insert before: %s", e.String()))
}
options = append(options, "Insert here.")
selectedEnv := ""
prompt := &survey.Select{
Message: "Where do you want to add the Environment variable?",
Options: options,
Default: options[len(options)-1],
}
err = survey.AskOne(prompt, &selectedEnv)
if err != nil {
return
}
for i, option := range options {
if option == selectedEnv {
insertToIndex = i
break
}
}
}
// SECTION - select the type of Environment variable to be added
secrets, err := k8s.ListSecretsNamesIfConnected(ctx, f.Deploy.Namespace)
if err != nil {
return
}
configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, f.Deploy.Namespace)
if err != nil {
return
}
selectedOption := ""
const (
optionEnvValue = "Environment variable with a specified value"
optionEnvLocal = "Value from a local environment variable"
optionEnvConfigMap = "ConfigMap: all key=value pairs as environment variables"
optionEnvConfigMapKey = "ConfigMap: value from a key"
optionEnvSecret = "Secret: all key=value pairs as environment variables"
optionEnvSecretKey = "Secret: value from a key"
)
options := []string{optionEnvValue, optionEnvLocal}
if len(configMaps) > 0 {
options = append(options, optionEnvConfigMap)
options = append(options, optionEnvConfigMapKey)
}
if len(secrets) > 0 {
options = append(options, optionEnvSecret)
options = append(options, optionEnvSecretKey)
}
err = survey.AskOne(&survey.Select{
Message: "What type of Environment variable do you want to add?",
Options: options,
}, &selectedOption)
if err != nil {
return
}
newEnv := fn.Env{}
switch selectedOption {
// SECTION - add new Environment variable with the specified value
case optionEnvValue:
qs := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "Please specify the Environment variable name:"},
Validate: func(val interface{}) error {
return utils.ValidateEnvVarName(val.(string))
},
},
{
Name: "value",
Prompt: &survey.Input{Message: "Please specify the Environment variable value:"},
},
}
answers := struct {
Name string
Value string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
return nil
}
return
}
newEnv.Name = &answers.Name
newEnv.Value = &answers.Value
// SECTION - add new Environment variable with value from a local environment variable
case optionEnvLocal:
qs := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "Please specify the Environment variable name:"},
Validate: func(val interface{}) error {
return utils.ValidateEnvVarName(val.(string))
},
},
{
Name: "value",
Prompt: &survey.Input{Message: "Please specify the local Environment variable:"},
Validate: survey.Required,
},
}
answers := struct {
Name string
Value string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
return nil
}
return
}
if _, ok := os.LookupEnv(answers.Value); !ok {
fmt.Printf("Warning: specified local environment variable %q is not set\n", answers.Value)
}
value := fmt.Sprintf("{{ env:%s }}", answers.Value)
newEnv.Name = &answers.Name
newEnv.Value = &value
// SECTION - Add all key=value pairs from ConfigMap as environment variables
case optionEnvConfigMap:
selectedResource := ""
err = survey.AskOne(&survey.Select{
Message: "Which ConfigMap do you want to use?",
Options: configMaps,
}, &selectedResource)
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
return nil
}
return
}
value := fmt.Sprintf("{{ configMap:%s }}", selectedResource)
newEnv.Value = &value
// SECTION - Environment variable with value from a key from ConfigMap
case optionEnvConfigMapKey:
qs := []*survey.Question{
{
Name: "configmap",
Prompt: &survey.Select{
Message: "Which ConfigMap do you want to use?",
Options: configMaps,
},
},
{
Name: "name",
Prompt: &survey.Input{Message: "Please specify the Environment variable name:"},
Validate: func(val interface{}) error {
return utils.ValidateEnvVarName(val.(string))
},
},
{
Name: "key",
Prompt: &survey.Input{Message: "Please specify a key from the selected ConfigMap:"},
Validate: func(val interface{}) error {
return utils.ValidateConfigMapKey(val.(string))
},
},
}
answers := struct {
ConfigMap string
Name string
Key string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
return nil
}
return
}
value := fmt.Sprintf("{{ configMap:%s:%s }}", answers.ConfigMap, answers.Key)
newEnv.Name = &answers.Name
newEnv.Value = &value
// SECTION - Add all key=value pairs from Secret as environment variables
case optionEnvSecret:
selectedResource := ""
err = survey.AskOne(&survey.Select{
Message: "Which Secret do you want to use?",
Options: secrets,
}, &selectedResource)
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
value := fmt.Sprintf("{{ secret:%s }}", selectedResource)
newEnv.Value = &value
// SECTION - Environment variable with value from a key from Secret
case optionEnvSecretKey:
qs := []*survey.Question{
{
Name: "secret",
Prompt: &survey.Select{
Message: "Which Secret do you want to use?",
Options: secrets,
},
},
{
Name: "name",
Prompt: &survey.Input{Message: "Please specify the Environment variable name:"},
Validate: func(val interface{}) error {
return utils.ValidateEnvVarName(val.(string))
},
},
{
Name: "key",
Prompt: &survey.Input{Message: "Please specify a key from the selected Secret:"},
Validate: func(val interface{}) error {
return utils.ValidateSecretKey(val.(string))
},
},
}
answers := struct {
Secret string
Name string
Key string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
value := fmt.Sprintf("{{ secret:%s:%s }}", answers.Secret, answers.Key)
newEnv.Name = &answers.Name
newEnv.Value = &value
}
// we have all necessary information -> let's insert the env to the selected position in the list
if insertToIndex == len(f.Run.Envs) {
f.Run.Envs = append(f.Run.Envs, newEnv)
} else {
f.Run.Envs = append(f.Run.Envs[:insertToIndex+1], f.Run.Envs[insertToIndex:]...)
f.Run.Envs[insertToIndex] = newEnv
}
err = f.Write()
if err == nil {
fmt.Println("Environment variable entry was added to the function configuration")
}
return
}
func runRemoveEnvsPrompt(f fn.Function) (err error) {
if len(f.Run.Envs) == 0 {
fmt.Println("There aren't any configured Environment variables")
return
}
options := []string{}
for _, e := range f.Run.Envs {
options = append(options, e.String())
}
selectedEnv := ""
prompt := &survey.Select{
Message: "Which Environment variables do you want to remove?",
Options: options,
}
err = survey.AskOne(prompt, &selectedEnv)
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
var newEnvs []fn.Env
removed := false
for i, e := range f.Run.Envs {
if e.String() == selectedEnv {
newEnvs = append(f.Run.Envs[:i], f.Run.Envs[i+1:]...)
removed = true
break
}
}
if removed {
f.Run.Envs = newEnvs
err = f.Write()
if err == nil {
fmt.Println("Environment variable entry was removed from the function configuration")
}
}
return
}

View File

@ -1,56 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
)
func NewConfigGitCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "git",
Short: "Manage Git configuration of a function",
Long: `Manage Git configuration of a function
Prints Git configuration for a function project present in
the current directory or from the directory specified with --path.
`,
SuggestFor: []string{"gti", "Git", "Gti"},
PreRunE: bindEnv("path"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
return runConfigGitCmd(cmd, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Function Context
f, _ := fn.NewFunction(effectivePath())
if f.Initialized() {
cfg = cfg.Apply(f)
}
configGitSetCmd := NewConfigGitSetCmd(newClient)
configGitRemoveCmd := NewConfigGitRemoveCmd(newClient)
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
cmd.AddCommand(configGitSetCmd)
cmd.AddCommand(configGitRemoveCmd)
return cmd
}
func runConfigGitCmd(_ *cobra.Command, _ ClientFactory) (err error) {
fmt.Printf("--------------------------- Function Git config ---------------------------\n")
fmt.Printf("Not implemented yet.\n")
return nil
}

View File

@ -1,170 +0,0 @@
package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/pipelines"
)
func NewConfigGitRemoveCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Remove Git settings from the function configuration",
Long: `Remove Git settings from the function configuration
Interactive prompt to remove Git settings from the function project in the current
directory or from the directory specified with --path.
It also removes any generated resources that are used for Git based build and deployment,
such as local generated Pipelines resources and any resources generated on the cluster.
`,
SuggestFor: []string{"rem", "rmeove", "del", "dle"},
PreRunE: bindEnv("path", "delete-local", "delete-cluster"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
return runConfigGitRemoveCmd(cmd, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Resources generated related Flags:
cmd.Flags().Bool("delete-local", false, "Delete local resources (pipeline templates).")
cmd.Flags().Bool("delete-cluster", false, "Delete cluster resources (credentials and config on the cluster).")
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
type configGitRemoveConfig struct {
// Globals (builder, confirm, registry, verbose)
config.Global
// Path of the function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// informs whether any specific flag for deleting only a subset of resources has been set
flagSet bool
metadata pipelines.PacMetadata
}
// newConfigGitRemoveConfig creates a configGitRemoveConfig populated from command flags
func newConfigGitRemoveConfig(_ *cobra.Command) (c configGitRemoveConfig) {
flagSet := false
// decide what resources we should delete:
// - by default all resources
// - if any parameter is explicitly specified then get value from parameters
deleteLocal := true
deleteCluster := true
if viper.HasChanged("delete-local") || viper.HasChanged("delete-cluster") {
deleteLocal = viper.GetBool("delete-local")
deleteCluster = viper.GetBool("delete-cluster")
flagSet = true
}
c = configGitRemoveConfig{
flagSet: flagSet,
metadata: pipelines.PacMetadata{
ConfigureLocalResources: deleteLocal,
ConfigureClusterResources: deleteCluster,
},
}
return c
}
func (c configGitRemoveConfig) Prompt(f fn.Function) (configGitRemoveConfig, error) {
deleteAll := true
// prompt if any flag hasn't been set yet
if !c.flagSet {
if err := survey.AskOne(&survey.Confirm{
Message: "Do you want to delete all Git related resources?",
Help: "Delete Git config, local Pipeline resourdces and on the cluster resources.",
Default: deleteAll,
}, &deleteAll, survey.WithValidator(survey.Required)); err != nil {
return c, err
}
}
if !deleteAll {
deleteLocal := true
if err := survey.AskOne(&survey.Confirm{
Message: "Do you want to delete all local Git related resources (Pipelines)?",
Help: "Delete local Pipeline resources created in the function project directory.",
Default: deleteLocal,
}, &deleteLocal, survey.WithValidator(survey.Required)); err != nil {
return c, err
}
c.metadata.ConfigureLocalResources = deleteLocal
deleteCluster := true
if err := survey.AskOne(&survey.Confirm{
Message: "Do you want to delete all Git related resources present on the cluster?",
Help: "Delete all Pipeline resources that were created on the cluster.",
Default: deleteCluster,
}, &deleteCluster, survey.WithValidator(survey.Required)); err != nil {
return c, err
}
c.metadata.ConfigureClusterResources = deleteCluster
}
return c, nil
}
// Configure the given function. Updates a function struct with all
// configurable values. Note that the config already includes function's
// current values, as they were passed through via flag defaults.
func (c configGitRemoveConfig) Configure(f fn.Function) (fn.Function, error) {
var err error
if c.metadata.ConfigureLocalResources {
f.Build.Git = fn.Git{}
}
// Save the function which has now been updated with flags/config
if err = f.Write(); err != nil { // TODO: remove when client API uses 'f'
return f, err
}
return f, nil
}
func runConfigGitRemoveCmd(cmd *cobra.Command, newClient ClientFactory) (err error) {
var (
cfg configGitRemoveConfig
f fn.Function
)
if err = config.CreatePaths(); err != nil { // for possible auth.json usage
return
}
cfg = newConfigGitRemoveConfig(cmd)
if f, err = fn.NewFunction(cfg.Path); err != nil {
return
}
if cfg, err = cfg.Prompt(f); err != nil {
return
}
if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
return client.RemovePAC(cmd.Context(), f, cfg.metadata)
}

View File

@ -1,311 +0,0 @@
package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
pacgit "github.com/openshift-pipelines/pipelines-as-code/pkg/git"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/git"
"knative.dev/func/pkg/pipelines"
)
func NewConfigGitSetCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "set",
Short: "Set Git settings in the function configuration",
Long: `Set Git settings in the function configuration
Interactive prompt to set Git settings in the function project in the current
directory or from the directory specified with --path.
`,
SuggestFor: []string{"add", "ad", "update", "create", "insert", "append"},
PreRunE: bindEnv("path", "builder", "builder-image", "image", "registry", "git-provider", "git-url", "git-branch", "git-dir", "gh-access-token", "config-local", "config-cluster", "config-remote"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
return runConfigGitSetCmd(cmd, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Function Context
f, _ := fn.NewFunction(effectivePath())
if f.Initialized() {
cfg = cfg.Apply(f)
}
// Flags
//
// Globally-Configurable Flags:
// Options whose value may be defined globally may also exist on the
// contextually relevant function; but sets are flattened via cfg.Apply(f)
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().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().StringP("namespace", "n", cfg.Namespace,
"Deploy into a specific namespace. Will use function's current namespace by default if already deployed, and the currently active namespace if it can be determined. ($FUNC_NAMESPACE)")
// Function-Context Flags:
// Options whose value is avaolable on the function with context only
// (persisted but not globally configurable)
builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().StringP("builder-image", "", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("image", "i", f.Image, "Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. ($FUNC_IMAGE)")
// Git related Flags:
cmd.Flags().String("git-provider", "",
fmt.Sprintf("The type of the Git platform provider to setup webhook. This value is usually automatically generated from input URL, use this parameter to override this setting. Currently supported providers are %s.", git.SupportedProvidersList.PrettyString()))
cmd.Flags().StringP("git-url", "g", "",
"Repository url containing the function to build ($FUNC_GIT_URL)")
cmd.Flags().StringP("git-branch", "t", "",
"Git revision (branch) to be used when deploying via the Git repository ($FUNC_GIT_BRANCH)")
cmd.Flags().StringP("git-dir", "d", "",
"Directory in the Git repository containing the function (default is the root) ($FUNC_GIT_DIR)")
// GitHub related Flags:
cmd.Flags().String("gh-access-token", "",
"GitHub Personal Access Token. For public repositories the scope is 'public_repo', for private is 'repo'. If you want to configure the webhook automatically, 'admin:repo_hook' is needed as well. Get more details: https://pipelines-as-code.pages.dev/docs/install/github_webhook/.")
cmd.Flags().String("gh-webhook-secret", "",
"GitHub Webhook Secret used for payload validation. If not specified, it will be generated automatically.")
// Resources generated related Flags:
cmd.Flags().Bool("config-local", false, "Configure local resources (pipeline templates).")
cmd.Flags().Bool("config-cluster", false, "Configure cluster resources (credentials and config on the cluster).")
cmd.Flags().Bool("config-remote", false, "Configure remote resources (webhook on the Git provider side).")
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
type configGitSetConfig struct {
buildConfig // further embeds config.Global
GitProvider string
GitURL string
GitRevision string
GitContextDir string
ConfigureRemoteResourcesSet bool // whether ConfigureRemoteResources value has been set
metadata pipelines.PacMetadata
}
// newConfigGitSetConfig creates a buildConfig populated from command flags and
// environment variables; in that precedence.
func newConfigGitSetConfig(_ *cobra.Command) (c configGitSetConfig) {
// decide what resources we should configure:
// - by default all resources
// - if any parameter is explicitly specified then get value from parameters
configLocal := true
configCluster := true
configRemote := true
configRemoteSet := false
if viper.HasChanged("config-local") || viper.HasChanged("config-cluster") || viper.HasChanged("config-remote") {
configLocal = viper.GetBool("config-local")
configCluster = viper.GetBool("config-cluster")
configRemote = viper.GetBool("config-remote")
configRemoteSet = true
}
c = configGitSetConfig{
buildConfig: newBuildConfig(),
GitURL: viper.GetString("git-url"),
GitRevision: viper.GetString("git-branch"),
GitContextDir: viper.GetString("git-dir"),
ConfigureRemoteResourcesSet: configRemoteSet,
metadata: pipelines.PacMetadata{
GitProvider: viper.GetString("git-provider"),
PersonalAccessToken: viper.GetString("gh-access-token"),
WebhookSecret: viper.GetString("gh-webhook-secret"),
ConfigureLocalResources: configLocal,
ConfigureClusterResources: configCluster,
ConfigureRemoteResources: configRemote,
},
}
return c
}
func (c configGitSetConfig) Prompt(f fn.Function) (configGitSetConfig, error) {
var err error
if c.buildConfig, err = c.buildConfig.Prompt(); err != nil {
return c, err
}
// try to read git url from the local .git settings
gitInfo := pacgit.GetGitInfo(c.Path)
// prompt if git URL hasn't been set previously
if c.GitURL == "" {
url := f.Build.Git.URL
if gitInfo.URL != "" {
url = gitInfo.URL
}
if err := survey.AskOne(&survey.Input{
Message: "The URL to Git repository with the function source code:",
Default: url,
}, &url, survey.WithValidator(survey.Required)); err != nil {
return c, err
}
c.GitURL = url
}
// prompt if git revision hasn't been set previously
if c.GitRevision == "" {
revision := f.Build.Git.Revision
if gitInfo.Branch != "" {
revision = gitInfo.Branch
}
if err := survey.AskOne(&survey.Input{
Message: "The Git branch or tag we are targeting:",
Help: "ie: main, refs/tags/*",
Default: revision,
}, &revision); err != nil {
return c, err
}
c.GitRevision = revision
}
// prompt if contextDir hasn't been set previously
if c.GitContextDir == "" {
contextDir := f.Build.Git.ContextDir
if err := survey.AskOne(&survey.Input{
Message: "A subpath within the repository:",
Help: "A subpath within the repository where the source code of a function is located.",
Default: contextDir,
}, &contextDir); err != nil {
return c, err
}
c.GitContextDir = contextDir
}
// prompt if webhook trigger setting hasn't been set previously
if !c.ConfigureRemoteResourcesSet {
trigger := true
if err := survey.AskOne(&survey.Confirm{
Message: "Do you want to configure webhook trigger?",
Help: "Webhook trigger also running pipeline on a git event, ie: commit, push",
Default: trigger,
}, &trigger, survey.WithValidator(survey.Required)); err != nil {
return c, err
}
c.metadata.ConfigureRemoteResources = trigger
c.ConfigureRemoteResourcesSet = true
}
if c.metadata.ConfigureRemoteResources {
// Configure Git provider
if c.metadata.GitProvider == "" {
provider, err := git.GitProviderName(c.GitURL)
if err != nil {
msg := "Please select the type of the Git platform provider to setup webhook:"
if err = survey.AskOne(&survey.Select{
Message: msg,
Options: git.SupportedProvidersList,
Default: 0,
}, &provider); err != nil {
return c, err
}
}
c.metadata.GitProvider = provider
}
// prompt if PersonalAccessToken hasn't been set previously
if c.metadata.PersonalAccessToken == "" {
var personalAccessToken string
if err := survey.AskOne(&survey.Password{
Message: "Please enter the GitHub Personal Access Token:",
Help: "For public repositories the scope is 'public_repo', for private is 'repo'. If you want to configure the webhook automatically 'admin:repo_hook' is needed as well. Get more details: https://pipelines-as-code.pages.dev/docs/install/github_webhook/.",
}, &personalAccessToken, survey.WithValidator(survey.Required)); err != nil {
return c, err
}
c.metadata.PersonalAccessToken = personalAccessToken
}
}
return c, nil
}
func (c configGitSetConfig) Validate(cmd *cobra.Command) (err error) {
// Bubble validation
if err = c.buildConfig.Validate(); err != nil {
return
}
return
}
// Configure the given function. Updates a function struct with all
// configurable values. Note that the config already includes function's
// current values, as they were passed through via flag defaults.
func (c configGitSetConfig) Configure(f fn.Function) (fn.Function, error) {
var err error
// Bubble configure request
//
// The member values on the config object now take absolute precidence
// because they include 1) static config 2) user's global config
// 3) Environment variables and 4) flag values (which were set with their
// default being 1-3).
f = c.buildConfig.Configure(f) // also configures .buildConfig.Global
// Configure basic members
f.Build.Git.URL = c.GitURL
f.Build.Git.ContextDir = c.GitContextDir
f.Build.Git.Revision = c.GitRevision // TODO: should match; perhaps "refSpec"
// Save the function which has now been updated with flags/config
if err = f.Write(); err != nil { // TODO: remove when client API uses 'f'
return f, err
}
return f, nil
}
func runConfigGitSetCmd(cmd *cobra.Command, newClient ClientFactory) (err error) {
var (
cfg configGitSetConfig
f fn.Function
)
if err = config.CreatePaths(); err != nil { // for possible auth.json usage
return
}
cfg = newConfigGitSetConfig(cmd)
if f, err = fn.NewFunction(cfg.Path); err != nil {
return
}
if cfg, err = cfg.Prompt(f); err != nil {
return
}
if err = cfg.Validate(cmd); err != nil {
return
}
if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, fn.WithRegistry(cfg.Registry))
defer done()
return client.ConfigurePAC(cmd.Context(), f, cfg.metadata)
}

View File

@ -1,360 +0,0 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/utils"
)
func NewConfigLabelsCmd(loaderSaver functionLoaderSaver) *cobra.Command {
var configLabelsCmd = &cobra.Command{
Use: "labels",
Short: "List and manage configured labels for a function",
Long: `List and manage configured labels for a function
Prints configured labels for a function project present in
the current directory or from the directory specified with --path.
`,
Aliases: []string{"label"},
SuggestFor: []string{"albels", "abels"},
PreRunE: bindEnv("path", "output", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(loaderSaver)
if err != nil {
return
}
return listLabels(function, cmd.OutOrStdout(), Format(viper.GetString("output")))
},
}
var configLabelsAddCmd = &cobra.Command{
Use: "add",
Short: "Add labels to the function configuration",
Long: `Add labels to the function configuration
If label is not set explicitly by flag, interactive prompt is used.
The label can be set directly from a value or from an environment variable on
the local machine.
`,
Example: `# set label directly
{{rootCmdUse}} config labels add --name=Foo --value=Bar
# set label from local env $FOO
{{rootCmdUse}} config labels add --name=Foo --value='{{"{{"}} env:FOO {{"}}"}}'`,
SuggestFor: []string{"ad", "create", "insert", "append"},
PreRunE: bindEnv("path", "name", "value", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(loaderSaver)
if err != nil {
return
}
var np *string
var vp *string
if cmd.Flags().Changed("name") {
s, e := cmd.Flags().GetString("name")
if e != nil {
return e
}
np = &s
}
if cmd.Flags().Changed("value") {
s, e := cmd.Flags().GetString("value")
if e != nil {
return e
}
vp = &s
}
if np != nil && vp != nil {
if err := utils.ValidateLabelKey(*np); err != nil {
return err
}
if err := utils.ValidateLabelValue(*vp); err != nil {
return err
}
function.Deploy.Labels = append(function.Deploy.Labels, fn.Label{Key: np, Value: vp})
return loaderSaver.Save(function)
}
return runAddLabelsPrompt(cmd.Context(), function, loaderSaver)
},
}
var configLabelsRemoveCmd = &cobra.Command{
Use: "remove",
Short: "Remove labels from the function configuration",
Long: `Remove labels from the function configuration
Interactive prompt to remove labels from the function project in the current
directory or from the directory specified with --path.
`,
Aliases: []string{"rm"},
SuggestFor: []string{"del", "delete", "rmeove"},
PreRunE: bindEnv("path", "name", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(loaderSaver)
if err != nil {
return
}
var name string
if cmd.Flags().Changed("name") {
s, e := cmd.Flags().GetString("name")
if e != nil {
return e
}
name = s
}
if name != "" {
labels := []fn.Label{}
for _, v := range function.Deploy.Labels {
if v.Key == nil || *v.Key != name {
labels = append(labels, v)
}
}
function.Deploy.Labels = labels
return loaderSaver.Save(function)
}
return runRemoveLabelsPrompt(function, loaderSaver)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(configLabelsCmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Add flags
configLabelsCmd.Flags().StringP("output", "o", "human", "Output format (human|json)")
configLabelsAddCmd.Flags().StringP("name", "", "", "Name of the label.")
configLabelsAddCmd.Flags().StringP("value", "", "", "Value of the label.")
configLabelsRemoveCmd.Flags().StringP("name", "", "", "Name of the label.")
addPathFlag(configLabelsCmd)
addPathFlag(configLabelsAddCmd)
addPathFlag(configLabelsRemoveCmd)
addVerboseFlag(configLabelsCmd, cfg.Verbose)
addVerboseFlag(configLabelsAddCmd, cfg.Verbose)
addVerboseFlag(configLabelsRemoveCmd, cfg.Verbose)
configLabelsCmd.AddCommand(configLabelsAddCmd)
configLabelsCmd.AddCommand(configLabelsRemoveCmd)
return configLabelsCmd
}
func listLabels(f fn.Function, w io.Writer, outputFormat Format) error {
switch outputFormat {
case Human:
if len(f.Deploy.Labels) == 0 {
_, err := fmt.Fprintln(w, "No labels defined")
return err
}
fmt.Fprintln(w, "Labels:")
for _, e := range f.Deploy.Labels {
_, err := fmt.Fprintln(w, " - ", e.String())
if err != nil {
return err
}
}
return nil
case JSON:
enc := json.NewEncoder(w)
return enc.Encode(f.Deploy.Labels)
default:
return fmt.Errorf("invalid format: %v", outputFormat)
}
}
func runAddLabelsPrompt(_ context.Context, f fn.Function, saver functionSaver) (err error) {
insertToIndex := 0
// SECTION - if there are some labels already set, choose the position of the new entry
if len(f.Deploy.Labels) > 0 {
options := []string{}
for _, e := range f.Deploy.Labels {
options = append(options, fmt.Sprintf("Insert before: %s", e.String()))
}
options = append(options, "Insert here.")
selectedLabel := ""
prompt := &survey.Select{
Message: "Where do you want to add the label?",
Options: options,
Default: options[len(options)-1],
}
err = survey.AskOne(prompt, &selectedLabel)
if err != nil {
return
}
for i, option := range options {
if option == selectedLabel {
insertToIndex = i
break
}
}
}
// SECTION - select the type of label to be added
selectedOption := ""
const (
optionLabelValue = "Label with a specified value"
optionLabelLocal = "Value from a local environment variable"
)
options := []string{optionLabelValue, optionLabelLocal}
err = survey.AskOne(&survey.Select{
Message: "What type of label do you want to add?",
Options: options,
}, &selectedOption)
if err != nil {
return
}
newPair := fn.Label{}
switch selectedOption {
// SECTION - add new label with the specified value
case optionLabelValue:
qs := []*survey.Question{
{
Name: "key",
Prompt: &survey.Input{Message: "Please specify the label key:"},
Validate: func(val interface{}) error {
return utils.ValidateLabelKey(val.(string))
},
},
{
Name: "value",
Prompt: &survey.Input{Message: "Please specify the label value:"},
Validate: func(val interface{}) error {
return utils.ValidateLabelValue(val.(string))
}},
}
answers := struct {
Key string
Value string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
return
}
newPair.Key = &answers.Key
newPair.Value = &answers.Value
// SECTION - add new label with value from a local environment variable
case optionLabelLocal:
qs := []*survey.Question{
{
Name: "key",
Prompt: &survey.Input{Message: "Please specify the label key:"},
Validate: func(val interface{}) error {
return utils.ValidateLabelKey(val.(string))
},
},
{
Name: "value",
Prompt: &survey.Input{Message: "Please specify the local environment variable:"},
Validate: func(val interface{}) error {
return utils.ValidateLabelValue(val.(string))
},
},
}
answers := struct {
Key string
Value string
}{}
err = survey.Ask(qs, &answers)
if err != nil {
return
}
if _, ok := os.LookupEnv(answers.Value); !ok {
fmt.Printf("Warning: specified local environment variable %q is not set\n", answers.Value)
}
value := fmt.Sprintf("{{ env:%s }}", answers.Value)
newPair.Key = &answers.Key
newPair.Value = &value
}
// we have all necessary information -> let's insert the label to the selected position in the list
if insertToIndex == len(f.Deploy.Labels) {
f.Deploy.Labels = append(f.Deploy.Labels, newPair)
} else {
f.Deploy.Labels = append(f.Deploy.Labels[:insertToIndex+1], f.Deploy.Labels[insertToIndex:]...)
f.Deploy.Labels[insertToIndex] = newPair
}
err = saver.Save(f)
if err == nil {
fmt.Println("Label entry was added to the function configuration")
}
return
}
func runRemoveLabelsPrompt(f fn.Function, saver functionSaver) (err error) {
if len(f.Deploy.Labels) == 0 {
fmt.Println("There aren't any configured labels")
return
}
options := []string{}
for _, e := range f.Deploy.Labels {
options = append(options, e.String())
}
selectedLabel := ""
prompt := &survey.Select{
Message: "Which labels do you want to remove?",
Options: options,
}
err = survey.AskOne(prompt, &selectedLabel)
if err != nil {
return
}
var newLabels []fn.Label
removed := false
for i, e := range f.Deploy.Labels {
if e.String() == selectedLabel {
newLabels = append(f.Deploy.Labels[:i], f.Deploy.Labels[i+1:]...)
removed = true
break
}
}
if removed {
f.Deploy.Labels = newLabels
err = saver.Save(f)
if err == nil {
fmt.Println("Label was removed from the function configuration")
}
}
return
}

View File

@ -1,199 +0,0 @@
//go:build linux
// +build linux
package cmd
import (
"context"
"os"
"reflect"
"sync"
"testing"
"time"
"github.com/Netflix/go-expect"
"github.com/creack/pty"
"github.com/hinshun/vt10x"
"github.com/spf13/cobra"
fn "knative.dev/func/pkg/functions"
)
type mockFunctionLoaderSaver struct {
f fn.Function
}
func (m *mockFunctionLoaderSaver) Load(path string) (fn.Function, error) {
return m.f, nil
}
func (m *mockFunctionLoaderSaver) Save(f fn.Function) error {
m.f = f
return nil
}
func assertLabelEq(t *testing.T, actual []fn.Label, want []fn.Label) {
t.Helper()
if !reflect.DeepEqual(actual, want) {
t.Errorf("labels = %v, want %v", actual, want)
}
}
func createRunFunc(cmd *cobra.Command, t *testing.T) func(subcmd string, input ...string) {
return func(subcmd string, input ...string) {
ctx := context.Background()
ptm, pts, err := pty.Open()
if err != nil {
t.Fatal(err)
}
term := vt10x.New(vt10x.WithWriter(pts))
c, err := expect.NewConsole(expect.WithStdin(ptm), expect.WithStdout(term), expect.WithCloser(ptm, pts))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { c.Close() })
var wg sync.WaitGroup
wg.Add(1)
go func() {
//defer wg.Done()
_, _ = c.ExpectEOF()
}()
go func() {
defer wg.Done()
time.Sleep(time.Millisecond * 50)
for _, s := range input {
_, _ = c.Send(s)
time.Sleep(time.Millisecond * 50)
}
}()
a := []string{subcmd}
cmd.SetArgs(a)
func() {
defer withMockedStdio(t, c)()
err = cmd.ExecuteContext(ctx)
wg.Wait()
}()
if err != nil {
t.Fatal(err)
}
}
}
func withMockedStdio(t *testing.T, c *expect.Console) func() {
t.Helper()
oldIn := os.Stdin
oldOut := os.Stdout
oldErr := os.Stderr
os.Stdin = c.Tty()
os.Stdout = c.Tty()
os.Stderr = c.Tty()
return func() {
os.Stdin = oldIn
os.Stdout = oldOut
os.Stderr = oldErr
}
}
const (
arrowUp = "\033[A"
arrowDown = "\033[B"
enter = "\r"
)
func TestNewConfigLabelsCmd(t *testing.T) {
var loaderSaver mockFunctionLoaderSaver
labels := &loaderSaver.f.Deploy.Labels
cmd := NewConfigLabelsCmd(&loaderSaver)
cmd.SetArgs([]string{})
run := createRunFunc(cmd, t)
p := func(k, v string) fn.Label {
return fn.Label{Key: &k, Value: &v}
}
assertLabel := func(ps []fn.Label) {
t.Helper()
assertLabelEq(t, *labels, ps)
}
run("add", enter, "a", enter, "b", enter)
assertLabel([]fn.Label{p("a", "b")})
run("add", enter, enter, "c", enter, "d", enter)
assertLabel([]fn.Label{p("a", "b"), p("c", "d")})
run("add", arrowUp, arrowUp, enter, enter, "e", enter, "f", enter)
assertLabel([]fn.Label{p("e", "f"), p("a", "b"), p("c", "d")})
run("remove", arrowDown, enter)
assertLabel([]fn.Label{p("e", "f"), p("c", "d")})
}
func TestListLabels(t *testing.T) {
p := func(k, v string) fn.Label {
return fn.Label{Key: &k, Value: &v}
}
var loaderSaver mockFunctionLoaderSaver
labels := &loaderSaver.f.Deploy.Labels
*labels = append(*labels, p("a", "b"), p("c", "d"))
cmd := NewConfigLabelsCmd(&loaderSaver)
cmd.SetArgs([]string{})
ctx := context.Background()
c, err := expect.NewConsole()
if err != nil {
t.Fatal(err)
}
defer c.Close()
errChan := make(chan error, 1)
func() {
var err error
defer func() {
errChan <- err
}()
defer withMockedStdio(t, c)()
err = cmd.ExecuteContext(ctx)
}()
expected := []string{
`Labels:`,
` - Label with key "a" and value "b"`,
` - Label with key "c" and value "d"`,
}
// prevents the ExpectString() function from waiting indefinitely
// in case when expected string is not printed to stdout nor the stdout is closed
go func() {
time.Sleep(time.Second * 5)
c.Close()
}()
for _, s := range expected {
out, err := c.ExpectString(s)
if err != nil {
t.Errorf("unexpected output: %q, err: %v\n", out, err)
}
}
err = <-errChan
if err != nil {
t.Fatal(err)
}
}

View File

@ -1,181 +0,0 @@
package cmd_test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"testing"
"github.com/ory/viper"
fnCmd "knative.dev/func/cmd"
fn "knative.dev/func/pkg/functions"
)
func TestListEnvs(t *testing.T) {
mock := newMockLoaderSaver()
foo := "foo"
bar := "bar"
envs := []fn.Env{{Name: &foo, Value: &bar}}
mock.load = func(path string) (fn.Function, error) {
if path != "<path>" {
t.Fatalf("bad path, got %q but expected <path>", path)
}
return fn.Function{Run: fn.RunSpec{Envs: envs}}, nil
}
cmd := fnCmd.NewConfigCmd(mock, fnCmd.NewClient)
cmd.SetArgs([]string{"envs", "-o=json", "--path=<path>"})
var buff bytes.Buffer
cmd.SetOut(&buff)
cmd.SetErr(&buff)
err := cmd.Execute()
if err != nil {
t.Fatal(err)
}
var data []fn.Env
err = json.Unmarshal(buff.Bytes(), &data)
if err != nil {
t.Fatal(err)
}
if !envsEqual(envs, data) {
t.Errorf("env mismatch, expedted %v but got %v", envs, data)
}
}
func TestListEnvAdd(t *testing.T) {
// strings as vars so we can take address of them
foo := "foo"
bar := "bar"
answer := "answer"
fortyTwo := "42"
configMapExpression := "{{ configMap:myMap }}"
mock := newMockLoaderSaver()
mock.load = func(path string) (fn.Function, error) {
return fn.Function{Run: fn.RunSpec{Envs: []fn.Env{{Name: &foo, Value: &bar}}}}, nil
}
var expectedEnvs []fn.Env
mock.save = func(f fn.Function) error {
if !envsEqual(expectedEnvs, f.Run.Envs) {
return fmt.Errorf("unexpected envs: got %v but %v was expected", f.Run.Envs, expectedEnvs)
}
return nil
}
expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: &answer, Value: &fortyTwo}}
cmd := fnCmd.NewConfigCmd(mock, fnCmd.NewClient)
cmd.SetArgs([]string{"envs", "add", "--name=answer", "--value=42"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()
if err != nil {
t.Error(err)
}
viper.Reset()
expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: nil, Value: &configMapExpression}}
cmd = fnCmd.NewConfigCmd(mock, fnCmd.NewClient)
cmd.SetArgs([]string{"envs", "add", "--value={{ configMap:myMap }}"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err = cmd.Execute()
if err != nil {
t.Error(err)
}
viper.Reset()
cmd = fnCmd.NewConfigCmd(mock, fnCmd.NewClient)
cmd.SetArgs([]string{"envs", "add", "--name=1", "--value=abc"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err = cmd.Execute()
if err == nil {
t.Error("expected variable name error but got nil")
}
}
func envsEqual(a, b []fn.Env) bool {
if len(a) != len(b) {
return false
}
strPtrEq := func(x, y *string) bool {
switch {
case x == nil && y == nil:
return true
case x != nil && y != nil:
return *x == *y
default:
return false
}
}
strPtrLess := func(x, y *string) bool {
switch {
case x == nil && y == nil:
return false
case x != nil && y != nil:
return *x < *y
case x == nil:
return true
default:
return false
}
}
lessForSlice := func(s []fn.Env) func(i, j int) bool {
return func(i, j int) bool {
x := s[i]
y := s[j]
if strPtrLess(x.Name, y.Name) {
return true
}
return strPtrLess(x.Value, y.Value)
}
}
sort.Slice(a, lessForSlice(a))
sort.Slice(b, lessForSlice(b))
for i := range a {
x := a[i]
y := b[i]
if !strPtrEq(x.Name, y.Name) || !strPtrEq(x.Value, y.Value) {
return false
}
}
return true
}
func newMockLoaderSaver() *mockLoaderSaver {
return &mockLoaderSaver{
load: func(path string) (fn.Function, error) {
return fn.Function{}, nil
},
save: func(f fn.Function) error {
return nil
},
}
}
type mockLoaderSaver struct {
load func(path string) (fn.Function, error)
save func(f fn.Function) error
}
func (m mockLoaderSaver) Load(path string) (fn.Function, error) {
return m.load(path)
}
func (m mockLoaderSaver) Save(f fn.Function) error {
return m.save(f)
}

View File

@ -1,433 +0,0 @@
package cmd
import (
"context"
"fmt"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s"
)
func NewConfigVolumesCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "volumes",
Short: "List and manage configured volumes for a function",
Long: `List and manage configured volumes for a function
Prints configured Volume mounts for a function project present in
the current directory or from the directory specified with --path.
`,
Aliases: []string{"volume"},
SuggestFor: []string{"vol", "volums", "vols"},
PreRunE: bindEnv("path", "verbose"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(defaultLoaderSaver)
if err != nil {
return
}
listVolumes(function)
return
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
configVolumesAddCmd := NewConfigVolumesAddCmd()
configVolumesRemoveCmd := NewConfigVolumesRemoveCmd()
addPathFlag(cmd)
addPathFlag(configVolumesAddCmd)
addPathFlag(configVolumesRemoveCmd)
addVerboseFlag(cmd, cfg.Verbose)
addVerboseFlag(configVolumesAddCmd, cfg.Verbose)
addVerboseFlag(configVolumesRemoveCmd, cfg.Verbose)
cmd.AddCommand(configVolumesAddCmd)
cmd.AddCommand(configVolumesRemoveCmd)
return cmd
}
func NewConfigVolumesAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Add volume to the function configuration",
Long: `Add volume to the function configuration
Interactive prompt to add Secrets and ConfigMaps as Volume mounts to the function project
in the current directory or from the directory specified with --path.
For non-interactive usage, use flags to specify the volume type and configuration.
`,
Example: `# Add a ConfigMap volume
{{rootCmdUse}} config volumes add --type=configmap --source=my-config --path=/etc/config
# Add a Secret volume
{{rootCmdUse}} config volumes add --type=secret --source=my-secret --path=/etc/secret
# Add a PersistentVolumeClaim volume
{{rootCmdUse}} config volumes add --type=pvc --source=my-pvc --path=/data
{{rootCmdUse}} config volumes add --type=pvc --source=my-pvc --path=/data --read-only
# Add an EmptyDir volume
{{rootCmdUse}} config volumes add --type=emptydir --path=/tmp/cache
{{rootCmdUse}} config volumes add --type=emptydir --path=/tmp/cache --size=1Gi --medium=Memory`,
SuggestFor: []string{"ad", "create", "insert", "append"},
PreRunE: bindEnv("path", "verbose", "type", "source", "mount-path", "read-only", "size", "medium"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(defaultLoaderSaver)
if err != nil {
return
}
// Check if flags are provided for non-interactive mode
volumeType, _ := cmd.Flags().GetString("type")
if volumeType != "" {
return runAddVolume(cmd, function)
}
// Fall back to interactive mode
return runAddVolumesPrompt(cmd.Context(), function)
},
}
// Add flags for non-interactive mode
cmd.Flags().StringP("type", "t", "", "Volume type: configmap, secret, pvc, or emptydir")
cmd.Flags().StringP("source", "s", "", "Name of the ConfigMap, Secret, or PVC to mount (not used for emptydir)")
cmd.Flags().StringP("mount-path", "m", "", "Path where the volume should be mounted in the container")
cmd.Flags().BoolP("read-only", "r", false, "Mount volume as read-only (only for PVC)")
cmd.Flags().StringP("size", "", "", "Maximum size limit for EmptyDir volume (e.g., 1Gi)")
cmd.Flags().StringP("medium", "", "", "Storage medium for EmptyDir volume: 'Memory' or '' (default)")
return cmd
}
func NewConfigVolumesRemoveCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Remove volume from the function configuration",
Long: `Remove volume from the function configuration
Interactive prompt to remove Volume mounts from the function project
in the current directory or from the directory specified with --path.
For non-interactive usage, use the --mount-path flag to specify which volume to remove.
`,
Example: `# Remove a volume by its mount path
{{rootCmdUse}} config volumes remove --mount-path=/etc/config`,
Aliases: []string{"rm"},
SuggestFor: []string{"del", "delete", "rmeove"},
PreRunE: bindEnv("path", "verbose", "mount-path"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
function, err := initConfigCommand(defaultLoaderSaver)
if err != nil {
return
}
// Check if mount-path flag is provided for non-interactive mode
mountPath, _ := cmd.Flags().GetString("mount-path")
if mountPath != "" {
return runRemoveVolume(cmd, function, mountPath)
}
// Fall back to interactive mode
return runRemoveVolumesPrompt(function)
},
}
// Add flag for non-interactive mode
cmd.Flags().StringP("mount-path", "m", "", "Path of the volume mount to remove")
return cmd
}
func listVolumes(f fn.Function) {
if len(f.Run.Volumes) == 0 {
fmt.Println("There aren't any configured Volume mounts")
return
}
fmt.Println("Configured Volumes mounts:")
for _, v := range f.Run.Volumes {
fmt.Println(" - ", v.String())
}
}
func runAddVolumesPrompt(ctx context.Context, f fn.Function) (err error) {
secrets, err := k8s.ListSecretsNamesIfConnected(ctx, f.Deploy.Namespace)
if err != nil {
return
}
configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, f.Deploy.Namespace)
if err != nil {
return
}
persistentVolumeClaims, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(ctx, f.Deploy.Namespace)
if err != nil {
return
}
// SECTION - select resource type to be mounted
options := []string{}
selectedOption := ""
const optionConfigMap = "ConfigMap"
const optionSecret = "Secret"
const optionPersistentVolumeClaim = "PersistentVolumeClaim"
const optionEmptyDir = "EmptyDir"
if len(configMaps) > 0 {
options = append(options, optionConfigMap)
}
if len(secrets) > 0 {
options = append(options, optionSecret)
}
if len(persistentVolumeClaims) > 0 {
options = append(options, optionPersistentVolumeClaim)
}
options = append(options, optionEmptyDir)
if len(options) == 1 {
selectedOption = options[0]
} else {
err = survey.AskOne(&survey.Select{
Message: "What do you want to mount as a Volume?",
Options: options,
}, &selectedOption)
if err != nil {
return
}
}
// SECTION - display a help message to enable advanced features
if selectedOption == optionEmptyDir || selectedOption == optionPersistentVolumeClaim {
fmt.Printf("Please make sure to enable the %s extension flag: https://knative.dev/docs/serving/configuration/feature-flags/\n", selectedOption)
}
// SECTION - select the specific resource to be mounted
optionsResoures := []string{}
switch selectedOption {
case optionConfigMap:
optionsResoures = configMaps
case optionSecret:
optionsResoures = secrets
case optionPersistentVolumeClaim:
optionsResoures = persistentVolumeClaims
}
selectedResource := ""
if selectedOption != optionEmptyDir {
err = survey.AskOne(&survey.Select{
Message: fmt.Sprintf("Which \"%s\" do you want to mount?", selectedOption),
Options: optionsResoures,
}, &selectedResource)
if err != nil {
return
}
}
// SECTION - specify mount Path of the Volume
path := ""
err = survey.AskOne(&survey.Input{
Message: fmt.Sprintf("Please specify the path where the %s should be mounted:", selectedOption),
}, &path, survey.WithValidator(func(val interface{}) error {
if str, ok := val.(string); !ok || len(str) <= 0 || !strings.HasPrefix(str, "/") {
return fmt.Errorf("the input must be non-empty absolute path")
}
return nil
}))
if err != nil {
return
}
// SECTION - is this read only for pvc
readOnly := false
if selectedOption == optionPersistentVolumeClaim {
err = survey.AskOne(&survey.Confirm{
Message: "Is this volume read-only?",
Default: false,
}, &readOnly)
if err != nil {
return
}
}
// we have all necessary information -> let's store the new Volume
newVolume := fn.Volume{Path: &path}
switch selectedOption {
case optionConfigMap:
newVolume.ConfigMap = &selectedResource
case optionSecret:
newVolume.Secret = &selectedResource
case optionPersistentVolumeClaim:
newVolume.PersistentVolumeClaim = &fn.PersistentVolumeClaim{
ClaimName: &selectedResource,
ReadOnly: readOnly,
}
case optionEmptyDir:
newVolume.EmptyDir = &fn.EmptyDir{}
}
f.Run.Volumes = append(f.Run.Volumes, newVolume)
err = f.Write()
if err == nil {
fmt.Println("Volume entry was added to the function configuration")
}
return
}
func runRemoveVolumesPrompt(f fn.Function) (err error) {
if len(f.Run.Volumes) == 0 {
fmt.Println("There aren't any configured Volume mounts")
return
}
options := []string{}
for _, v := range f.Run.Volumes {
options = append(options, v.String())
}
selectedVolume := ""
prompt := &survey.Select{
Message: "Which Volume do you want to remove?",
Options: options,
}
err = survey.AskOne(prompt, &selectedVolume)
if err != nil {
return
}
var newVolumes []fn.Volume
removed := false
for i, v := range f.Run.Volumes {
if v.String() == selectedVolume {
newVolumes = append(f.Run.Volumes[:i], f.Run.Volumes[i+1:]...)
removed = true
break
}
}
if removed {
f.Run.Volumes = newVolumes
err = f.Write()
if err == nil {
fmt.Println("Volume entry was removed from the function configuration")
}
}
return
}
// runAddVolume handles adding volumes using command line flags
func runAddVolume(cmd *cobra.Command, f fn.Function) error {
var (
volumeType, _ = cmd.Flags().GetString("type")
source, _ = cmd.Flags().GetString("source")
mountPath, _ = cmd.Flags().GetString("mount-path")
readOnly, _ = cmd.Flags().GetBool("read-only")
sizeLimit, _ = cmd.Flags().GetString("size")
medium, _ = cmd.Flags().GetString("medium")
)
// Validate mount path
if mountPath == "" {
return fmt.Errorf("--mount-path is required")
}
if !strings.HasPrefix(mountPath, "/") {
return fmt.Errorf("mount path must be an absolute path (start with /)")
}
// Create the volume based on type
newVolume := fn.Volume{Path: &mountPath}
// All volumeTypes except emptydir require a source
if volumeType != "emptydir" && source == "" {
return fmt.Errorf("--source is required for %s volumes", volumeType)
}
switch volumeType {
case "configmap":
newVolume.ConfigMap = &source
case "secret":
newVolume.Secret = &source
case "pvc":
newVolume.PersistentVolumeClaim = &fn.PersistentVolumeClaim{
ClaimName: &source,
ReadOnly: readOnly,
}
if readOnly {
fmt.Fprintf(cmd.OutOrStderr(), "PersistentVolumeClaim will be mounted as read-only")
}
fmt.Fprintf(cmd.OutOrStderr(), "Please ensure the PersistentVolumeClaim extension flag is enabled:\nhttps://knative.dev/docs/serving/configuration/feature-flags/\n")
case "emptydir":
emptyDir := &fn.EmptyDir{}
if sizeLimit != "" {
emptyDir.SizeLimit = &sizeLimit
}
if medium != "" {
if medium != fn.StorageMediumMemory && medium != fn.StorageMediumDefault {
return fmt.Errorf("invalid medium: must be 'Memory' or empty")
}
emptyDir.Medium = medium
}
newVolume.EmptyDir = emptyDir
fmt.Fprintf(cmd.OutOrStderr(), "Please make sure to enable the EmptyDir extension flag:\nhttps://knative.dev/docs/serving/configuration/feature-flags/\n")
default:
return fmt.Errorf("invalid volume type: %s (must be one of: configmap, secret, pvc, emptydir)", volumeType)
}
// Add the volume to the function
f.Run.Volumes = append(f.Run.Volumes, newVolume)
// Save the function
err := f.Write()
if err == nil {
fmt.Printf("Volume entry was added to the function configuration\n")
fmt.Printf("Added: %s\n", newVolume.String())
}
return err
}
// runRemoveVolume handles removing volumes by mount path
func runRemoveVolume(cmd *cobra.Command, f fn.Function, mountPath string) error {
if !strings.HasPrefix(mountPath, "/") {
return fmt.Errorf("mount path must be an absolute path (start with /)")
}
// Find and remove the volume with the specified path
var newVolumes []fn.Volume
removed := false
for _, v := range f.Run.Volumes {
if v.Path != nil && *v.Path == mountPath {
removed = true
} else {
newVolumes = append(newVolumes, v)
}
}
if !removed {
return fmt.Errorf("no volume found with mount path: %s", mountPath)
}
f.Run.Volumes = newVolumes
err := f.Write()
if err == nil {
fmt.Fprintf(cmd.OutOrStderr(), "Volume entry was removed from the function configuration\n")
fmt.Fprintf(cmd.OutOrStderr(), "Removed volume at path: %s\n", mountPath)
}
return err
}

View File

@ -1,570 +1,157 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"text/tabwriter"
"text/template"
"path/filepath"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/utils"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/prompt"
"github.com/boson-project/func/utils"
)
// ErrNoRuntime indicates that the language runtime flag was not passed.
type ErrNoRuntime error
func init() {
root.AddCommand(createCmd)
createCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
createCmd.Flags().StringP("runtime", "l", bosonFunc.DefaultRuntime, "Function runtime language/framework. Available runtimes: "+utils.RuntimeList()+" (Env: $FUNC_RUNTIME)")
createCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "templates"), "Path to additional templates (Env: $FUNC_TEMPLATES)")
createCmd.Flags().StringP("trigger", "t", bosonFunc.DefaultTrigger, "Function trigger. Available triggers: 'http' and 'events' (Env: $FUNC_TRIGGER)")
// ErrInvalidRuntime indicates that the passed language runtime was invalid.
type ErrInvalidRuntime error
// ErrInvalidTemplate indicates that the passed template was invalid.
type ErrInvalidTemplate error
// NewCreateCmd creates a create command using the given client creator.
func NewCreateCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a function",
Long: `
NAME
{{.Name}} create - Create a function
SYNOPSIS
{{.Name}} create [-l|--language] [-t|--template] [-r|--repository]
[-c|--confirm] [-v|--verbose] [path]
DESCRIPTION
Creates a new function project.
$ {{.Name}} create -l node
Creates a function in the current directory '.' which is written in the
language/runtime 'node' and handles HTTP events.
If [path] is provided, the function is initialized at that path, creating
the path if necessary.
To complete this command interactively, use --confirm (-c):
$ {{.Name}} create -c
Available Language Runtimes and Templates:
{{ .Options | indent 2 " " | indent 1 "\t" }}
To install more language runtimes and their templates see '{{.Name}} repository'.
EXAMPLES
o Create a Node.js function in the current directory (the default path) which
handles http events (the default template).
$ {{.Name}} create -l node
o Create a Node.js function in the directory 'myfunc'.
$ {{.Name}} create -l node myfunc
o Create a Go function which handles CloudEvents in ./myfunc.
$ {{.Name}} create -l go -t cloudevents myfunc
`,
SuggestFor: []string{"vreate", "creaet", "craete", "new"},
PreRunE: bindEnv("language", "template", "repository", "confirm", "verbose"),
Aliases: []string{"init"},
RunE: func(cmd *cobra.Command, args []string) error {
return runCreate(cmd, args, newClient)
},
if err := createCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Flags
cmd.Flags().StringP("language", "l", cfg.Language, "Language Runtime (see help text for list) ($FUNC_LANGUAGE)")
cmd.Flags().StringP("template", "t", fn.DefaultTemplate, "Function template. (see help text for list) ($FUNC_TEMPLATE)")
cmd.Flags().StringP("repository", "r", "", "URI to a Git repository containing the specified template ($FUNC_REPOSITORY)")
addConfirmFlag(cmd, cfg.Confirm)
// TODO: refactor to use --path like all the other commands
addVerboseFlag(cmd, cfg.Verbose)
// Help Action
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { runCreateHelp(cmd, args, newClient) })
// Tab completion
if err := cmd.RegisterFlagCompletionFunc("language", newRuntimeCompletionFunc(newClient)); err != nil {
fmt.Fprintf(os.Stderr, "unable to provide language runtime suggestions: %v", err)
}
if err := cmd.RegisterFlagCompletionFunc("template", newTemplateCompletionFunc(newClient)); err != nil {
fmt.Fprintf(os.Stderr, "unable to provide template suggestions: %v", err)
}
return cmd
}
// Run Create
func runCreate(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
// Config
// Create a config based on args. Also uses the newClient to create a
// temporary client for completing options such as available runtimes.
cfg, err := newCreateConfig(cmd, args, newClient)
if err != nil {
return
}
var createCmd = &cobra.Command{
Use: "create [PATH]",
Short: "Create a function project",
Long: `Create a function project
// Client
// From environment variables, flags, arguments, and user prompts if --confirm
// (in increasing levels of precedence)
client, done := newClient(
ClientConfig{Verbose: cfg.Verbose},
fn.WithRepository(cfg.Repository))
defer done()
Creates a new function project in PATH, or in the current directory if no PATH is given.
The name of the project is determined by the directory name the project is created in.
`,
Example: `
# Create a Node.js function project in the current directory, choosing the
# directory name as the project's name.
kn func create
// Validate - a deeper validation than that which is performed when
// instantiating the client with the raw config above.
if err = cfg.Validate(client); err != nil {
return
}
# Create a Quarkus function project in the directory "sample-service".
# The directory will be created in the local directory if non-existent and
# the project is called "sample-service"
kn func create --runtime quarkus myfunc
// Create
_, err = client.Init(fn.Function{
Name: cfg.Name,
Root: cfg.Path,
Runtime: cfg.Runtime,
Template: cfg.Template,
})
if err != nil {
# Create a function project that uses a CloudEvent based function signature
kn func create --trigger events myfunc
`,
SuggestFor: []string{"inti", "new"},
PreRunE: bindEnv("runtime", "templates", "trigger", "confirm"),
RunE: runCreate,
// TODO: autocomplate Functions for runtime and trigger.
}
func runCreate(cmd *cobra.Command, args []string) error {
config := newCreateConfig(args)
if err := utils.ValidateFunctionName(config.Name); err != nil {
return err
}
// Confirm
fmt.Fprintf(cmd.OutOrStderr(), "Created %v function in %v\n", cfg.Runtime, cfg.Path)
return nil
config = config.Prompt()
function := bosonFunc.Function{
Name: config.Name,
Root: config.Path,
Runtime: config.Runtime,
Trigger: config.Trigger,
}
client := bosonFunc.New(
bosonFunc.WithTemplates(config.Templates),
bosonFunc.WithVerbose(config.Verbose))
return client.Create(function)
}
type createConfig struct {
Path string // Absolute path to function source
Runtime string // Language Runtime
Repository string // Repository URI (overrides builtin and installed)
Verbose bool // Verbose output
Confirm bool // Confirm values via an interactive prompt
// Template is the code written into the new function project, including
// an implementation adhering to one of the supported function signatures.
// May also include additional configuration settings or examples.
// For example, embedded are 'http' for a function whose function signature
// is invoked via straight HTTP requests, or 'events' for a function which
// will be invoked with CloudEvents. These embedded templates contain a
// minimum implementation of the signature itself and example tests.
Template string
// Name of the function
// Name of the Function.
Name string
// Absolute path to Function on disk.
Path string
// Runtime language/framework.
Runtime string
// Templates is an optional path that, if it exists, will be used as a source
// for additional templates not included in the binary. If not provided
// explicitly as a flag (--templates) or env (FUNC_TEMPLATES), the default
// location is $XDG_CONFIG_HOME/templates ($HOME/.config/func/templates)
Templates string
// Trigger is the form of the resultant Function, i.e. the Function signature
// and contextually avaialable resources. For example 'http' for a Function
// expected to be invoked via straight HTTP requests, or 'events' for a
// Function which will be invoked with CloudEvents.
Trigger string
// Verbose output
Verbose bool
// Confirm: confirm values arrived upon from environment plus flags plus defaults,
// with interactive prompting (only applicable when attached to a TTY).
Confirm bool
}
// newCreateConfig returns a config populated from the current execution context
// (args, flags and environment variables)
// The client constructor function is used to create a transient client for
// accessing things like the current valid templates list, and uses the
// current value of the config at time of prompting.
func newCreateConfig(cmd *cobra.Command, args []string, newClient ClientFactory) (cfg createConfig, err error) {
var (
path string
dirName string
absolutePath string
)
if len(args) >= 1 {
path = args[0]
func newCreateConfig(args []string) createConfig {
var path string
if len(args) > 0 {
path = args[0] // If explicitly provided, use.
}
// Convert the path to an absolute path, and extract the ending directory name
// as the function name. TODO: refactor to be git-like with no name up-front
// and set instead as a named one-to-many deploy target.
dirName, absolutePath = deriveNameAndAbsolutePathFromPath(path)
// Config is the final default values based off the execution context.
// When prompting, these become the defaults presented.
cfg = createConfig{
Name: dirName, // TODO: refactor to be git-like
Path: absolutePath,
Repository: viper.GetString("repository"),
Runtime: viper.GetString("language"), // users refer to it is language
Template: viper.GetString("template"),
derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(path)
return createConfig{
Name: derivedName,
Path: derivedPath,
Runtime: viper.GetString("runtime"),
Templates: viper.GetString("templates"),
Trigger: viper.GetString("trigger"),
Confirm: viper.GetBool("confirm"),
Verbose: viper.GetBool("verbose"),
}
// If not in confirm/prompting mode, this cfg structure is complete.
if !cfg.Confirm {
return
}
// Create a tempoarary client for use by the following prompts to complete
// runtime/template suggestions etc
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
// IN confirm mode. If also in an interactive terminal, run prompts.
if interactiveTerminal() {
createdCfg, err := cfg.prompt(client)
if err != nil {
return createdCfg, err
}
fmt.Println("Command:")
fmt.Println(singleCommand(cmd, args, createdCfg))
return createdCfg, nil
}
// Confirming, but noninteractive
// Print out the final values as a confirmation. Only show Repository or
// Repositories, not both (repository takes precedence) in order to avoid
// likely confusion if both are displayed and one is empty.
// be removed and both displayed.
fmt.Printf("Path: %v\n", cfg.Path)
fmt.Printf("Language: %v\n", cfg.Runtime) // users refer to it as language
if cfg.Repository != "" { // if an override was provided
fmt.Printf("Repository: %v\n", cfg.Repository) // show only the override
}
fmt.Printf("Template: %v\n", cfg.Template)
return
}
// singleCommand that could be used by the current user to minimally recreate the current state.
func singleCommand(cmd *cobra.Command, args []string, cfg createConfig) string {
var b strings.Builder
b.WriteString(cmd.Root().Name()) // process executable
b.WriteString(" -l " + cfg.Runtime) // language runtime is required
if cmd.Flags().Lookup("template").Changed {
b.WriteString(" -t " + cfg.Template)
}
if cmd.Flags().Lookup("repository").Changed {
b.WriteString(" -r " + cfg.Repository)
}
if cmd.Flags().Lookup("verbose").Changed {
b.WriteString(fmt.Sprintf(" -v %v", cfg.Verbose))
}
if len(args) > 0 {
b.WriteString(" " + cfg.Path) // optional trailing <path> argument
}
return b.String()
}
// Validate the current state of the config, returning any errors.
// Note this is a deeper validation using a client already configured with a
// preliminary config object from flags/config, such that the client instance
// can be used to determine possible values for runtime, templates, etc. a
// pre-client validation should not be required, as the Client does its own
// validation.
func (c createConfig) Validate(client *fn.Client) (err error) {
// Confirm Name is valid
// Note that this is highly constricted, as it must currently adhere to the
// naming of a Knative Service, which itself is constrained to a Kubernetes
// Service, which itself is constrained to a DNS label (a subdomain).
// TODO: refactor to be git-like with no name at time of creation, but rather
// with named deployment targets in a one-to-many configuration.
dirName, _ := deriveNameAndAbsolutePathFromPath(c.Path)
if err = utils.ValidateFunctionName(dirName); err != nil {
return
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
// all prompts) was set (default).
func (c createConfig) Prompt() createConfig {
if !interactiveTerminal() || !c.Confirm {
// Just print the basics if not confirming
fmt.Printf("Project path: %v\n", c.Path)
fmt.Printf("Function name: %v\n", c.Name)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Trigger: %v\n", c.Trigger)
return c
}
// Validate Runtime and Template Name
//
// Perhaps additional validation would be of use here in the CLI, but
// the client libray itself is ultimately responsible for validating all input
// prior to exeuting any requests.
// Client validates both language runtime and template exist, with language runtime
// being a mandatory flag while defaulting template if not present to 'http'.
// However, if either of them are invalid, or the chosen combination does not exist,
// the error message is a rather terse one-liner. This is suitable for libraries, but
// for a CLI it behooves us to be more verbose, including valid options for
// each. So here, we check that the values entered (if any) are both valid
// and valid together.
if c.Runtime == "" {
return noRuntimeError(client)
var derivedName, derivedPath string
for {
derivedName, derivedPath = deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.Path, prompt.WithRequired(true)))
err := utils.ValidateFunctionName(derivedName)
if err == nil {
break
}
if c.Runtime != "" && c.Repository == "" &&
!isValidRuntime(client, c.Runtime) {
return newInvalidRuntimeError(client, c.Runtime)
fmt.Println("Error:", err)
}
if c.Template != "" && c.Repository == "" &&
!isValidTemplate(client, c.Runtime, c.Template) {
return newInvalidTemplateError(client, c.Runtime, c.Template)
}
return
}
// isValidRuntime determines if the given language runtime is a valid choice.
func isValidRuntime(client *fn.Client, runtime string) bool {
runtimes, err := client.Runtimes()
if err != nil {
return false
}
for _, v := range runtimes {
if v == runtime {
return true
}
}
return false
}
// isValidTemplate determines if the given template is valid for the given
// runtime.
func isValidTemplate(client *fn.Client, runtime, template string) bool {
if !isValidRuntime(client, runtime) {
return false
}
templates, err := client.Templates().List(runtime)
if err != nil {
return false
}
for _, v := range templates {
if v == template {
return true
}
}
return false
}
// noRuntimeError creates an error stating that the language flag
// is required, and a verbose list of valid options.
func noRuntimeError(client *fn.Client) error {
b := strings.Builder{}
fmt.Fprintf(&b, "Required flag \"language\" not set.\n")
fmt.Fprintln(&b, "Available language runtimes are:")
runtimes, err := client.Runtimes()
if err != nil {
return err
}
for _, v := range runtimes {
fmt.Fprintf(&b, " %v\n", v)
}
return ErrNoRuntime(errors.New(b.String()))
}
// newInvalidRuntimeError creates an error stating that the given language
// is not valid, and a verbose list of valid options.
func newInvalidRuntimeError(client *fn.Client, runtime string) error {
b := strings.Builder{}
fmt.Fprintf(&b, "The language runtime '%v' is not recognized.\n", runtime)
fmt.Fprintln(&b, "Available language runtimes are:")
runtimes, err := client.Runtimes()
if err != nil {
return err
}
for _, v := range runtimes {
fmt.Fprintf(&b, " %v\n", v)
}
return ErrInvalidRuntime(errors.New(b.String()))
}
// newInvalidTemplateError creates an error stating that the given template
// is not available for the given runtime, and a verbose list of valid options.
// The runtime is expected to already have been validated.
func newInvalidTemplateError(client *fn.Client, runtime, template string) error {
b := strings.Builder{}
fmt.Fprintf(&b, "The template '%v' was not found for language runtime '%v'.\n", template, runtime)
fmt.Fprintln(&b, "Available templates for this language runtime are:")
templates, err := client.Templates().List(runtime)
if err != nil {
return err
}
for _, v := range templates {
fmt.Fprintf(&b, " %v\n", v)
}
return ErrInvalidTemplate(errors.New(b.String()))
}
// prompt the user with value of config members, allowing for interactively
// mutating the values. The provided clientFn is used to construct a transient
// client for use during prompt autocompletion/suggestions (such as suggesting
// valid templates)
func (c createConfig) prompt(client *fn.Client) (createConfig, error) {
var qs []*survey.Question
runtimes, err := client.Runtimes()
if err != nil {
return createConfig{}, err
}
// First ask for path...
qs = []*survey.Question{
{
Name: "Path",
Prompt: &survey.Input{
Message: "Function Path:",
Default: c.Path,
},
Validate: func(val interface{}) error {
derivedName, _ := deriveNameAndAbsolutePathFromPath(val.(string))
return utils.ValidateFunctionName(derivedName)
},
Transform: func(ans interface{}) interface{} {
_, absolutePath := deriveNameAndAbsolutePathFromPath(ans.(string))
return absolutePath
},
}, {
Name: "Runtime",
Prompt: &survey.Select{
Message: "Language Runtime:",
Options: runtimes,
Default: surveySelectDefault(c.Runtime, runtimes),
},
}}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
// Second loop: choose template with autocompletion filtered by chosen runtime
qs = []*survey.Question{
{
Name: "Template",
Prompt: &survey.Input{
Message: "Template:",
Default: c.Template,
Suggest: func(prefix string) []string {
suggestions, err := templatesWithPrefix(prefix, c.Runtime, client)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to suggest: %v", err)
}
return suggestions
},
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
return c, nil
}
// Tab Completion and Prompt Suggestions Helpers
// ---------------------------------------------
type flagCompletionFunc func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)
func newRuntimeCompletionFunc(newClient ClientFactory) flagCompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cfg, err := newCreateConfig(cmd, args, newClient)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating client config for flag completion: %v", err)
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
return CompleteRuntimeList(cmd, args, toComplete, client)
return createConfig{
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Trigger: prompt.ForString("Trigger", c.Trigger),
// Templates intentionally omitted from prompt for being an edge case.
}
}
func newTemplateCompletionFunc(newClient ClientFactory) flagCompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cfg, err := newCreateConfig(cmd, args, newClient)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating client config for flag completion: %v", err)
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
return CompleteTemplateList(cmd, args, toComplete, client)
}
}
// return templates for language runtime whose full name (including repository)
// have the given prefix.
func templatesWithPrefix(prefix, runtime string, client *fn.Client) ([]string, error) {
var (
suggestions = []string{}
templates, err = client.Templates().List(runtime)
)
if err != nil {
return suggestions, err
}
for _, template := range templates {
if strings.HasPrefix(template, prefix) {
suggestions = append(suggestions, template)
}
}
return suggestions, nil
}
// runCreateHelp prints help for the create command using a template
// and options.
func runCreateHelp(cmd *cobra.Command, args []string, newClient ClientFactory) {
failSoft := func(err error) {
if err != nil {
fmt.Fprintf(cmd.OutOrStderr(), "error: help text may be partial: %v", err)
}
}
tpl := newHelpTemplate(cmd)
cfg, err := newCreateConfig(cmd, args, newClient)
failSoft(err)
client, done := newClient(
ClientConfig{Verbose: cfg.Verbose},
fn.WithRepository(cfg.Repository))
defer done()
options, err := RuntimeTemplateOptions(client) // human-friendly
failSoft(err)
var data = struct {
Options string
Name string
}{
Options: options,
Name: cmd.Root().Use,
}
if err := tpl.Execute(cmd.OutOrStdout(), data); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "unable to display help text: %v", err)
}
}
// newHelpTemplate returns a template for the create command's help text
func newHelpTemplate(cmd *cobra.Command) *template.Template {
body := cmd.Long + "\n\n" + cmd.UsageString()
t := template.New("help")
fm := template.FuncMap{
"indent": func(i int, c string, v string) string {
indentation := strings.Repeat(c, i)
return indentation + strings.ReplaceAll(v, "\n", "\n"+indentation)
},
}
t.Funcs(fm)
return template.Must(t.Parse(body))
}
// RuntimeTemplateOptions is a human-friendly table of valid Language Runtime
// to Template combinations.
// Exported for use in docs.
func RuntimeTemplateOptions(client *fn.Client) (string, error) {
runtimes, err := client.Runtimes()
if err != nil {
return "", err
}
builder := strings.Builder{}
writer := tabwriter.NewWriter(&builder, 0, 0, 3, ' ', 0)
fmt.Fprint(writer, "Language\tTemplate\n")
fmt.Fprint(writer, "--------\t--------\n")
for _, r := range runtimes {
templates, err := client.Templates().List(r)
// Not all language packs will have templates for
// all available runtimes. Without this check
if err != nil && !errors.Is(err, fn.ErrTemplateNotFound) {
return "", err
}
for _, t := range templates {
fmt.Fprintf(writer, "%v\t%v\n", r, t) // write tabbed
}
}
writer.Flush()
return builder.String(), nil
}

View File

@ -1,100 +0,0 @@
package cmd
import (
"errors"
"testing"
. "knative.dev/func/pkg/testing"
"knative.dev/func/pkg/utils"
)
// TestCreate_Execute ensures that an invocation of create with minimal settings
// and valid input completes without error; degenerate case.
func TestCreate_Execute(t *testing.T) {
_ = FromTempDirectory(t)
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"--language", "go", "myfunc"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
}
// TestCreate_NoRuntime ensures that an invocation of create must be
// done with a runtime.
func TestCreate_NoRuntime(t *testing.T) {
_ = FromTempDirectory(t)
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"myfunc"}) // Do not use test command args
err := cmd.Execute()
var e ErrNoRuntime
if !errors.As(err, &e) {
t.Fatalf("Did not receive ErrNoRuntime. Got %v", err)
}
}
// TestCreate_WithNoRuntime ensures that an invocation of create must be
// done with one of the valid runtimes only.
func TestCreate_WithInvalidRuntime(t *testing.T) {
_ = FromTempDirectory(t)
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"--language", "invalid", "myfunc"})
err := cmd.Execute()
var e ErrInvalidRuntime
if !errors.As(err, &e) {
t.Fatalf("Did not receive ErrInvalidRuntime. Got %v", err)
}
}
// TestCreate_InvalidTemplate ensures that an invocation of create must be
// done with one of the valid templates only.
func TestCreate_InvalidTemplate(t *testing.T) {
_ = FromTempDirectory(t)
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"--language", "go", "--template", "invalid", "myfunc"})
err := cmd.Execute()
var e ErrInvalidTemplate
if !errors.As(err, &e) {
t.Fatalf("Did not receive ErrInvalidTemplate. Got %v", err)
}
}
// TestCreate_ValidatesName ensures that the create command only accepts
// DNS-1123 labels for function name.
func TestCreate_ValidatesName(t *testing.T) {
_ = FromTempDirectory(t)
// Execute the command with a function name containing invalid characters and
// confirm the expected error is returned
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"invalid!"})
err := cmd.Execute()
var e utils.ErrInvalidFunctionName
if !errors.As(err, &e) {
t.Fatalf("Did not receive ErrInvalidFunctionName. Got %v", err)
}
}
// TestCreate_ConfigOptional ensures that the system can be used without
// any additional configuration being required.
func TestCreate_ConfigOptional(t *testing.T) {
_ = FromTempDirectory(t)
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"--language=go", "myfunc"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Not failing is success. Config files or settings beyond what are
// automatically written to to the given config home are currently optional.
}

View File

@ -3,17 +3,21 @@ package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
fn "github.com/boson-project/func"
"github.com/boson-project/func/knative"
"github.com/boson-project/func/prompt"
)
func NewDeleteCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "delete <name>",
func init() {
root.AddCommand(deleteCmd)
}
func NewDeleteCmd(newRemover func(ns string, verbose bool) (fn.Remover, error)) *cobra.Command {
delCmd := &cobra.Command{
Use: "delete [NAME]",
Short: "Undeploy a function",
Long: `Undeploy a function
@ -25,134 +29,105 @@ No local files are deleted.
`,
Example: `
# Undeploy the function defined in the local directory
{{rootCmdUse}} delete
kn func delete
# Undeploy the function 'myfunc' in namespace 'apps'
{{rootCmdUse}} delete myfunc --namespace apps
kn func delete -n apps myfunc
`,
SuggestFor: []string{"remove", "del"},
Aliases: []string{"rm"},
SuggestFor: []string{"remove", "rm", "del"},
ValidArgsFunction: CompleteFunctionList,
PreRunE: bindEnv("path", "confirm", "all", "namespace", "verbose"),
SilenceUsage: true, // no usage dump on error
RunE: func(cmd *cobra.Command, args []string) error {
return runDelete(cmd, args, newClient)
PreRunE: bindEnv("path", "confirm", "namespace"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
config := newDeleteConfig(args).Prompt()
var function fn.Function
// Initialize func with explicit name (when provided)
if len(args) > 0 && args[0] != "" {
pathChanged := cmd.Flags().Changed("path")
if pathChanged {
return fmt.Errorf("Only one of --path and [NAME] should be provided")
}
function = fn.Function{
Name: args[0],
}
} else {
function, err = fn.NewFunction(config.Path)
if err != nil {
return
}
// Check if the Function has been initialized
if !function.Initialized() {
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
}
}
ns := config.Namespace
if ns == "" {
ns = function.Namespace
}
remover, err := newRemover(ns, config.Verbose)
if err != nil {
return
}
client := fn.New(
fn.WithVerbose(config.Verbose),
fn.WithRemover(remover))
return client.Remove(cmd.Context(), function)
},
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
delCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
delCmd.Flags().StringP("path", "p", cwd(), "Path to the function project that should be undeployed (Env: $FUNC_PATH)")
delCmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
// Flags
cmd.Flags().StringP("namespace", "n", defaultNamespace(fn.Function{}, false), "The namespace when deleting by name. ($FUNC_NAMESPACE)")
cmd.Flags().StringP("all", "a", "true", "Delete all resources created for a function, eg. Pipelines, Secrets, etc. ($FUNC_ALL) (allowed values: \"true\", \"false\")")
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
return delCmd
}
func runDelete(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
cfg, err := newDeleteConfig(cmd, args)
var deleteCmd = NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
r, err := knative.NewRemover(ns)
if err != nil {
return
return nil, err
}
if cfg, err = cfg.Prompt(); err != nil {
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
if cfg.Name != "" { // Delete by name if provided
return client.Remove(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{}, cfg.All)
} else { // Otherwise; delete the function at path (cwd by default)
f, err := fn.NewFunction(cfg.Path)
if err != nil {
return err
}
return client.Remove(cmd.Context(), "", "", f, cfg.All)
}
}
r.Verbose = verbose
return r, nil
})
type deleteConfig struct {
Name string
Namespace string
Path string
All bool
Verbose bool
}
// newDeleteConfig returns a config populated from the current execution context
// (args, flags and environment variables)
func newDeleteConfig(cmd *cobra.Command, args []string) (cfg deleteConfig, err error) {
func newDeleteConfig(args []string) deleteConfig {
var name string
if len(args) > 0 {
name = args[0]
}
cfg = deleteConfig{
All: viper.GetBool("all"),
Name: name, // args[0] or derived
Namespace: viper.GetString("namespace"),
return deleteConfig{
Path: viper.GetString("path"),
Namespace: viper.GetString("namespace"),
Name: deriveName(name, viper.GetString("path")), // args[0] or derived
Verbose: viper.GetBool("verbose"), // defined on root
}
if cfg.Name == "" && cmd.Flags().Changed("namespace") {
// logicially inconsistent to supply only a namespace.
// Either use the function's local state in its entirety, or specify
// both a name and a namespace to ignore any local function source.
err = fmt.Errorf("must also specify a name when specifying namespace")
}
if cfg.Name != "" && cmd.Flags().Changed("path") {
// logically inconsistent to provide both a name and a path to source.
// Either use the function's local state on disk (--path), or specify
// a name and a namespace to ignore any local function source.
err = fmt.Errorf("only one of --path and [NAME] should be provided")
}
return
}
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c deleteConfig) Prompt() (deleteConfig, error) {
func (c deleteConfig) Prompt() deleteConfig {
if !interactiveTerminal() || !viper.GetBool("confirm") {
return c, nil
return c
}
dc := c
var qs = []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{
Message: "Function to remove:",
Default: deriveName(c.Name, c.Path)},
Validate: survey.Required,
},
{
Name: "all",
Prompt: &survey.Confirm{
Message: "Do you want to delete all resources?",
Default: c.All,
},
},
return deleteConfig{
// TODO: Path should be prompted for and set prior to name attempting path derivation. Test/fix this if necessary.
Name: prompt.ForString("Function to remove", deriveName(c.Name, c.Path), prompt.WithRequired(true)),
}
answers := struct {
Name string
All bool
}{}
err := survey.Ask(qs, &answers)
if err != nil {
return dc, err
}
dc.Name = answers.Name
dc.All = answers.All
return dc, err
}

View File

@ -2,319 +2,126 @@ package cmd
import (
"context"
fn "github.com/boson-project/func"
"io/ioutil"
"os"
"path/filepath"
"testing"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)
// TestDelete_Default ensures that the deployed function is deleted correctly
// with default options and the default situation: running "delete" from
// within the same directory of the function which is to be deleted.
func TestDelete_Default(t *testing.T) {
var (
err error
root = FromTempDirectory(t)
name = "myfunc"
namespace = "testns"
remover = mock.NewRemover()
ctx = context.Background()
)
// Remover which confirms the name and namespace received are those
// originally requested via the CLI flags.
remover.RemoveFn = func(n, ns string) error {
if n != name {
t.Errorf("expected name '%v', got '%v'", name, n)
}
if ns != namespace {
t.Errorf("expected namespace '%v', got '%v'", namespace, ns)
}
return nil
}
// A function which will be created in the requested namespace
f := fn.Function{
Runtime: "go",
Name: name,
Namespace: namespace,
Root: root,
Registry: TestRegistry,
}
if _, f, err = fn.New().New(ctx, f); err != nil {
t.Fatal(err)
}
if err = f.Write(); err != nil {
t.Fatal(err)
}
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Fail if remover's .Remove not invoked at all
if !remover.RemoveInvoked {
t.Fatal("fn.Remover not invoked")
}
type testRemover struct {
invokedWith *string
}
// TestDelete_ByName ensures that running delete specifying the name of the
// function explicitly as an argument invokes the remover appropriately.
func TestDelete_ByName(t *testing.T) {
var (
root = FromTempDirectory(t)
testname = "testname" // explicit name for the function
testnamespace = "testnamespace" // explicit namespace for the function
remover = mock.NewRemover() // with a mock remover
err error
)
// Remover fails the test if it receives the incorrect name
remover.RemoveFn = func(n, _ string) error {
if n != testname {
t.Fatalf("expected delete name %v, got %v", testname, n)
}
func (t *testRemover) Remove(ctx context.Context, name string) error {
t.invokedWith = &name
return nil
}
f := fn.Function{
Root: root,
Runtime: "go",
Registry: TestRegistry,
Name: "testname",
}
if f, err = fn.New().Init(f); err != nil {
t.Fatal(err)
}
// simulate deployed function in namespace for the client Remover
f.Deploy.Namespace = testnamespace
if err = f.Write(); err != nil {
t.Fatal(err)
}
// Create a command with a client constructor fn that instantiates a client
// with a mocked remover.
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
cmd.SetArgs([]string{testname}) // run: func delete <name>
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Fail if remover's .Remove not invoked at all
if !remover.RemoveInvoked {
t.Fatal("fn.Remover not invoked")
}
}
// TestDelete_Namespace ensures that remover is envoked when --namespace flag is
// given --> func delete myfunc --namespace myns
func TestDelete_Namespace(t *testing.T) {
var (
namespace = "myns"
remover = mock.NewRemover()
testname = "testname"
)
// test delete outside project just using function name
func TestDeleteCmdWithoutProject(t *testing.T) {
tr := &testRemover{}
cmd := NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
return tr, nil
})
remover.RemoveFn = func(_, ns string) error {
if ns != namespace {
t.Fatalf("expected delete namespace '%v', got '%v'", namespace, ns)
}
return nil
}
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
cmd.SetArgs([]string{testname, "--namespace", namespace})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if !remover.RemoveInvoked {
t.Fatal("remover was not invoked")
}
}
// TestDelete_NamespaceFlagPriority ensures that even thought there is
// a deployed function the namespace flag takes precedence and essentially
// ignores the the function on disk
func TestDelete_NamespaceFlagPriority(t *testing.T) {
var (
root = FromTempDirectory(t)
namespace = "myns"
namespace2 = "myns2"
remover = mock.NewRemover()
testname = "testname"
err error
)
remover.RemoveFn = func(_, ns string) error {
if ns != namespace2 {
t.Fatalf("expected delete namespace '%v', got '%v'", namespace2, ns)
}
return nil
}
// Ensure the extant function's namespace is used
f := fn.Function{
Name: testname,
Root: root,
Runtime: "go",
Registry: TestRegistry,
Namespace: namespace,
}
client := fn.New()
_, _, err = client.New(context.Background(), f)
cmd.SetArgs([]string{"foo"})
err := cmd.Execute()
if err != nil {
t.Fatal(err)
}
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
cmd.SetArgs([]string{testname, "--namespace", namespace2})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
if tr.invokedWith == nil {
t.Fatal("fn.Remover has not been invoked")
}
if !remover.RemoveInvoked {
t.Fatal("remover was not invoked")
if *tr.invokedWith != "foo" {
t.Fatalf("expected fn.Remover to be called with 'foo', but was called with '%s'", *tr.invokedWith)
}
}
// TestDelete_NamespaceWithoutNameFails ensures that providing wrong argument
// combination fails nice and fast (no name of the Function)
func TestDelete_NamespaceWithoutNameFails(t *testing.T) {
_ = FromTempDirectory(t)
cmd := NewDeleteCmd(NewTestClient())
cmd.SetArgs([]string{"--namespace=myns"})
if err := cmd.Execute(); err == nil {
t.Fatal("invoking Delete with namespace BUT without name provided anywhere")
}
}
// TestDelete_ByProject ensures that running delete with a valid project as its
// context invokes remove and with the correct name (reads name from func.yaml)
func TestDelete_ByProject(t *testing.T) {
_ = FromTempDirectory(t)
// Write a func.yaml config which specifies a name
// test delete from inside project directory (reading func name from func.yaml)
func TestDeleteCmdWithProject(t *testing.T) {
funcYaml := `name: bar
namespace: "func"
namespace: ""
runtime: go
image: ""
imageDigest: ""
trigger: http
builder: quay.io/boson/faas-go-builder
builders:
builderMap:
default: quay.io/boson/faas-go-builder
envs: []
env: {}
annotations: {}
labels: []
created: 2021-01-01T00:00:00+00:00
`
if err := os.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
tmpDir, err := ioutil.TempDir("", "bar")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// A mock remover which fails if the name from the func.yaml is not received.
remover := mock.NewRemover()
remover.RemoveFn = func(n, _ string) error {
if n != "bar" {
t.Fatalf("expected name 'bar', got '%v'", n)
f, err := os.Create(filepath.Join(tmpDir, "func.yaml"))
if err != nil {
t.Fatal(err)
}
return nil
defer f.Close()
_, err = f.WriteString(funcYaml)
if err != nil {
t.Fatal(err)
}
f.Close()
// Command with a Client constructor that returns client with the
// mocked remover.
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
cmd.SetArgs([]string{}) // Do not use test command args
// Execute the command simulating no arguments.
err := cmd.Execute()
oldWD, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer func() {
err = os.Chdir(oldWD)
if err != nil {
t.Fatal(err)
}
}()
err = os.Chdir(tmpDir)
if err != nil {
t.Fatal(err)
}
// Also fail if remover's .Remove is not invoked
if !remover.RemoveInvoked {
t.Fatal("fn.Remover not invoked")
}
}
tr := &testRemover{}
cmd := NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
return tr, nil
})
// TestDelete_ByPath ensures that providing only path deletes the Function
// successfully
func TestDelete_ByPath(t *testing.T) {
var (
// A mock remover which will be sampled to ensure it is not invoked.
remover = mock.NewRemover()
root = FromTempDirectory(t)
err error
namespace = "func"
)
// Ensure the extant function's namespace is used
f := fn.Function{
Root: root,
Runtime: "go",
Registry: TestRegistry,
Deploy: fn.DeploySpec{Namespace: namespace},
}
// Initialize a function in temp dir
if f, err = fn.New().Init(f); err != nil {
t.Fatal(err)
}
if err = f.Write(); err != nil {
t.Fatal(err)
}
// Command with a Client constructor using the mock remover.
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
// Execute the command only with the path argument
cmd.SetArgs([]string{"-p", root})
cmd.SetArgs([]string{"-p", "."})
err = cmd.Execute()
if err != nil {
t.Fatalf("failed with: %v", err)
t.Fatal(err)
}
// Also fail if remover's .Remove is not invoked.
if !remover.RemoveInvoked {
t.Fatal("fn.Remover not invoked despite valid argument")
if tr.invokedWith == nil {
t.Fatal("fn.Remover has not been invoked")
}
if *tr.invokedWith != "bar" {
t.Fatalf("expected fn.Remover to be called with 'bar', but was called with '%s'", *tr.invokedWith)
}
}
// TestDelete_NameAndPathExclusivity ensures that providing both a name and a
// path generates an error.
// Providing the --path (-p) flag indicates the name of the function to delete
// is to be taken from the function at the given path. Providing the name as
// an argument as well is therefore redundant and an error.
func TestDelete_NameAndPathExclusivity(t *testing.T) {
// test where both name and path are provided
func TestDeleteCmdWithBothPathAndName(t *testing.T) {
tr := &testRemover{}
cmd := NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
return tr, nil
})
// A mock remover which will be sampled to ensure it is not invoked.
remover := mock.NewRemover()
// Command with a Client constructor using the mock remover.
cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
// Execute the command simulating the invalid argument combination of both
// a path and an explicit name.
cmd.SetArgs([]string{"-p", "./testpath", "testname"})
cmd.SetArgs([]string{"foo", "-p", "/adir/"})
err := cmd.Execute()
if err == nil {
// TODO should really either parse the output or use typed errors to ensure it's
// failing for the expected reason.
t.Fatalf("expected error on conflicting flags not received")
t.Fatal("error was expected as both name an path cannot be used together")
}
// Also fail if remover's .Remove is invoked.
if remover.RemoveInvoked {
t.Fatal("fn.Remover invoked despite invalid combination and an error")
if tr.invokedWith != nil {
t.Fatal("fn.Remove was call when it shouldn't have been")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ package cmd
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"os"
@ -12,86 +11,73 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/knative"
)
func NewDescribeCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "describe <name>",
Short: "Describe a function",
Long: `Describe a function
func init() {
root.AddCommand(describeCmd)
describeCmd.Flags().StringP("namespace", "n", "", "Namespace of the function. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
describeCmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml) (Env: $FUNC_OUTPUT)")
describeCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
Prints the name, route and event subscriptions for a deployed function in
err := describeCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
if err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
}
var describeCmd = &cobra.Command{
Use: "describe <name>",
Short: "Show details of a function",
Long: `Show details of a function
Prints the name, route and any event subscriptions for a deployed function in
the current directory or from the directory specified with --path.
`,
Example: `
# Show the details of a function as declared in the local func.yaml
{{rootCmdUse}} describe
kn func describe
# Show the details of the function in the directory with yaml output
{{rootCmdUse}} describe --output yaml --path myotherfunc
# Show the details of the function in the myotherfunc directory with yaml output
kn func describe --output yaml --path myotherfunc
`,
SuggestFor: []string{"ifno", "fino", "get"},
SuggestFor: []string{"desc", "get"},
ValidArgsFunction: CompleteFunctionList,
Aliases: []string{"info", "desc"},
PreRunE: bindEnv("output", "path", "namespace", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runDescribe(cmd, args, newClient)
},
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Flags
cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml|url) ($FUNC_OUTPUT)")
cmd.Flags().StringP("namespace", "n", defaultNamespace(fn.Function{}, false), "The namespace in which to look for the named function. ($FUNC_NAMESPACE)")
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
return cmd
PreRunE: bindEnv("namespace", "output", "path"),
RunE: runDescribe,
}
func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
cfg, err := newDescribeConfig(cmd, args)
func runDescribe(cmd *cobra.Command, args []string) (err error) {
config := newDescribeConfig(args)
function, err := bosonFunc.NewFunction(config.Path)
if err != nil {
return
}
// TODO cfg.Prompt()
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
var details fn.Instance
if cfg.Name != "" { // Describe by name if provided
details, err = client.Describe(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{})
if err != nil {
return err
}
} else {
f, err := fn.NewFunction(cfg.Path)
if err != nil {
return err
}
if !f.Initialized() {
return errors.New("function not found at this path and no name provided")
}
details, err = client.Describe(cmd.Context(), "", "", f)
if err != nil {
return err
}
// Check if the Function has been initialized
if !function.Initialized() {
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
}
write(os.Stdout, info(details), cfg.Output)
describer, err := knative.NewDescriber(config.Namespace)
if err != nil {
return
}
describer.Verbose = config.Verbose
client := bosonFunc.New(
bosonFunc.WithVerbose(config.Verbose),
bosonFunc.WithDescriber(describer))
d, err := client.Describe(cmd.Context(), config.Name, config.Path)
if err != nil {
return
}
d.Image = function.Image
write(os.Stdout, description(d), config.Output)
return
}
@ -106,105 +92,72 @@ type describeConfig struct {
Verbose bool
}
func newDescribeConfig(cmd *cobra.Command, args []string) (cfg describeConfig, err error) {
func newDescribeConfig(args []string) describeConfig {
var name string
if len(args) > 0 {
name = args[0]
}
cfg = describeConfig{
Name: name,
return describeConfig{
Name: deriveName(name, viper.GetString("path")),
Namespace: viper.GetString("namespace"),
Output: viper.GetString("output"),
Path: viper.GetString("path"),
Verbose: viper.GetBool("verbose"),
}
if cfg.Name == "" && cmd.Flags().Changed("namespace") {
// logicially inconsistent to supply only a namespace.
// Either use the function's local state in its entirety, or specify
// both a name and a namespace to ignore any local function source.
err = fmt.Errorf("must also specify a name when specifying namespace")
}
if cfg.Name != "" && cmd.Flags().Changed("path") {
// logically inconsistent to provide both a name and a path to source.
// Either use the function's local state on disk (--path), or specify
// a name and a namespace to ignore any local function source.
err = fmt.Errorf("only one of --path and [NAME] should be provided")
}
return
}
// Output Formatting (serializers)
// -------------------------------
type info fn.Instance
type description bosonFunc.Description
func (i info) Human(w io.Writer) error {
func (d description) Human(w io.Writer) error {
fmt.Fprintln(w, "Function name:")
fmt.Fprintf(w, " %v\n", i.Name)
fmt.Fprintf(w, " %v\n", d.Name)
fmt.Fprintln(w, "Function is built in image:")
fmt.Fprintf(w, " %v\n", i.Image)
fmt.Fprintf(w, " %v\n", d.Image)
fmt.Fprintln(w, "Function is deployed in namespace:")
fmt.Fprintf(w, " %v\n", i.Namespace)
fmt.Fprintf(w, " %v\n", d.Namespace)
fmt.Fprintln(w, "Routes:")
for _, route := range i.Routes {
for _, route := range d.Routes {
fmt.Fprintf(w, " %v\n", route)
}
if len(i.Subscriptions) > 0 {
if len(d.Subscriptions) > 0 {
fmt.Fprintln(w, "Subscriptions (Source, Type, Broker):")
for _, s := range i.Subscriptions {
for _, s := range d.Subscriptions {
fmt.Fprintf(w, " %v %v %v\n", s.Source, s.Type, s.Broker)
}
}
if len(i.Labels) > 0 {
fmt.Fprintln(w, "Labels:")
for k, v := range i.Labels {
fmt.Fprintf(w, " %v: %v\n", k, v)
}
}
return nil
}
func (i info) Plain(w io.Writer) error {
fmt.Fprintf(w, "Name %v\n", i.Name)
fmt.Fprintf(w, "Image %v\n", i.Image)
fmt.Fprintf(w, "Namespace %v\n", i.Namespace)
func (d description) Plain(w io.Writer) error {
fmt.Fprintf(w, "Name %v\n", d.Name)
fmt.Fprintf(w, "Image %v\n", d.Image)
fmt.Fprintf(w, "Namespace %v\n", d.Namespace)
for _, route := range i.Routes {
for _, route := range d.Routes {
fmt.Fprintf(w, "Route %v\n", route)
}
if len(i.Subscriptions) > 0 {
for _, s := range i.Subscriptions {
if len(d.Subscriptions) > 0 {
for _, s := range d.Subscriptions {
fmt.Fprintf(w, "Subscription %v %v %v\n", s.Source, s.Type, s.Broker)
}
}
if len(i.Labels) > 0 {
for k, v := range i.Labels {
fmt.Fprintf(w, "Label %v %v\n", k, v)
}
}
return nil
}
func (i info) JSON(w io.Writer) error {
return json.NewEncoder(w).Encode(i)
func (d description) JSON(w io.Writer) error {
return json.NewEncoder(w).Encode(d)
}
func (i info) XML(w io.Writer) error {
return xml.NewEncoder(w).Encode(i)
func (d description) XML(w io.Writer) error {
return xml.NewEncoder(w).Encode(d)
}
func (i info) YAML(w io.Writer) error {
return yaml.NewEncoder(w).Encode(i)
}
func (i info) URL(w io.Writer) error {
if len(i.Routes) > 0 {
fmt.Fprintf(w, "%s\n", i.Routes[0])
}
return nil
func (d description) YAML(w io.Writer) error {
return yaml.NewEncoder(w).Encode(d)
}

View File

@ -1,135 +0,0 @@
package cmd
import (
"context"
"strings"
"testing"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)
// TestDescribe_Default ensures that running describe when there is no
// function in the given directory fails correctly.
func TestDescribe_Default(t *testing.T) {
_ = FromTempDirectory(t)
describer := mock.NewDescriber()
cmd := NewDescribeCmd(NewTestClient(fn.WithDescriber(describer)))
cmd.SetArgs([]string{})
err := cmd.Execute()
if err == nil {
t.Fatal("describing a nonexistent function should error")
}
if !strings.Contains(err.Error(), "function not found at this path and no name provided") {
t.Fatalf("Unexpected error text returned: %v", err)
}
if describer.DescribeInvoked {
t.Fatal("Describer incorrectly invoked")
}
}
// TestDescribe_Undeployed ensures that describing a function which exists,
// but has not been deployed, does not error but rather delegates to the
// deployer which will presumably describe it as being !deployed (See deployer
// test suite)
func TestDescribe_Undeployed(t *testing.T) {
root := FromTempDirectory(t)
client := fn.New()
_, err := client.Init(fn.Function{
Name: "testfunc",
Runtime: "go",
Registry: TestRegistry,
Root: root,
})
if err != nil {
t.Fatal(err)
}
describer := mock.NewDescriber()
cmd := NewDescribeCmd(NewTestClient(fn.WithDescriber(describer)))
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if !describer.DescribeInvoked {
t.Fatal("Describer should have been invoked for any initialized function")
}
}
// TestDescribe_ByName ensures that describing a function by name invokes
// the describer appropriately.
func TestDescribe_ByName(t *testing.T) {
var (
testname = "testname"
describer = mock.NewDescriber()
)
describer.DescribeFn = func(_ context.Context, name, namespace string) (fn.Instance, error) {
if name != testname {
t.Fatalf("expected describe name '%v', got '%v'", testname, name)
}
return fn.Instance{}, nil
}
cmd := NewDescribeCmd(NewTestClient(fn.WithDescriber(describer)))
cmd.SetArgs([]string{testname})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if !describer.DescribeInvoked {
t.Fatal("Describer not invoked")
}
}
// TestDescribe_ByProject ensures that describing the currently active project
// (func created in the current working directory) invokes the describer with
// its name correctly.
func TestDescribe_ByProject(t *testing.T) {
root := FromTempDirectory(t)
expected := "testname"
_, err := fn.New().Init(fn.Function{
Name: expected,
Runtime: "go",
Registry: TestRegistry,
Root: root,
})
if err != nil {
t.Fatal(err)
}
describer := mock.NewDescriber()
describer.DescribeFn = func(_ context.Context, name, namespace string) (i fn.Instance, err error) {
if name != expected {
t.Fatalf("expected describer to receive name %q, got %q", expected, name)
}
return
}
cmd := NewDescribeCmd(NewTestClient(fn.WithDescriber(describer)))
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
}
// TestDescribe_NameAndPathExclusivity ensures that providing both a name
// and a path will generate an error.
func TestDescribe_NameAndPathExclusivity(t *testing.T) {
d := mock.NewDescriber()
cmd := NewDescribeCmd(NewTestClient(fn.WithDescriber(d)))
cmd.SetArgs([]string{"-p", "./testpath", "testname"})
if err := cmd.Execute(); err == nil {
// TODO(lkingland): use a typed error
t.Fatalf("expected error on conflicting flags not received")
}
if d.DescribeInvoked {
t.Fatal("describer was invoked when conflicting flags were provided")
}
}

141
cmd/emit.go Normal file
View File

@ -0,0 +1,141 @@
package cmd
import (
"fmt"
"io/ioutil"
fn "github.com/boson-project/func"
"github.com/boson-project/func/cloudevents"
"github.com/boson-project/func/knative"
"github.com/google/uuid"
"github.com/ory/viper"
"github.com/spf13/cobra"
)
func init() {
e := cloudevents.NewEmitter()
root.AddCommand(emitCmd)
// TODO: do these env vars make sense?
emitCmd.Flags().StringP("sink", "k", "", "Send the CloudEvent to the function running at [sink]. The special value \"local\" can be used to send the event to a function running on the local host. When provided, the --path flag is ignored (Env: $FUNC_SINK)")
emitCmd.Flags().StringP("source", "s", e.Source, "CloudEvent source (Env: $FUNC_SOURCE)")
emitCmd.Flags().StringP("type", "t", e.Type, "CloudEvent type (Env: $FUNC_TYPE)")
emitCmd.Flags().StringP("id", "i", uuid.NewString(), "CloudEvent ID (Env: $FUNC_ID)")
emitCmd.Flags().StringP("data", "d", "", "Any arbitrary string to be sent as the CloudEvent data. Ignored if --file is provided (Env: $FUNC_DATA)")
emitCmd.Flags().StringP("file", "f", "", "Path to a local file containing CloudEvent data to be sent (Env: $FUNC_FILE)")
emitCmd.Flags().StringP("content-type", "c", "application/json", "The MIME Content-Type for the CloudEvent data (Env: $FUNC_CONTENT_TYPE)")
emitCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory. Ignored when --sink is provided (Env: $FUNC_PATH)")
}
var emitCmd = &cobra.Command{
Use: "emit",
Short: "Emit a CloudEvent to a function endpoint",
Long: `Emit event
Emits a CloudEvent, sending it to the deployed function.
`,
Example: `
# Send a CloudEvent to the deployed function with no data and default values
# for source, type and ID
kn func emit
# Send a CloudEvent to the deployed function with the data found in ./test.json
kn func emit --file ./test.json
# Send a CloudEvent to the function running locally with a CloudEvent containing
# "Hello World!" as the data field, with a content type of "text/plain"
kn func emit --data "Hello World!" --content-type "text/plain" -s local
# Send a CloudEvent to the function running locally with an event type of "my.event"
kn func emit --type my.event --sink local
# Send a CloudEvent to the deployed function found at /path/to/fn with an id of "fn.test"
kn func emit --path /path/to/fn -i fn.test
# Send a CloudEvent to an arbitrary endpoint
kn func emit --sink "http://my.event.broker.com"
`,
SuggestFor: []string{"meit", "emti", "send"},
PreRunE: bindEnv("source", "type", "id", "data", "file", "path", "sink", "content-type"),
RunE: runEmit,
}
func runEmit(cmd *cobra.Command, args []string) (err error) {
config := newEmitConfig()
var endpoint string
if config.Sink != "" {
if config.Sink == "local" {
endpoint = "http://localhost:8080"
} else {
endpoint = config.Sink
}
} else {
var f fn.Function
f, err = fn.NewFunction(config.Path)
if err != nil {
return
}
// What happens if the function hasn't been deployed but they don't run with --local=true
// Maybe we should be thinking about saving the endpoint URL in func.yaml after each deploy
var d *knative.Describer
d, err = knative.NewDescriber("")
if err != nil {
return
}
var desc fn.Description
desc, err = d.Describe(cmd.Context(), f.Name)
if err != nil {
return
}
// Use the first available route
endpoint = desc.Routes[0]
}
emitter := cloudevents.NewEmitter()
emitter.Source = config.Source
emitter.Type = config.Type
emitter.Id = config.Id
emitter.ContentType = config.ContentType
emitter.Data = config.Data
if config.File != "" {
var buf []byte
if emitter.Data != "" && config.Verbose {
return fmt.Errorf("Only one of --data and --file may be specified \n")
}
buf, err = ioutil.ReadFile(config.File)
if err != nil {
return
}
emitter.Data = string(buf)
}
client := fn.New(
fn.WithEmitter(emitter),
)
return client.Emit(cmd.Context(), endpoint)
}
type emitConfig struct {
Path string
Source string
Type string
Id string
Data string
File string
ContentType string
Sink string
Verbose bool
}
func newEmitConfig() emitConfig {
return emitConfig{
Path: viper.GetString("path"),
Source: viper.GetString("source"),
Type: viper.GetString("type"),
Id: viper.GetString("id"),
Data: viper.GetString("data"),
File: viper.GetString("file"),
ContentType: viper.GetString("content-type"),
Sink: viper.GetString("sink"),
Verbose: viper.GetBool("verbose"),
}
}

View File

@ -1,222 +0,0 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/ory/viper"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"knative.dev/func/pkg/builders/buildpacks"
"knative.dev/func/pkg/builders/s2i"
"knative.dev/func/pkg/config"
"knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s"
"knative.dev/func/pkg/pipelines/tekton"
)
var format string = "json"
func NewEnvironmentCmd(newClient ClientFactory, version *Version) *cobra.Command {
cmd := &cobra.Command{
Use: "environment",
Short: "Display function execution environment information",
Long: `
NAME
{{rootCmdUse}} environment - display function execution environment information
SYNOPSIS
{{rootCmdUse}} environment [-f|--format] [-v|--verbose] [-p|--path]
DESCRIPTION
Display information about the function execution environment, including
the version of func, the version of the function spec, the default builder,
available runtimes, and available templates.
`,
PreRunE: bindEnv("verbose", "format", "path"),
RunE: func(cmd *cobra.Command, args []string) error {
return runEnvironment(cmd, newClient, version)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
cmd.Flags().StringP("format", "f", format, "Format of output environment information, 'json' or 'yaml'. ($FUNC_FORMAT)")
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
type Environment struct {
Version string
GitRevision string
SpecVersion string
SocatImage string
TarImage string
FuncUtilsImage string
DeployerImage string
ScaffoldImage string
S2IImage string
Languages []string
DefaultImageBuilders map[string]map[string]string
Templates map[string][]string
Environment []string
Cluster string
Defaults config.Global
Function *functions.Function `json:",omitempty" yaml:",omitempty"`
Instance *functions.Instance `json:",omitempty" yaml:",omitempty"`
}
func runEnvironment(cmd *cobra.Command, newClient ClientFactory, v *Version) (err error) {
cfg, err := newEnvironmentConfig()
if err != nil {
return
}
// Create a client to get runtimes and templates
client := functions.New(functions.WithVerbose(cfg.Verbose))
r, err := getRuntimes(client)
if err != nil {
return
}
t, err := getTemplates(client, r)
if err != nil {
return
}
// Get all environment variables that start with FUNC_
var envs []string
for _, e := range os.Environ() {
if strings.HasPrefix(e, "FUNC_") {
envs = append(envs, e)
}
}
// If no environment variables are set, make sure we return an empty array
// otherwise the output is "null" instead of "[]"
if len(envs) == 0 {
envs = make([]string, 0)
}
// Get global defaults
defaults, err := config.NewDefault()
if err != nil {
return
}
// Gets the cluster host
var host string
cc, err := k8s.GetClientConfig().ClientConfig()
if err != nil {
fmt.Printf("error getting client config %v\n", err)
} else {
host = cc.Host
}
//Get default image builders
builderimagesdefault := make(map[string]map[string]string)
builderimagesdefault["s2i"] = s2i.DefaultBuilderImages
builderimagesdefault["buildpacks"] = buildpacks.DefaultBuilderImages
environment := Environment{
Version: v.String(),
GitRevision: v.Hash,
SpecVersion: functions.LastSpecVersion(),
SocatImage: k8s.SocatImage,
TarImage: k8s.TarImage,
FuncUtilsImage: tekton.FuncUtilImage,
DeployerImage: tekton.DeployerImage,
ScaffoldImage: tekton.ScaffoldImage,
S2IImage: tekton.S2IImage,
Languages: r,
DefaultImageBuilders: builderimagesdefault,
Templates: t,
Environment: envs,
Cluster: host,
Defaults: defaults,
}
function, instance := describeFuncInformation(cmd.Context(), newClient, cfg)
if function != nil {
environment.Function = function
}
if instance != nil {
environment.Instance = instance
}
var s []byte
switch cfg.Format {
case "json":
s, err = json.MarshalIndent(environment, "", " ")
case "yaml":
s, err = yaml.Marshal(&environment)
default:
err = fmt.Errorf("unsupported format: %s", cfg.Format)
}
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), string(s))
return nil
}
func getRuntimes(client *functions.Client) ([]string, error) {
runtimes, err := client.Runtimes()
if err != nil {
return nil, err
}
return runtimes, nil
}
func getTemplates(client *functions.Client, runtimes []string) (map[string][]string, error) {
templateMap := make(map[string][]string)
for _, runtime := range runtimes {
templates, err := client.Templates().List(runtime)
if err != nil {
return nil, err
}
templateMap[runtime] = templates
}
return templateMap, nil
}
func describeFuncInformation(context context.Context, newClient ClientFactory, cfg environmentConfig) (*functions.Function, *functions.Instance) {
function, err := functions.NewFunction(cfg.Path)
if err != nil || !function.Initialized() {
return nil, nil
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
instance, err := client.Describe(context, function.Name, function.Deploy.Namespace, function)
if err != nil {
return &function, nil
}
return &function, &instance
}
type environmentConfig struct {
Verbose bool
Format string
Path string
}
func newEnvironmentConfig() (cfg environmentConfig, err error) {
cfg = environmentConfig{
Verbose: viper.GetBool("verbose"),
Format: viper.GetString("format"),
Path: viper.GetString("path"),
}
return
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"io"
"os"
)
type Format string
@ -13,7 +14,6 @@ const (
JSON = "json" // Technically a ⊆ yaml, but no one likes yaml.
XML = "xml"
YAML = "yaml"
URL = "url"
)
// formatter is any structure which has methods for serialization.
@ -23,7 +23,6 @@ type Formatter interface {
JSON(io.Writer) error
XML(io.Writer) error
YAML(io.Writer) error
URL(io.Writer) error
}
// write to the output the output of the formatter's appropriate serilization function.
@ -41,12 +40,11 @@ func write(out io.Writer, s Formatter, formatName string) {
err = s.XML(out)
case YAML:
err = s.YAML(out)
case URL:
err = s.URL(out)
default:
err = fmt.Errorf("format not recognized: %v", formatName)
err = fmt.Errorf("format not recognized: %v\n", formatName)
}
if err != nil {
panic(err)
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}

View File

@ -1,202 +0,0 @@
//go:build exclude_graphdriver_btrfs || !cgo
// +build exclude_graphdriver_btrfs !cgo
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"slices"
"syscall"
"golang.org/x/sys/unix"
"github.com/openshift/source-to-image/pkg/cmd/cli"
"k8s.io/klog/v2"
"knative.dev/func/pkg/builders/s2i"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s"
"knative.dev/func/pkg/knative"
"knative.dev/func/pkg/scaffolding"
"knative.dev/func/pkg/tar"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
cancel()
<-sigs // second sigint/sigterm is treated as sigkill
os.Exit(137)
}()
var cmd = unknown
switch filepath.Base(os.Args[0]) {
case "deploy":
cmd = deploy
case "scaffold":
cmd = scaffold
case "s2i":
cmd = s2iCmd
case "socat":
cmd = socat
case "sh":
cmd = sh
case "s2i-generate":
cmd = s2iGenerate
}
err := cmd(ctx)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(1)
}
}
func unknown(_ context.Context) error {
return fmt.Errorf("unknown command: %q", os.Args[0])
}
func socat(ctx context.Context) error {
cmd := newSocatCmd()
cmd.SetContext(ctx)
return cmd.Execute()
}
func scaffold(ctx context.Context) error {
if len(os.Args) != 2 {
return fmt.Errorf("expected exactly one positional argument (function project path)")
}
path := os.Args[1]
f, err := fn.NewFunction(path)
if err != nil {
return fmt.Errorf("cannot load func project: %w", err)
}
if f.Runtime != "go" && f.Runtime != "python" {
// Scaffolding is for now supported/needed only for Go.
return nil
}
embeddedRepo, err := fn.NewRepository("", "")
if err != nil {
return fmt.Errorf("cannot initialize repository: %w", err)
}
appRoot := filepath.Join(f.Root, ".s2i", "builds", "last")
_ = os.RemoveAll(appRoot)
err = scaffolding.Write(appRoot, f.Root, f.Runtime, f.Invoke, embeddedRepo.FS())
if err != nil {
return fmt.Errorf("cannot write the scaffolding: %w", err)
}
if err := os.MkdirAll(filepath.Join(f.Root, ".s2i", "bin"), 0755); err != nil {
return fmt.Errorf("unable to create .s2i bin dir. %w", err)
}
var asm string
switch f.Runtime {
case "go":
asm = s2i.GoAssembler
case "python":
asm = s2i.PythonAssembler
default:
panic("unreachable")
}
if err := os.WriteFile(filepath.Join(f.Root, ".s2i", "bin", "assemble"), []byte(asm), 0755); err != nil {
return fmt.Errorf("unable to write go assembler. %w", err)
}
return nil
}
func s2iCmd(ctx context.Context) error {
klog.InitFlags(flag.CommandLine)
cmd := cli.CommandFor()
cmd.SetContext(ctx)
return cmd.Execute()
}
func deploy(ctx context.Context) error {
var err error
deployer := knative.NewDeployer(
knative.WithDeployerVerbose(true),
knative.WithDeployerDecorator(deployDecorator{}))
var root string
if len(os.Args) > 1 {
root = os.Args[1]
} else {
root, err = os.Getwd()
if err != nil {
return fmt.Errorf("cannot determine working directory: %w", err)
}
}
f, err := fn.NewFunction(root)
if err != nil {
return fmt.Errorf("cannot load function: %w", err)
}
if len(os.Args) > 2 {
f.Deploy.Image = os.Args[2]
}
if f.Deploy.Image == "" {
f.Deploy.Image = f.Image
}
res, err := deployer.Deploy(ctx, f)
if err != nil {
return fmt.Errorf("cannont deploy the function: %w", err)
}
fmt.Printf("function has been deployed\n%+v\n", res)
return nil
}
type deployDecorator struct {
oshDec k8s.OpenshiftMetadataDecorator
}
func (d deployDecorator) UpdateAnnotations(function fn.Function, annotations map[string]string) map[string]string {
if k8s.IsOpenShift() {
return d.oshDec.UpdateAnnotations(function, annotations)
}
return annotations
}
func (d deployDecorator) UpdateLabels(function fn.Function, labels map[string]string) map[string]string {
if k8s.IsOpenShift() {
return d.oshDec.UpdateLabels(function, labels)
}
return labels
}
func sh(ctx context.Context) error {
if !slices.Equal(os.Args[1:], []string{"-c", "umask 0000 && exec tar -xmf -"}) {
return fmt.Errorf("this is a fake sh (only for backward compatiblility purposes)")
}
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot get working directory: %w", err)
}
unix.Umask(0)
return tar.Extract(os.Stdin, wd)
}

View File

@ -1,143 +0,0 @@
//go:build exclude_graphdriver_btrfs || !cgo
// +build exclude_graphdriver_btrfs !cgo
package main
import (
"context"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/build/strategies"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/spf13/cobra"
fn "knative.dev/func/pkg/functions"
)
func s2iGenerate(ctx context.Context) error {
cmd := newS2IGenerateCmd()
err := cmd.ExecuteContext(ctx)
if err != nil {
return fmt.Errorf("cannot s2i generate: %w", err)
}
return nil
}
type genConfig struct {
target string
pathContext string
builderImage string
registry string
imageScriptUrl string
logLevel string
envVars []string
}
func newS2IGenerateCmd() *cobra.Command {
var config genConfig
genCmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
config.envVars = args
return runS2IGenerate(cmd.Context(), config)
},
}
genCmd.Flags().StringVar(&config.target, "target", "/gen-source", "")
genCmd.Flags().StringVar(&config.pathContext, "path-context", ".", "")
genCmd.Flags().StringVar(&config.builderImage, "builder-image", "", "")
genCmd.Flags().StringVar(&config.registry, "registry", "", "")
genCmd.Flags().StringVar(&config.imageScriptUrl, "image-script-url", "image:///usr/libexec/s2i", "")
genCmd.Flags().StringVar(&config.logLevel, "log-level", "0", "")
return genCmd
}
func runS2IGenerate(ctx context.Context, c genConfig) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot get working directory: %w", err)
}
funcRoot := filepath.Join(wd, c.pathContext)
// replace registry in func.yaml
f, err := fn.NewFunction(funcRoot)
if err != nil {
return fmt.Errorf("cannot load function: %w", err)
}
f.Registry = c.registry
err = f.Write()
if err != nil {
return fmt.Errorf("cannot write function: %w", err)
}
// append node_modules into .s2iignore
s2iIgnorePath := filepath.Join(funcRoot, ".s2iignore")
if fi, _ := os.Stat(s2iIgnorePath); fi != nil {
var file *os.File
file, err = os.OpenFile(s2iIgnorePath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("cannot open s2i ignore file for append: %w", err)
}
defer func(file *os.File) {
_ = file.Close()
}(file)
_, err = file.Write([]byte("\nnode_modules"))
if err != nil {
return fmt.Errorf("cannot append node_modules directory to s2i ignore file: %w", err)
}
}
// prepare envvars
var envs = make([]api.EnvironmentSpec, 0, len(c.envVars))
for _, e := range c.envVars {
var es api.EnvironmentSpec
part := strings.SplitN(e, "=", 2)
switch len(part) {
case 1:
es.Name = part[0]
case 2:
es.Name = part[0]
es.Value = part[1]
default:
continue
}
if es.Name != "" {
envs = append(envs, es)
}
}
s2iConfig := api.Config{
Source: &git.URL{
URL: url.URL{Path: funcRoot},
Type: git.URLTypeLocal,
},
BuilderImage: c.builderImage,
ImageScriptsURL: c.imageScriptUrl,
KeepSymlinks: true,
Environment: envs,
AsDockerfile: filepath.Join(c.target, "Dockerfile.gen"),
}
builder, _, err := strategies.Strategy(nil, &s2iConfig, build.Overrides{})
if err != nil {
return fmt.Errorf("cannot create builder: %w", err)
}
_, err = builder.Build(&s2iConfig)
if err != nil {
return fmt.Errorf("cannot build: %w", err)
}
return nil
}

View File

@ -1,141 +0,0 @@
package main
import (
"fmt"
"io"
"net"
"os"
"strings"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
func newSocatCmd() *cobra.Command {
var (
uniDir bool
dbg string
)
cmd := cobra.Command{
Use: "socat [-u] <address> <address>",
Short: "Minimalistic socat.",
Long: `Minimalistic socat.
Implements only TCP, OPEN and stdio ("-") addresses with no options.
Only supported flag is -u.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
stdio := rwc{
ReadCloser: cmd.InOrStdin().(io.ReadCloser),
WriteCloser: cmd.OutOrStdout().(io.WriteCloser),
}
left, err := createConnection(args[0], stdio)
if err != nil {
return err
}
defer left.Close()
right, err := createConnection(args[1], stdio)
if err != nil {
return err
}
defer right.Close()
return connect(left, right, uniDir)
},
}
cmd.Flags().BoolVarP(&uniDir, "unidirect", "u", false, "unidirectional mode (left to right)")
cmd.Flags().StringVarP(&dbg, "debug", "d", "", "log level (this flag is present only for compatibility and has no effect)")
return &cmd
}
func createConnection(address string, stdio connection) (connection, error) {
if address == "-" {
return stdio, nil
}
parts := strings.SplitN(address, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("cannot parse address: %q", address)
}
typ := strings.ToLower(parts[0])
parts = strings.Split(parts[1], ",")
if len(parts) > 1 {
_, _ = fmt.Fprintf(os.Stderr, "ignored options: %q\n", parts[1])
}
addr := parts[0]
switch typ {
case "tcp", "tcp4", "tcp6":
_, _ = fmt.Fprintln(os.Stderr, "opening connection")
var laddr net.TCPAddr
raddr, err := net.ResolveTCPAddr(typ, addr)
if err != nil {
return nil, fmt.Errorf("name does not resolve: %w", err)
}
conn, err := net.DialTCP(typ, &laddr, raddr)
if err == nil {
_, _ = fmt.Fprintf(os.Stderr, "successfully connected to %v\n", raddr)
}
return conn, err
case "open":
return os.OpenFile(addr, os.O_RDWR, 0644)
default:
return nil, fmt.Errorf("unsupported address: %q", address)
}
}
func connect(left, right connection, uniDir bool) error {
g := errgroup.Group{}
g.SetLimit(2)
if !uniDir {
g.Go(func() error {
_, err := io.Copy(left, right)
tryCloseWriteSide(left)
return err
})
}
g.Go(func() error {
_, err := io.Copy(right, left)
tryCloseWriteSide(right)
return err
})
return g.Wait()
}
type connection interface {
io.Reader
io.Writer
io.Closer
}
type writeCloser interface {
CloseWrite() error
}
type rwc struct {
io.ReadCloser
io.WriteCloser
}
func (r rwc) Close() error {
err := r.WriteCloser.Close()
if err != nil {
return err
}
return r.ReadCloser.Close()
}
func (r rwc) CloseWrite() error {
return r.WriteCloser.Close()
}
func tryCloseWriteSide(c connection) {
if wc, ok := c.(writeCloser); ok {
err := wc.CloseWrite()
if err != nil {
fmt.Fprintf(os.Stderr, "waring: cannot close write side: %+v\n", err)
}
}
}

View File

@ -1,226 +0,0 @@
package main
import (
"bytes"
"errors"
"io"
"net"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRootCmd(t *testing.T) {
/* Begin prepare TCP server and the files */
addr := startTCPEcho(t)
const testData = "file-content\n"
tmpDir := t.TempDir()
inputFile := filepath.Join(tmpDir, "a.txt")
err := os.WriteFile(inputFile, []byte(testData), 0644)
if err != nil {
t.Fatal(err)
}
outputFile := filepath.Join(tmpDir, "b.txt")
err = os.WriteFile(outputFile, []byte{}, 0644)
if err != nil {
t.Fatal(err)
}
/* End prepare TCP server and the files */
type matcher = func(string) bool
contains := func(pattern string) func(string) bool {
return func(s string) bool { return strings.Contains(s, pattern) }
}
equalsTo := func(pattern string) func(string) bool {
return func(s string) bool { return s == pattern }
}
type args struct {
args []string
inputString string
outMatcher matcher
errOutMatcher matcher
outFileMatcher matcher
wantErr bool
}
tests := []struct {
name string
args args
}{
{
name: "stdio<->tcp",
args: args{
args: []string{"-", "TCP:" + addr},
inputString: testData,
outMatcher: equalsTo(testData),
},
},
{
name: "tcp<->stdio",
args: args{
args: []string{"TCP:" + addr, "-"},
inputString: testData,
outMatcher: equalsTo(testData),
},
},
{
name: "tcp-no-such-host",
args: args{
args: []string{"-", "TCP:does.not.exist:10000"},
inputString: "tcp-echo",
errOutMatcher: contains("not resolve"),
wantErr: true,
},
},
{
name: "file->stdio",
args: args{
args: []string{"-u", "OPEN:" + inputFile, "-"},
inputString: "",
outMatcher: equalsTo(testData),
},
},
{
name: "stdio->file",
args: args{
args: []string{"-u", "-", "OPEN:" + outputFile},
inputString: testData,
outFileMatcher: equalsTo(testData),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out, errOut bytes.Buffer
stdout := &testWriter{Writer: &out}
stderr := &testWriter{Writer: &errOut}
cmd := newSocatCmd()
cmd.SetIn(io.NopCloser(strings.NewReader(tt.args.inputString)))
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.SetArgs(tt.args.args)
err = cmd.Execute()
if err != nil && !tt.args.wantErr {
t.Error(err)
t.Logf("errOut: %q", errOut.String())
}
if err == nil && tt.args.wantErr {
t.Error("expected error but got nil")
}
if tt.args.outMatcher != nil && !tt.args.outMatcher(out.String()) {
t.Error("bad standard output")
}
if tt.args.errOutMatcher != nil && !tt.args.errOutMatcher(errOut.String()) {
t.Error("bad standard error output")
}
if tt.args.outFileMatcher != nil {
bs, e := os.ReadFile(outputFile)
if e != nil {
t.Fatal(e)
}
if !tt.args.outFileMatcher(string(bs)) {
t.Error("bad content of the output file")
}
}
})
}
}
type testWriter struct {
io.Writer
}
func (n *testWriter) Close() error {
return nil
}
func startTCPEcho(t *testing.T) (addr string) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
addr = l.Addr().String()
go func() {
for {
conn, err := l.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
panic(err)
}
go func(conn net.Conn) {
defer conn.Close()
_, err = io.Copy(conn, conn)
if err != nil {
panic(err)
}
}(conn)
}
}()
t.Cleanup(func() {
l.Close()
})
return addr
}
func TestNewRootCmdWithPipe(t *testing.T) {
addr := startTCPEcho(t)
r, stdOut, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
stdIn, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
var data = []byte("testing data")
go func() {
var err error
_, err = w.Write(data)
if err != nil {
t.Error(err)
}
err = w.Close()
if err != nil {
t.Error(err)
}
}()
go func() {
var err error
var errBuff bytes.Buffer
cmd := newSocatCmd()
cmd.SetIn(stdIn)
cmd.SetOut(stdOut)
cmd.SetErr(&errBuff)
cmd.SetArgs([]string{"-dd", "-", "TCP:" + addr})
err = cmd.Execute()
if err != nil {
t.Error(err)
}
}()
bs, e := io.ReadAll(r)
if e != nil {
t.Error(e)
}
t.Log(string(data))
if !bytes.Equal(data, bs) {
t.Errorf("bad data: %q", string(bs))
}
}

View File

@ -1,7 +1,32 @@
package main
import "knative.dev/func/pkg/app"
import (
"context"
"github.com/boson-project/func/cmd"
"os"
"os/signal"
"syscall"
)
// Statically-populated build metadata set
// by `make build`.
var date, vers, hash string
func main() {
app.Main()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
cancel()
// second sigint/sigterm is treated as sigkill
<-sigs
os.Exit(137)
}()
cmd.SetMeta(date, vers, hash)
cmd.Execute(ctx)
}

View File

@ -1,421 +0,0 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/utils"
)
func NewInvokeCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "invoke",
Short: "Invoke a local or remote function",
Long: `
NAME
{{rootCmdUse}} invoke - test a function by invoking it with test data
SYNOPSIS
{{rootCmdUse}} invoke [-t|--target] [-f|--format]
[--id] [--source] [--type] [--data] [--file] [--content-type]
[-s|--save] [-p|--path] [-i|--insecure] [-c|--confirm] [-v|--verbose]
DESCRIPTION
Invokes the function by sending a test request to the currently running
function instance, either locally or remote. If the function is running
both locally and remote, the local instance will be invoked. This behavior
can be manually overridden using the --target flag.
Functions are invoked with a test data structure consisting of five values:
id: A unique identifier for the request.
source: A sender name for the request (sender).
type: A type for the request.
data: Data (content) for this request.
content-type: The MIME type of the value contained in 'data'.
The values of these parameters can be individually altered from their defaults
using their associated flags. Data can also be provided from a file using the
--file flag.
Invocation Target
The function instance to invoke can be specified using the --target flag
which accepts the values "local", "remote", or <URL>. By default the
local function instance is chosen if running (see {{rootCmdUse}} run).
To explicitly target the remote (deployed) function:
{{rootCmdUse}} invoke --target=remote
To target an arbitrary endpoint, provide a URL:
{{rootCmdUse}} invoke --target=https://myfunction.example.com
Invocation Data
Providing a filename in the --file flag will base64 encode its contents
as the "data" parameter sent to the function. The value of --content-type
should be set to the type from the source file. For example, the following
would send a JPEG base64 encoded in the "data" POST parameter:
{{rootCmdUse}} invoke --file=example.jpeg --content-type=image/jpeg
Message Format
By default functions are sent messages which match the invocation format
of the template they were created using; for example "http" or "cloudevent".
To override this behavior, use the --format (-f) flag.
{{rootCmdUse}} invoke -f=cloudevent -t=http://my-sink.my-cluster
EXAMPLES
o Invoke the default (local or remote) running function with default values
$ {{rootCmdUse}} invoke
o Run the function locally and then invoke it with a test request:
(run in two terminals or by running the first in the background)
$ {{rootCmdUse}} run
$ {{rootCmdUse}} invoke
o Deploy and then invoke the remote function:
$ {{rootCmdUse}} deploy
$ {{rootCmdUse}} invoke
o Invoke a remote (deployed) function when it is already running locally:
(overrides the default behavior of preferring locally running instances)
$ {{rootCmdUse}} invoke --target=remote
o Specify the data to send to the function as a flag
$ {{rootCmdUse}} invoke --data="Hello World!"
o Send a JPEG to the function
$ {{rootCmdUse}} invoke --file=example.jpeg --content-type=image/jpeg
o Invoke an arbitrary endpoint (HTTP POST)
$ {{rootCmdUse}} invoke --target="https://my-http-handler.example.com"
o Invoke an arbitrary endpoint (CloudEvent)
$ {{rootCmdUse}} invoke -f=cloudevent -t="https://my-event-broker.example.com"
o Allow insecure server connections when using SSL
$ {{rootCmdUse}} invoke --insecure
o In case you need to specifically send GET request
$ {{rootCmdUse}} invoke --request-type=GET
`,
SuggestFor: []string{"emit", "emti", "send", "emit", "exec", "nivoke",
"onvoke", "unvoke", "knvoke", "imvoke", "ihvoke", "ibvoke"},
PreRunE: bindEnv("path", "format", "target", "id", "source", "type",
"data", "content-type", "request-type", "file", "insecure",
"confirm", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runInvoke(cmd, args, newClient)
},
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Flags
cmd.Flags().StringP("format", "f", "", "Format of message to send, 'http' or 'cloudevent'. Default is to choose automatically. ($FUNC_FORMAT)")
cmd.Flags().StringP("target", "t", "", "Function instance to invoke. Can be 'local', 'remote' or a URL. Defaults to auto-discovery if not provided. ($FUNC_TARGET)")
cmd.Flags().StringP("id", "", "", "ID for the request data. ($FUNC_ID)")
cmd.Flags().StringP("source", "", fn.DefaultInvokeSource, "Source value for the request data. ($FUNC_SOURCE)")
cmd.Flags().StringP("type", "", fn.DefaultInvokeType, "Type value for the request data. ($FUNC_TYPE)")
cmd.Flags().StringP("content-type", "", fn.DefaultInvokeContentType, "Content Type of the data. ($FUNC_CONTENT_TYPE)")
cmd.Flags().StringP("request-type", "", fn.DefaultInvokeRequestType, "Type of request to use. Can be POST or GET. ($FUNC_REQUEST_TYPE)")
cmd.Flags().StringP("data", "", fn.DefaultInvokeData, "Data to send in the request. ($FUNC_DATA)")
cmd.Flags().StringP("file", "", "", "Path to a file to use as data. Overrides --data flag and should be sent with a correct --content-type. ($FUNC_FILE)")
cmd.Flags().BoolP("insecure", "i", false, "Allow insecure server connections when using SSL. ($FUNC_INSECURE)")
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
// Run
func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) {
// Gather flag values for the invocation
cfg, err := newInvokeConfig()
if err != nil {
return
}
// Load the function
f, err := fn.NewFunction(cfg.Path)
if err != nil {
return
}
if err = f.Validate(); err != nil {
fmt.Printf("error validating function at '%v'. %v\n", f.Root, err)
return err
}
if !f.Initialized() {
return fn.NewErrNotInitialized(f.Root)
}
// Client instance from env vars, flags, args and user prompts (if --confirm)
client, done := newClient(ClientConfig{Verbose: cfg.Verbose, InsecureSkipVerify: cfg.Insecure})
defer done()
// Message to send the running function built from parameters gathered
// from the user (or defaults)
m := fn.InvokeMessage{
ID: cfg.ID,
Source: cfg.Source,
Type: cfg.Type,
ContentType: cfg.ContentType,
RequestType: strings.ToUpper(cfg.RequestType),
Data: cfg.Data,
Format: cfg.Format,
}
// If --file was specified, use its content for message data
if cfg.File != "" {
content, err := os.ReadFile(cfg.File)
if err != nil {
return err
}
m.Data = content
}
// Invoke
metadata, body, err := client.Invoke(cmd.Context(), cfg.Path, cfg.Target, m)
if err != nil {
return err
}
// When Verbose
// - Print an explicit "Received response" indicator
// - Print metadata (headers for HTTP requests, CloudEvents already include
// metadata in their data value.
if cfg.Verbose {
// Print a "Received response" message because a simple echo to
// stdout could be confusing on a first-time run, viewing a proper echo.
// user feedback suggests this actually be placed behind the --verbose
// setting:
fmt.Println("Function invoked. Response:")
if len(metadata) > 0 {
fmt.Println(" Metadata:")
}
for k, vv := range metadata {
values := strings.Join(vv, ";")
fmt.Fprintf(cmd.OutOrStdout(), " %v: %v\n", k, values)
}
if len(metadata) > 0 {
fmt.Println(" Content:")
}
}
// Always print the response's default stringification
// Note body already includes a linebreak.
fmt.Fprint(cmd.OutOrStdout(), body)
return
}
type invokeConfig struct {
Path string
Target string
Format string
ID string
Source string
Type string
Data []byte
ContentType string
RequestType string
File string
Confirm bool
Verbose bool
Insecure bool
}
func newInvokeConfig() (cfg invokeConfig, err error) {
cfg = invokeConfig{
Path: viper.GetString("path"),
Target: viper.GetString("target"),
Format: viper.GetString("format"),
ID: viper.GetString("id"),
Source: viper.GetString("source"),
Type: viper.GetString("type"),
Data: []byte(viper.GetString("data")),
ContentType: viper.GetString("content-type"),
RequestType: viper.GetString("request-type"),
File: viper.GetString("file"),
Confirm: viper.GetBool("confirm"),
Verbose: viper.GetBool("verbose"),
Insecure: viper.GetBool("insecure"),
}
// If file was passed, read it in as data
if cfg.File != "" {
b, err := os.ReadFile(cfg.File)
if err != nil {
return cfg, err
}
cfg.Data = b
}
// if not in confirm/prompting mode, the cfg structure is complete.
if !cfg.Confirm {
return
}
// If in interactive terminal mode, prompt to modify defaults.
if interactiveTerminal() {
return cfg.prompt()
}
// Confirming, but noninteractive, is essentially a selective verbose mode
// which prints out the effective values of config as a confirmation.
fmt.Printf("Path: %v\n", cfg.Path)
fmt.Printf("Target: %v\n", cfg.Target)
fmt.Printf("ID: %v\n", cfg.ID)
fmt.Printf("Source: %v\n", cfg.Source)
fmt.Printf("Type: %v\n", cfg.Type)
fmt.Printf("Data: %v\n", cfg.Data)
fmt.Printf("Content Type: %v\n", cfg.ContentType)
fmt.Printf("File: %v\n", cfg.File)
fmt.Printf("Insecure: %v\n", cfg.Insecure)
return
}
func (c invokeConfig) prompt() (invokeConfig, error) {
var qs []*survey.Question
// First get path to effective function
qs = []*survey.Question{
{
Name: "Path",
Prompt: &survey.Input{
Message: "Function Path:",
Default: c.Path,
},
Validate: func(val interface{}) error {
if val.(string) != "" {
derivedName, _ := deriveNameAndAbsolutePathFromPath(val.(string))
return utils.ValidateFunctionName(derivedName)
}
return nil
},
Transform: func(ans interface{}) interface{} {
if ans.(string) != "" {
_, absolutePath := deriveNameAndAbsolutePathFromPath(ans.(string))
return absolutePath
}
return ""
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
formatOptions := []string{"", "http", "cloudevent"}
qs = []*survey.Question{
{
Name: "Target",
Prompt: &survey.Input{
Message: "(Optional) Target ('local', 'remote' or URL). If not provided, local will be preferred over remote.",
Default: "",
},
},
{
Name: "Format",
Prompt: &survey.Select{
Message: "(Optional) Format Override",
Options: formatOptions,
Default: surveySelectDefault(c.Format, formatOptions),
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
// Prompt for the next set of values, with defaults set first by the function
// as it exists on disk, followed by environment variables, and finally flags.
// user interactive prompts therefore are the last applied, and thus highest
// precedence values.
qs = []*survey.Question{
{
Name: "ID",
Prompt: &survey.Input{
Message: "Data ID",
Default: c.ID,
},
}, {
Name: "Source",
Prompt: &survey.Input{
Message: "Data Source",
Default: c.Source,
},
}, {
Name: "Type",
Prompt: &survey.Input{
Message: "Data Type",
Default: c.Type,
},
}, {
Name: "File",
Prompt: &survey.Input{
Message: "(Optional) Load Data Content from File",
Default: c.File,
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
// If the user did not specify a file for data content, prompt for it
if c.File == "" {
qs = []*survey.Question{
{
Name: "Data",
Prompt: &survey.Input{
Message: "Data Content",
Default: string(c.Data),
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
}
// Finally, allow mutation of the data content type.
contentTypeMessage := "Content type of data"
if c.File != "" {
contentTypeMessage = "Content type of file"
}
qs = []*survey.Question{
{
Name: "ContentType",
Prompt: &survey.Input{
Message: contentTypeMessage,
Default: c.ContentType,
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
qs = []*survey.Question{
{
Name: "Insecure",
Prompt: &survey.Confirm{
Message: "Allow insecure server connections when using SSL",
Default: c.Insecure,
},
},
}
if err := survey.Ask(qs, &c); err != nil {
return c, err
}
return c, nil
}

View File

@ -1,80 +0,0 @@
package cmd
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"sync/atomic"
"testing"
"time"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)
// TestInvoke command executes the invocation path.
func TestInvoke(t *testing.T) {
root := FromTempDirectory(t)
var invoked int32
// Create a test function to be invoked
if _, err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil {
t.Fatal(err)
}
// Mock Runner
// Starts a service which sets invoked=1 on any request
runner := mock.NewRunner()
runner.RunFn = func(ctx context.Context, f fn.Function, _ string, _ time.Duration) (job *fn.Job, err error) {
var (
l net.Listener
h = http.NewServeMux()
s = http.Server{Handler: h}
)
if l, err = net.Listen("tcp4", "127.0.0.1:"); err != nil {
t.Fatal(err)
}
h.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
atomic.StoreInt32(&invoked, 1)
_, _ = res.Write([]byte("invoked"))
})
go func() {
if err = s.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
fmt.Fprintf(os.Stderr, "error serving: %v", err)
}
}()
host, port, _ := net.SplitHostPort(l.Addr().String())
errs := make(chan error, 10)
stop := func() error { _ = s.Close(); return nil }
return fn.NewJob(f, host, port, errs, stop, false)
}
// Run the mock http service function interloper
f, err := fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
client := fn.New(fn.WithRunner(runner))
job, err := client.Run(context.Background(), f)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = job.Stop() })
// Test that the invoke command invokes
cmd := NewInvokeCmd(NewClient)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if atomic.LoadInt32(&invoked) != 1 {
t.Fatal("function was not invoked")
}
}

View File

@ -1,117 +0,0 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
)
func NewLanguagesCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "languages",
Short: "List available function language runtimes",
Long: `
NAME
{{rootCmdUse}} languages - list available language runtimes.
SYNOPSIS
{{rootCmdUse}} languages [--json] [-r|--repository]
DESCRIPTION
List the language runtimes that are currently available.
This includes embedded (included) language runtimes as well as any installed
via the 'repositories add' command.
To specify a URI of a single, specific repository for which languages
should be displayed, use the --repository flag.
Installed repositories are by default located at ~/.func/repositories
($XDG_CONFIG_HOME/.func/repositories). This can be overridden with
$FUNC_REPOSITORIES_PATH.
To see templates available for a given language, see the 'templates' command.
EXAMPLES
o Show a list of all available language runtimes
$ {{rootCmdUse}} languages
o Return a list of all language runtimes in JSON
$ {{rootCmdUse}} languages --json
o Return language runtimes in a specific repository
$ {{rootCmdUse}} languages --repository=https://github.com/boson-project/templates
`,
SuggestFor: []string{"language", "runtime", "runtimes", "lnaguages", "languagse",
"panguages", "manguages", "kanguages", "lsnguages", "lznguages"},
PreRunE: bindEnv("json", "repository", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runLanguages(cmd, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
cmd.Flags().BoolP("json", "", false, "Set output to JSON format. ($FUNC_JSON)")
cmd.Flags().StringP("repository", "r", "", "URI to a specific repository to consider ($FUNC_REPOSITORY)")
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
func runLanguages(cmd *cobra.Command, newClient ClientFactory) (err error) {
cfg, err := newLanguagesConfig()
if err != nil {
return
}
client, done := newClient(
ClientConfig{Verbose: cfg.Verbose},
fn.WithRepository(cfg.Repository))
defer done()
runtimes, err := client.Runtimes()
if err != nil {
return
}
if cfg.JSON {
var s []byte
s, err = json.MarshalIndent(runtimes, "", " ")
if err != nil {
return
}
fmt.Fprintln(cmd.OutOrStdout(), string(s))
} else {
for _, runtime := range runtimes {
fmt.Fprintln(cmd.OutOrStdout(), runtime)
}
}
return
}
type languagesConfig struct {
Verbose bool
Repository string // Consider only a specific repository (URI)
JSON bool // output as JSON
}
func newLanguagesConfig() (cfg languagesConfig, err error) {
cfg = languagesConfig{
Verbose: viper.GetBool("verbose"),
Repository: viper.GetString("repository"),
JSON: viper.GetBool("json"),
}
return
}

View File

@ -1,60 +0,0 @@
package cmd
import (
"testing"
. "knative.dev/func/pkg/testing"
)
// TestLanguages_Default ensures that the default behavior of listing
// all supported languages is to print a plain text list of all the builtin
// language runtimes.
func TestLanguages_Default(t *testing.T) {
_ = FromTempDirectory(t)
buf := piped(t) // gather output
cmd := NewLanguagesCmd(NewClient)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
expected := `go
node
python
quarkus
rust
springboot
typescript`
output := buf()
if output != expected {
t.Fatalf("expected:\n'%v'\ngot:\n'%v'\n", expected, output)
}
}
// TestLanguages_JSON ensures that listing languages in --json format returns
// builtin languages as a JSON array.
func TestLanguages_JSON(t *testing.T) {
_ = FromTempDirectory(t)
buf := piped(t) // gather output
cmd := NewLanguagesCmd(NewClient)
cmd.SetArgs([]string{"--json"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
expected := `[
"go",
"node",
"python",
"quarkus",
"rust",
"springboot",
"typescript"
]`
output := buf()
if output != expected {
t.Fatalf("expected:\n%v\ngot:\n%v\n", expected, output)
}
}

View File

@ -3,7 +3,6 @@ package cmd
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"os"
@ -13,96 +12,75 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/knative"
)
func NewListCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List deployed functions",
Long: `List deployed functions
func init() {
root.AddCommand(listCmd)
listCmd.Flags().BoolP("all-namespaces", "A", false, "List functions in all namespaces. If set, the --namespace flag is ignored.")
listCmd.Flags().StringP("namespace", "n", "", "Namespace to search for functions. By default, the functions of the actual active namespace are listed. (Env: $FUNC_NAMESPACE)")
listCmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml) (Env: $FUNC_OUTPUT)")
err := listCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
if err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
}
Lists deployed functions.
var listCmd = &cobra.Command{
Use: "list",
Short: "List functions",
Long: `List functions
Lists all deployed functions in a given namespace.
`,
Example: `
# List all functions in the current namespace with human readable output
{{rootCmdUse}} list
kn func list
# List all functions in the 'test' namespace with yaml output
{{rootCmdUse}} list --namespace test --output yaml
kn func list --namespace test --output yaml
# List all functions in all namespaces with JSON output
{{rootCmdUse}} list --all-namespaces --output json
kn func list --all-namespaces --output json
`,
SuggestFor: []string{"lsit"},
Aliases: []string{"ls"},
PreRunE: bindEnv("all-namespaces", "output", "namespace", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd, args, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Namespace Config
// Differing from other commands, the default namespace for the list
// command is set to the currently active namespace as returned by
// calling k8s.DefaultNamespace(). This way a call to `func list` will
// show functions in the currently active namespace. If the value can
// not be determined due to error, a warning is printed to log and
// no namespace is passed to the lister, which should result in the
// lister showing functions for all namespaces.
//
// This also extends to the treatment of the global setting for
// namespace. This is likewise intended for command which require a
// namespace no matter what. Therefore the global namespace setting is
// not applicable to this command because "default" really means "all".
//
// This is slightly different than other commands wherein their
// default is often to presume namespace "default" if none was either
// supplied nor available.
// Flags
cmd.Flags().BoolP("all-namespaces", "A", false, "List functions in all namespaces. If set, the --namespace flag is ignored.")
cmd.Flags().StringP("namespace", "n", defaultNamespace(fn.Function{}, false), "The namespace for which to list functions. ($FUNC_NAMESPACE)")
cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml) ($FUNC_OUTPUT)")
addVerboseFlag(cmd, cfg.Verbose)
if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
return cmd
SuggestFor: []string{"ls", "lsit"},
PreRunE: bindEnv("namespace", "output"),
RunE: runList,
}
func runList(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) {
cfg, err := newListConfig(cmd)
func runList(cmd *cobra.Command, args []string) (err error) {
config := newListConfig()
lister, err := knative.NewLister(config.Namespace)
if err != nil {
return err
return
}
lister.Verbose = config.Verbose
a, err := cmd.Flags().GetBool("all-namespaces")
if err != nil {
return
}
if a {
lister.Namespace = ""
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
client := bosonFunc.New(
bosonFunc.WithVerbose(config.Verbose),
bosonFunc.WithLister(lister))
items, err := client.List(cmd.Context(), cfg.Namespace)
items, err := client.List(cmd.Context())
if err != nil {
return
}
if len(items) == 0 {
if cfg.Namespace != "" {
fmt.Printf("no functions found in namespace '%v'\n", cfg.Namespace)
} else {
fmt.Println("no functions found")
}
if len(items) < 1 {
fmt.Printf("No functions found in %v namespace\n", lister.Namespace)
return
}
write(os.Stdout, listItems(items), cfg.Output)
write(os.Stdout, listItems(items), config.Output)
return
}
@ -116,30 +94,18 @@ type listConfig struct {
Verbose bool
}
func newListConfig(cmd *cobra.Command) (cfg listConfig, err error) {
cfg = listConfig{
func newListConfig() listConfig {
return listConfig{
Namespace: viper.GetString("namespace"),
Output: viper.GetString("output"),
Verbose: viper.GetBool("verbose"),
}
// If --all-namespaces, zero out any value for namespace (such as)
// "all" to the lister.
if viper.GetBool("all-namespaces") {
cfg.Namespace = ""
}
// specifying both -A and --namespace is logically inconsistent
if cmd.Flags().Changed("namespace") && viper.GetBool("all-namespaces") {
err = errors.New("both --namespace and --all-namespaces specified")
}
return
}
// Output Formatting (serializers)
// -------------------------------
type listItems []fn.ListItem
type listItems []bosonFunc.ListItem
func (items listItems) Human(w io.Writer) error {
return items.Plain(w)
@ -169,10 +135,3 @@ func (items listItems) XML(w io.Writer) error {
func (items listItems) YAML(w io.Writer) error {
return yaml.NewEncoder(w).Encode(items)
}
func (items listItems) URL(w io.Writer) error {
for _, item := range items {
fmt.Fprintf(w, "%s\n", item.URL)
}
return nil
}

View File

@ -1,109 +0,0 @@
package cmd
import (
"context"
"testing"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)
// TestList_Namespace ensures that list command handles namespace options
// namespace (--namespace) and all namespaces (--all-namespaces) correctly
// and that the current kube context is used by default.
func TestList_Namespace(t *testing.T) {
_ = FromTempDirectory(t)
tests := []struct {
name string
namespace string // --namespace flag (use specific namespace)
all bool // --all-namespaces (no namespace filter)
allShort bool // -A (no namespace filter)
expected string // expected value passed to lister
err bool // expected error
}{
{
name: "default (none specififed)",
namespace: "",
all: false,
allShort: false,
expected: "func", // see testdata kubeconfig
},
{
name: "namespace provided",
namespace: "ns",
all: false,
allShort: false,
expected: "ns",
},
{
name: "all namespaces",
namespace: "",
all: true,
allShort: false,
expected: "", // --all-namespaces | -A explicitly mean none specified
},
{
name: "all namespaces - short flag",
all: false,
allShort: true,
expected: "", // blank is implemented by lister as meaning all
},
{
name: "both flags error",
namespace: "ns",
all: true,
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create a mock lister implementation which validates the expected
// value has been passed.
lister := mock.NewLister()
lister.ListFn = func(_ context.Context, namespace string) ([]fn.ListItem, error) {
if namespace != test.expected {
t.Fatalf("expected list namespace %q, got %q", test.expected, namespace)
}
return []fn.ListItem{}, nil
}
// Create an instance of the command which sets the flags
// according to the test case
cmd := NewListCmd(NewTestClient(fn.WithLister(lister)))
args := []string{}
if test.namespace != "" {
args = append(args, "--namespace", test.namespace)
}
if test.all {
args = append(args, "--all-namespaces")
}
if test.allShort {
args = append(args, "-A")
}
cmd.SetArgs(args)
// Execute
err := cmd.Execute()
// Check for expected error
if err != nil {
if !test.err {
t.Fatalf("unexpected error: %v", err)
}
// expected error received
return
} else if test.err {
t.Fatalf("did not receive expected error ")
}
// For tests which did not expect an error, ensure the lister
// was invoked
if !lister.ListInvoked {
t.Fatalf("%v: the lister was not invoked", test.name)
}
})
}
}

View File

@ -1,46 +0,0 @@
package cmd
import (
"log"
"github.com/spf13/cobra"
"knative.dev/func/pkg/mcp"
)
func NewMCPServerCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "mcp",
Short: "Start MCP server",
Long: `
NAME
{{rootCmdUse}} mcp - start a Model Context Protocol (MCP) server
SYNOPSIS
{{rootCmdUse}} mcp [flags]
DESCRIPTION
Starts a Model Context Protocol (MCP) server over standard input/output (stdio) transport.
This implementation aims to support tools for deploying and creating serverless functions.
Note: This command is still under development.
EXAMPLES
o Run an MCP server:
{{rootCmdUse}} mcp
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runMCPServer(cmd, args)
},
}
return cmd
}
func runMCPServer(cmd *cobra.Command, args []string) error {
s := mcp.NewServer()
if err := s.Start(); err != nil {
log.Fatalf("Server error: %v", err)
return err
}
return nil
}

View File

@ -1,133 +0,0 @@
package prompt
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"golang.org/x/term"
"knative.dev/func/pkg/docker"
"knative.dev/func/pkg/docker/creds"
)
func NewPromptForCredentials(in io.Reader, out, errOut io.Writer) func(repository string) (docker.Credentials, error) {
firstTime := true
return func(repository string) (docker.Credentials, error) {
var result docker.Credentials
if firstTime {
firstTime = false
fmt.Fprintf(out, "Please provide credentials for image repository '%s'.\n", repository)
} else {
fmt.Fprintf(out, "Incorrect credentials for repository '%s'. Please make sure the repository is correct and try again.\n", repository)
}
var qs = []*survey.Question{
{
Name: "username",
Prompt: &survey.Input{
Message: "Username:",
},
Validate: survey.Required,
},
{
Name: "password",
Prompt: &survey.Password{
Message: "Password:",
},
Validate: survey.Required,
},
}
var (
fr terminal.FileReader
ok bool
)
isTerm := false
if fr, ok = in.(terminal.FileReader); ok {
isTerm = term.IsTerminal(int(fr.Fd()))
}
if isTerm {
err := survey.Ask(qs, &result, survey.WithStdio(fr, out.(terminal.FileWriter), errOut))
if err != nil {
return docker.Credentials{}, err
}
} else {
reader := bufio.NewReader(in)
fmt.Fprintf(out, "Username: ")
u, err := reader.ReadString('\n')
if err != nil {
return docker.Credentials{}, err
}
u = strings.Trim(u, "\r\n")
fmt.Fprintf(out, "Password: ")
p, err := reader.ReadString('\n')
if err != nil {
return docker.Credentials{}, err
}
p = strings.Trim(p, "\r\n")
result = docker.Credentials{Username: u, Password: p}
}
return result, nil
}
}
func NewPromptForCredentialStore() creds.ChooseCredentialHelperCallback {
return func(availableHelpers []string) (string, error) {
if len(availableHelpers) < 1 {
fmt.Fprintf(os.Stderr, `Credentials will not be saved.
If you would like to save your credentials in the future,
you can install docker credential helper https://github.com/docker/docker-credential-helpers.
`)
return "", nil
}
isTerm := term.IsTerminal(int(os.Stdin.Fd()))
var resp string
if isTerm {
err := survey.AskOne(&survey.Select{
Message: "Choose credentials helper",
Options: append(availableHelpers, "None"),
}, &resp, survey.WithValidator(survey.Required))
if err != nil {
return "", err
}
if resp == "None" {
fmt.Fprintf(os.Stderr, "No helper selected. Credentials will not be saved.\n")
return "", nil
}
} else {
fmt.Fprintf(os.Stderr, "Available credential helpers:\n")
for _, helper := range availableHelpers {
fmt.Fprintf(os.Stderr, "%s\n", helper)
}
fmt.Fprintf(os.Stderr, "Choose credentials helper: ")
reader := bufio.NewReader(os.Stdin)
var err error
resp, err = reader.ReadString('\n')
if err != nil {
return "", err
}
resp = strings.Trim(resp, "\r\n")
if resp == "" {
fmt.Fprintf(os.Stderr, "No helper selected. Credentials will not be saved.\n")
}
}
return resp, nil
}
}

View File

@ -1,83 +0,0 @@
//go:build linux
// +build linux
package prompt
import (
"io"
"strings"
"testing"
"time"
"github.com/Netflix/go-expect"
"github.com/creack/pty"
"github.com/hinshun/vt10x"
"knative.dev/func/pkg/docker"
)
const (
enter = "\r"
)
func Test_NewPromptForCredentials(t *testing.T) {
expectedCreds := docker.Credentials{
Username: "testuser",
Password: "testpwd",
}
ptm, pts, err := pty.Open()
if err != nil {
t.Fatal(err)
}
term := vt10x.New(vt10x.WithWriter(pts))
console, err := expect.NewConsole(expect.WithStdin(ptm), expect.WithStdout(term), expect.WithCloser(ptm, pts))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { console.Close() })
go func() {
_, _ = console.ExpectEOF()
}()
go func() {
chars := expectedCreds.Username + enter + expectedCreds.Password + enter
for _, ch := range chars {
time.Sleep(time.Millisecond * 100)
_, _ = console.Send(string(ch))
}
}()
tests := []struct {
name string
in io.Reader
out io.Writer
errOut io.Writer
}{
{
name: "with non-tty",
in: strings.NewReader(expectedCreds.Username + "\r\n" + expectedCreds.Password + "\r\n"),
out: io.Discard,
errOut: io.Discard,
},
{
name: "with tty",
in: console.Tty(),
out: console.Tty(),
errOut: console.Tty(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
credPrompt := NewPromptForCredentials(tt.in, tt.out, tt.errOut)
cred, err := credPrompt("example.com")
if err != nil {
t.Fatal(err)
}
if cred != expectedCreds {
t.Errorf("bad credentials: %+v", cred)
}
})
}
}

View File

@ -1,664 +0,0 @@
package cmd
import (
"errors"
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
)
// command constructors
// --------------------
func NewRepositoryCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Short: "Manage installed template repositories",
Use: "repository",
Aliases: []string{"repo", "repositories"},
Long: `
NAME
{{rootCmdUse}} - Manage set of installed repositories.
SYNOPSIS
{{rootCmdUse}} repo [-c|--confirm] [-v|--verbose]
{{rootCmdUse}} repo list [-r|--repositories] [-c|--confirm] [-v|--verbose]
{{rootCmdUse}} repo add <name> <url>[-r|--repositories] [-c|--confirm] [-v|--verbose]
{{rootCmdUse}} repo rename <old> <new> [-r|--repositories] [-c|--confirm] [-v|--verbose]
{{rootCmdUse}} repo remove <name> [-r|--repositories] [-c|--confirm] [-v|--verbose]
DESCRIPTION
Manage template repositories installed on disk at either the default location
(~/.config/func/repositories) or the location specified by the --repository
flag. Once added, a template from the repository can be used when creating
a new function.
Interactive Prompts:
To complete these commands interactively, pass the --confirm (-c) flag to
the 'repository' command, or any of the inidivual subcommands.
The Default Repository:
The default repository is not stored on disk, but embedded in the binary and
can be used without explicitly specifying the name. The default repository
is always listed first, and is assumed when creating a new function without
specifying a repository name prefix.
For example, to create a new Go function using the 'http' template from the
default repository.
$ {{rootCmdUse}} create -l go -t http
The Repository Flag:
Installing repositories locally is optional. To use a template from a remote
repository directly, it is possible to use the --repository flag on create.
This leaves the local disk untouched. For example, To create a function using
the Boson Project Hello-World template without installing the template
repository locally, use the --repository (-r) flag on create:
$ {{rootCmdUse}} create -l go \
--template hello-world \
--repository https://github.com/boson-project/templates
Alternative Repositories Location:
Repositories are stored on disk in ~/.config/func/repositories by default.
This location can be altered by setting the FUNC_REPOSITORIES_PATH
environment variable.
COMMANDS
With no arguments, this help text is shown. To manage repositories with
an interactive prompt, use the use the --confirm (-c) flag.
$ {{rootCmdUse}} repository -c
add
Add a new repository to the installed set.
$ {{rootCmdUse}} repository add <name> <URL>
For Example, to add the Boson Project repository:
$ {{rootCmdUse}} repository add boson https://github.com/boson-project/templates
Once added, a function can be created with templates from the new repository
by prefixing the template name with the repository. For example, to create
a new function using the Go Hello World template:
$ {{rootCmdUse}} create -l go -t boson/hello-world
list
List all available repositories, including the installed default
repository. Repositories available are listed by name. To see the URL
which was used to install remotes, use --verbose (-v).
rename
Rename a previously installed repository from <old> to <new>. Only installed
repositories can be renamed.
$ {{rootCmdUse}} repository rename <name> <new name>
remove
Remove a repository by name. Removes the repository from local storage
entirely. When in confirm mode (--confirm) it will confirm before
deletion, but in regular mode this is done immediately, so please use
caution, especially when using an altered repositories location
(via the FUNC_REPOSITORIES_PATH environment variable).
$ {{rootCmdUse}} repository remove <name>
EXAMPLES
o Run in confirmation mode (interactive prompts) using the --confirm flag
$ {{rootCmdUse}} repository -c
o Add a repository and create a new function using a template from it:
$ {{rootCmdUse}} repository add functastic https://github.com/knative-extensions/func-tastic
$ {{rootCmdUse}} repository list
default
functastic
$ {{rootCmdUse}} create -l go -t functastic/hello-world
...
o Add a repository specifying the branch to use (metacontroller):
$ {{rootCmdUse}} repository add metacontroller https://github.com/knative-extensions/func-tastic#metacontroler
$ {{rootCmdUse}} repository list
default
metacontroller
$ {{rootCmdUse}} create -l node -t metacontroller/metacontroller
...
o List all repositories including the URL from which remotes were installed
$ {{rootCmdUse}} repository list -v
default
metacontroller https://github.com/knative-extensions/func-tastic#metacontroller
o Rename an installed repository
$ {{rootCmdUse}} repository list
default
boson
$ {{rootCmdUse}} repository rename boson functastic
$ {{rootCmdUse}} repository list
default
functastic
o Remove an installed repository
$ {{rootCmdUse}} repository list
default
functastic
$ {{rootCmdUse}} repository remove functastic
$ {{rootCmdUse}} repository list
default
`,
SuggestFor: []string{"repositories", "repos", "template", "templates", "pack", "packs"},
PreRunE: bindEnv("confirm", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runRepository(cmd, args, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
addConfirmFlag(cmd, cfg.Confirm)
addVerboseFlag(cmd, cfg.Verbose)
cmd.AddCommand(NewRepositoryListCmd(newClient))
cmd.AddCommand(NewRepositoryAddCmd(newClient))
cmd.AddCommand(NewRepositoryRenameCmd(newClient))
cmd.AddCommand(NewRepositoryRemoveCmd(newClient))
return cmd
}
func NewRepositoryListCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Short: "List repositories",
Use: "list",
Aliases: []string{"ls"},
PreRunE: bindEnv("confirm", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runRepositoryList(cmd, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
addConfirmFlag(cmd, cfg.Confirm)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
func NewRepositoryAddCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Short: "Add a repository",
Use: "add <name> <url>",
SuggestFor: []string{"ad", "install"},
PreRunE: bindEnv("confirm", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runRepositoryAdd(cmd, args, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
addConfirmFlag(cmd, cfg.Confirm)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
func NewRepositoryRenameCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Short: "Rename a repository",
Use: "rename <old> <new>",
Aliases: []string{"mv"},
PreRunE: bindEnv("confirm", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runRepositoryRename(cmd, args, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
addConfirmFlag(cmd, cfg.Confirm)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
func NewRepositoryRemoveCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Short: "Remove a repository",
Use: "remove <name>",
Aliases: []string{"rm"},
SuggestFor: []string{"delete", "del"},
PreRunE: bindEnv("confirm", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runRepositoryRemove(cmd, args, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
addConfirmFlag(cmd, cfg.Confirm)
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
// command implementations
// -----------------------
// Run
// (list by default or interactive with -c|--confirm)
func runRepository(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
cfg, err := newRepositoryConfig()
if err != nil {
return
}
// If in noninteractive, normal mode the help text is shown
if !cfg.Confirm {
return cmd.Help()
}
// If in interactive mode, the user chan choose which subcommand to invoke
// Prompt for action to perform
question := &survey.Question{
Name: "Action",
Prompt: &survey.Select{
Message: "Operation to perform:",
Options: []string{"list", "add", "rename", "remove"},
Default: "list",
}}
answer := struct{ Action string }{}
if err = survey.Ask([]*survey.Question{question}, &answer); err != nil {
return
}
// Run the command indicated
switch answer.Action {
case "list":
return runRepositoryList(cmd, newClient)
case "add":
return runRepositoryAdd(cmd, args, newClient)
case "rename":
return runRepositoryRename(cmd, args, newClient)
case "remove":
return runRepositoryRemove(cmd, args, newClient)
}
return fmt.Errorf("invalid action '%v'", answer.Action) // Unreachable
}
// List
func runRepositoryList(_ *cobra.Command, newClient ClientFactory) (err error) {
cfg, err := newRepositoryConfig()
if err != nil {
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
// List all repositories given a client instantiated about config.
rr, err := client.Repositories().All()
if err != nil {
return
}
// Print repository names, or name plus url if verbose
// This follows the format of `git remote`, as it is likely familiar.
for _, r := range rr {
if cfg.Verbose {
fmt.Fprintln(os.Stdout, r.Name+"\t"+r.URL())
} else {
fmt.Fprintln(os.Stdout, r.Name)
}
}
return
}
// Add
func runRepositoryAdd(_ *cobra.Command, args []string, newClient ClientFactory) (err error) {
// Supports both composable, discrete CLI commands or prompt-based "config"
// by setting the argument values (name and ulr) to value of positional args,
// but only requires them if not prompting. If prompting, those values
// become the prompt defaults.
cfg, err := newRepositoryConfig()
if err != nil {
return
}
// Adding a repository requires there be a config path structure on disk
if err = config.CreatePaths(); err != nil {
return
}
// Create a client instance which utilizes the given repositories path.
// Note that this MAY not be in the config structure if the environment
// variable to override said path was provided explicitly.
// TODO: rectify this inconsitency: the config default path structure will
// be created in XDG_CONFIG_HOME/func even if the repo path environment
// was set to some other location on disk.
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
// Preconditions
// If not confirming/prompting, assert the args were both provided.
if len(args) != 2 && !cfg.Confirm {
return fmt.Errorf("usage: func repository add <name> <url>")
}
// Extract Params
// Populate a struct with the arguments (if provided)
params := struct {
Name string
URL string
}{}
if len(args) > 0 {
params.Name = args[0]
}
if len(args) > 1 {
params.URL = args[1]
}
// Prompt/Confirm
// If confirming/prompting, interactively populate the params from the user
// (using the current values as defaults)
//
// If terminal not interactive, effective values are echoed.
//
// Note that empty values can be passed to the final client's Add method if:
// Argument(s) not provided
// Confirming (-c|--confirm)
// Is a noninteractive terminal
// This is an expected case. The empty value will be echoed to stdout, the
// API will be invoked, and a helpful error message will indicate that the
// request is missing required parameters.
if cfg.Confirm && interactiveTerminal() {
questions := []*survey.Question{
{
Name: "Name",
Validate: survey.Required,
Prompt: &survey.Input{
Message: "Name for the new repository:",
Default: params.Name,
},
}, {
Name: "URL",
Validate: survey.Required,
Prompt: &survey.Input{
Message: "URL of the new repository:",
Default: params.URL,
},
},
}
if err = survey.Ask(questions, &params); err != nil {
return
// not checking for terminal.InterruptError because failure to complete,
// for whatever reason, should exit the program non-zero.
}
} else if cfg.Confirm {
fmt.Fprintf(os.Stdout, "Name: %v\n", params.Name)
fmt.Fprintf(os.Stdout, "URL: %v\n", params.URL)
}
// Add repository
var n string
if n, err = client.Repositories().Add(params.Name, params.URL); err != nil {
return
}
if cfg.Verbose {
fmt.Fprintf(os.Stdout, "Repository added: %s\n", n)
}
return
}
// Rename
func runRepositoryRename(_ *cobra.Command, args []string, newClient ClientFactory) (err error) {
cfg, err := newRepositoryConfig()
if err != nil {
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
// Preconditions
if len(args) != 2 && !cfg.Confirm {
return fmt.Errorf("usage: func repository rename <old> <new>")
}
// Extract Params
params := struct {
Old string
New string
}{}
if len(args) > 0 {
params.Old = args[0]
}
if len(args) > 1 {
params.New = args[1]
}
// Repositories installed according to the client
// (does not include the builtin default)
repositories, err := installedRepositories(client)
if err != nil {
return
}
// Confirm (interactive prompt mode)
if cfg.Confirm && interactiveTerminal() {
questions := []*survey.Question{
{
Name: "Old",
Validate: survey.Required,
Prompt: &survey.Select{
Message: "Repository to rename:",
Options: repositories,
},
}, {
Name: "New",
Validate: survey.Required,
Prompt: &survey.Input{
Message: "New name:",
Default: params.New,
},
},
}
if err = survey.Ask(questions, &params); err != nil {
return // for any reason, including interrupt, is an nonzero exit
}
} else if cfg.Confirm {
fmt.Fprintf(os.Stdout, "Old: %v\n", params.Old)
fmt.Fprintf(os.Stdout, "New: %v\n", params.New)
}
// Rename the repository
if err = client.Repositories().Rename(params.Old, params.New); err != nil {
return
}
if cfg.Verbose {
fmt.Fprintln(os.Stdout, "Repository renamed")
}
return
}
// Remove
func runRepositoryRemove(_ *cobra.Command, args []string, newClient ClientFactory) (err error) {
cfg, err := newRepositoryConfig()
if err != nil {
return
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()
// Preconditions
if len(args) != 1 && !cfg.Confirm {
return fmt.Errorf("usage: func repository remove <name>")
}
// Extract param(s)
params := struct {
Name string
Sure bool
}{}
if len(args) > 0 {
params.Name = args[0]
}
// "Are you sure" confirmation flag
// (not using name 'Confirm' to avoid confustion with cfg.Confirm)
// defaults to Yes. This is debatable, but I don't want to choose the repo
// to remove and then have to see a prompt and then have to hit 'y'. Just
// prompting once to make sure, which requires another press of enter, seems
// sufficient.
params.Sure = true
// Repositories installed according to the client
// (does not include the builtin default)
repositories, err := installedRepositories(client)
if err != nil {
return
}
if len(repositories) == 0 {
return errors.New("no repositories installed. use 'add' to install")
}
// Confirm (interactive prompt mode)
if cfg.Confirm && interactiveTerminal() {
questions := []*survey.Question{
{
Name: "Name",
Validate: survey.Required,
Prompt: &survey.Select{
Message: "Repository to remove:",
Options: repositories,
},
}, {
Name: "Sure",
Prompt: &survey.Confirm{
Message: "This will remove the repository from local disk. Are you sure?",
Default: params.Sure,
},
},
}
if err = survey.Ask(questions, &params); err != nil {
return // for any reason, including interrupt, is a nonzero exit
}
} else if cfg.Confirm {
fmt.Fprintf(os.Stdout, "Repository: %v\n", params.Name)
}
// Cancel if they got cold feet.
if !params.Sure {
// While an argument could be made to the contrary, I believe it is
// important than an abort by the user, either by answering no to the
// confirmation or by an os interrupt such as ^C be considered an error,
// and thus a non-zero program exit. This is because a user may have
// chained the command, and an abort (for whatever reason) should cancel
// the whole chain. For example, given the command:
// func repo rm -cv && doSomethingOnSuccess
// The trailing command 'doSomethignOnSuccess' should not be evaluated if
// the first, `func repo rm`, does not exit 0.
if cfg.Verbose {
fmt.Fprintln(os.Stdout, "Repository remove canceled")
}
return fmt.Errorf("repository removal canceled")
}
// Remove the repository
if err = client.Repositories().Remove(params.Name); err != nil {
return
}
if cfg.Verbose {
fmt.Fprintln(os.Stdout, "Repository removed")
}
return
}
// Installed repositories
// All repositories which have been installed (does not include builtin)
func installedRepositories(client *fn.Client) ([]string, error) {
// Client API contract stipulates the list always lists the defeault builtin
// repo, and always lists it at index 0
repositories, err := client.Repositories().List()
if err != nil {
return []string{}, err
}
return repositories[1:], nil
}
// client config
// -------------
// repositoryConfig used for instantiating a fn.Client
type repositoryConfig struct {
Verbose bool // Enables verbose logging
Confirm bool // Enables interactive confirmation/prompting mode
}
// newRepositoryConfig creates a configuration suitable for use instantiating the
// fn Client. Note that parameters for the individual commands (add, remove etc)
// are collected separately in their requisite run functions.
func newRepositoryConfig() (cfg repositoryConfig, err error) {
// initial config is populated based on flags, which are themselves
// first populated by static defaults, then environment variables,
// finally command flags.
cfg = repositoryConfig{
Verbose: viper.GetBool("verbose"),
Confirm: viper.GetBool("confirm"),
}
// If not in confirm (interactive prompting) mode,
// this struct is complete.
if !cfg.Confirm {
return
}
// Prompt the terminal for interactive input using the current values
// as defaults. (noninteractive terminals are a noop)
if interactiveTerminal() {
return cfg.prompt()
}
// Noninteractive terminals in confirm/prompt mode simply echo
// effective values to stdout.
fmt.Fprintf(os.Stdout, "Verbose logging: %v\n", cfg.Verbose)
return
}
// prompt returns a config with values populated from interactivly prompting
// the user.
func (c repositoryConfig) prompt() (repositoryConfig, error) {
// These prompts are overly verbose, as the user calling --confirm likely
// only cares about the individual command-specific values (for example
// "name" and "url" when calling "add". However, we want to provide the
// ability to interactively choose _all_ options if the user really wants
// to, therefore these prompts are only shown if the user is "confirming
// verbosely", for example `func repository add -cv`. (of course the
// associated flags, environment variables etc are still respected. Just
// no prompts unless verbose)
if !c.Verbose || !interactiveTerminal() {
return c, nil
}
qs := []*survey.Question{
{
Name: "Verbose",
Prompt: &survey.Confirm{
Message: "Enable verbose logging:",
Default: c.Verbose,
},
},
}
err := survey.Ask(qs, &c)
return c, err
}

View File

@ -1,166 +0,0 @@
package cmd
import (
"testing"
. "knative.dev/func/pkg/testing"
)
// TestRepository_List ensures that the 'list' subcommand shows the client's
// set of repositories by name for builtin repositories, by explicitly
// setting the repositories' path to a new path which includes no others.
func TestRepository_List(t *testing.T) {
_ = FromTempDirectory(t)
cmd := NewRepositoryListCmd(NewClient)
cmd.SetArgs([]string{}) // Do not use test command args
// Execute the command, capturing the output sent to stdout
stdout := piped(t)
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Assert the output matches expectd (whitespace trimmed)
expect := "default"
output := stdout()
if output != expect {
t.Fatalf("expected:\n'%v'\ngot:\n'%v'\n", expect, output)
}
}
// TestRepository_Add ensures that the 'add' subcommand accepts its positional
// arguments, respects the repositories' path flag, and the expected name is echoed
// upon subsequent 'list'.
func TestRepository_Add(t *testing.T) {
url := ServeRepo("repository.git", t) + "#main"
_ = FromTempDirectory(t)
t.Log(url)
var (
add = NewRepositoryAddCmd(NewClient)
list = NewRepositoryListCmd(NewClient)
stdout = piped(t)
)
// Do not use test command args
add.SetArgs([]string{})
list.SetArgs([]string{})
// add [flags] <old> <new>
add.SetArgs([]string{
"newrepo",
url,
})
// Parse flags and args, performing action
if err := add.Execute(); err != nil {
t.Fatal(err)
}
// List post-add, capturing output from stdout
if err := list.Execute(); err != nil {
t.Fatal(err)
}
// Assert the list output now includes the name from args (whitespace trimmed)
expect := "default\nnewrepo"
output := stdout()
if output != expect {
t.Fatalf("expected:\n'%v'\ngot:\n'%v'\n", expect, output)
}
}
// TestRepository_Rename ensures that the 'rename' subcommand accepts its
// positional arguments, respects the repositories' path flag, and the name is
// reflected as having been renamed upon subsequent 'list'.
func TestRepository_Rename(t *testing.T) {
url := ServeRepo("repository.git", t)
_ = FromTempDirectory(t)
var (
add = NewRepositoryAddCmd(NewClient)
rename = NewRepositoryRenameCmd(NewClient)
list = NewRepositoryListCmd(NewClient)
stdout = piped(t)
)
// Do not use test command args
add.SetArgs([]string{})
rename.SetArgs([]string{})
list.SetArgs([]string{})
// add a repo which will be renamed
add.SetArgs([]string{"newrepo", url})
if err := add.Execute(); err != nil {
t.Fatal(err)
}
// rename [flags] <old> <new>
rename.SetArgs([]string{
"newrepo",
"renamed",
})
// Parse flags and args, performing action
if err := rename.Execute(); err != nil {
t.Fatal(err)
}
// List post-rename, capturing output from stdout
if err := list.Execute(); err != nil {
t.Fatal(err)
}
// Assert the list output now includes the name from args (whitespace trimmed)
expect := "default\nrenamed"
output := stdout()
if output != expect {
t.Fatalf("expected:\n'%v'\ngot:\n'%v'\n", expect, output)
}
}
// TestRepository_Remove ensures that the 'remove' subcommand accepts name as
// its argument, respects the repositories' flag, and the entry is removed upon
// subsequent 'list'.
func TestRepository_Remove(t *testing.T) {
url := ServeRepo("repository.git", t)
_ = FromTempDirectory(t)
var (
add = NewRepositoryAddCmd(NewClient)
remove = NewRepositoryRemoveCmd(NewClient)
list = NewRepositoryListCmd(NewClient)
stdout = piped(t)
)
// Do not use test command args
add.SetArgs([]string{})
remove.SetArgs([]string{})
list.SetArgs([]string{})
// add a repo which will be removed
add.SetArgs([]string{"newrepo", url})
if err := add.Execute(); err != nil {
t.Fatal(err)
}
// remove [flags] <name>
remove.SetArgs([]string{
"newrepo",
})
// Parse flags and args, performing action
if err := remove.Execute(); err != nil {
t.Fatal(err)
}
// List post-remove, capturing output from stdout
if err := list.Execute(); err != nil {
t.Fatal(err)
}
// Assert the list output now includes the name from args (whitespace trimmed)
expect := "default"
output := stdout()
if output != expect {
t.Fatalf("expected:\n'%v'\ngot:\n'%v'\n", expect, output)
}
}

View File

@ -1,222 +1,134 @@
package cmd
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver"
"github.com/mitchellh/go-homedir"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/term"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/client/pkg/util"
"knative.dev/func/cmd/templates"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s"
bosonFunc "github.com/boson-project/func"
)
// DefaultVersion when building source directly (bypassing the Makefile)
const DefaultVersion = "v0.0.0+source"
// DefaultNamespace is the global static default namespace, and is equivalent
// to the Kubernetes default namespace.
const DefaultNamespace = "default"
type RootCommandConfig struct {
Name string // usually `func` or `kn func`
Version
NewClient ClientFactory
}
// NewRootCmd creates the root of the command tree defines the command name, description, globally
// The root of the command tree defines the command name, descriotion, globally
// available flags, etc. It has no action of its own, such that running the
// resultant binary with no arguments prints the help/usage text.
func NewRootCmd(cfg RootCommandConfig) *cobra.Command {
cmd := &cobra.Command{
Use: cfg.Name,
Short: fmt.Sprintf("%s manages Knative Functions", cfg.Name),
Long: fmt.Sprintf(`%s is the command line interface for managing Knative Function resources
Create a new Node.js function in the current directory:
{{.Use}} create --language node myfunction
Deploy the function using Docker hub to host the image:
{{.Use}} deploy --registry docker.io/alice
Learn more about Functions: https://knative.dev/docs/functions/
Learn more about Knative at: https://knative.dev`, cfg.Name),
DisableAutoGenTag: true, // no docs header
SilenceUsage: true, // no usage dump on error
var root = &cobra.Command{
Use: "func",
Short: "Serverless functions",
SilenceErrors: true, // we explicitly handle errors in Execute()
}
SilenceUsage: true, // no usage dump on error
Long: `Serverless functions
// Environment Variables
// Evaluated first after static defaults, set all flags to be associated with
// a version prefixed by "FUNC_"
viper.AutomaticEnv() // read in environment variables for FUNC_<flag>
viper.SetEnvPrefix("func") // ensure that all have the prefix
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
Create, build and deploy functions in serverless containers for multiple runtimes on Knative`,
Example: `
# Create a node function called "node-sample" and enter the directory
kn func create myfunc && cd myfunc
// check if permissions for FUNC HOME are sufficient; warn if otherwise
cp := config.File()
if _, err := os.ReadFile(cp); os.IsPermission(err) {
fmt.Fprintf(os.Stderr, "Warning: Insufficient permissions to read config file at '%s' - continuing without it\n", cp)
}
// Client
// Use the provided ClientFactory or default to NewClient
newClient := cfg.NewClient
if newClient == nil {
newClient = NewClient
}
# Build the container image, push it to a registry and deploy it to the connected Knative cluster
# (replace <registry/user> with something like quay.io/user with an account that have you access to)
kn func deploy --registry <registry/user>
// Grouped commands
groups := templates.CommandGroups{
{
Header: "Primary Commands:",
Commands: []*cobra.Command{
NewCreateCmd(newClient),
NewDescribeCmd(newClient),
NewDeployCmd(newClient),
NewDeleteCmd(newClient),
NewListCmd(newClient),
NewSubscribeCmd(),
},
},
{
Header: "Development Commands:",
Commands: []*cobra.Command{
NewRunCmd(newClient),
NewInvokeCmd(newClient),
NewBuildCmd(newClient),
},
},
{
Header: "System Commands:",
Commands: []*cobra.Command{
NewConfigCmd(defaultLoaderSaver, newClient),
NewLanguagesCmd(newClient),
NewTemplatesCmd(newClient),
NewRepositoryCmd(newClient),
NewEnvironmentCmd(newClient, &cfg.Version),
},
},
{
Header: "MCP Commands:",
Commands: []*cobra.Command{
NewMCPServerCmd(),
},
},
{
Header: "Other Commands:",
Commands: []*cobra.Command{
NewCompletionCmd(),
NewVersionCmd(cfg.Version),
NewTektonClusterTasksCmd(),
},
},
}
// Add all commands to the root command, and initialize
groups.AddTo(cmd)
groups.SetRootUsage(cmd, nil)
return cmd
# Curl the service with the service URL
curl $(kn service describe myfunc -o url)
`,
}
// Helpers
// NewRootCmd is used to initialize func as kn plugin
func NewRootCmd() *cobra.Command {
return root
}
// When the code is loaded into memory upon invocation, the cobra/viper packages
// are invoked to gather system context. This includes reading the configuration
// file, environment variables, and parsing the command flags.
func init() {
// read in environment variables that match
viper.AutomaticEnv()
verbose := viper.GetBool("verbose")
// Populate the `verbose` flag with the value of --verbose, if provided,
// which thus overrides both the default and the value read in from the
// config file (i.e. flags always take highest precidence).
root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", verbose, "print verbose logs")
err := viper.BindPFlag("verbose", root.PersistentFlags().Lookup("verbose"))
if err != nil {
panic(err)
}
// Override the --version template to match the output format from the
// version subcommand: nothing but the version.
root.SetVersionTemplate(`{{printf "%s\n" .Version}}`)
// Prefix all environment variables with "FUNC_" to avoid collisions with other apps.
viper.SetEnvPrefix("func")
}
// Execute the command tree by executing the root command, which runs
// according to the context defined by: the optional config file,
// Environment Variables, command arguments and flags.
func Execute(ctx context.Context) {
// Sets version to a string partially populated by compile-time flags.
root.Version = version.String()
// Execute the root of the command tree.
if err := root.ExecuteContext(ctx); err != nil {
if ctx.Err() != nil {
os.Exit(130)
return
}
// Errors are printed to STDERR output and the process exits with code of 1.
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// Helper functions used by multiple commands
// ------------------------------------------
// registry to use is that provided as --registry or FUNC_REGISTRY.
// If not provided, global configuration determines the default to use.
func registry() string {
if r := viper.GetString("registry"); r != "" {
return r
}
cfg, _ := config.NewDefault()
return cfg.RegistryDefault()
}
// effectivePath to use is that which was provided by --path or FUNC_PATH.
// Manually parses flags such that this can be used during (cobra/viper) flag
// definition (prior to parsing).
func effectivePath() (path string) {
var (
env = os.Getenv("FUNC_PATH")
fs = pflag.NewFlagSet("", pflag.ContinueOnError)
p = fs.StringP("path", "p", "", "")
)
fs.SetOutput(io.Discard)
fs.ParseErrorsWhitelist.UnknownFlags = true // wokeignore:rule=whitelist
// Preparsing flags intentionally ignores errors because this is intended
// to be an opportunistic parse of the path flags, with actual validation of
// flags taking place later in the instantiation process by the cobra pkg.
_ = fs.Parse(os.Args[1:])
if env != "" {
path = env
}
if *p != "" {
path = *p
}
return path
}
// defaultNamespace to use when none is provided explicitly.
// This requires a bit more logic than normal flag defaults, which rely
// on the order of precedence Static Config -> Global Config -> Current Func ->
// -> Environment Variables -> Flags. This default calculation adds the
// step of using the active Kubernetes namespace after Static Config and before
// the optional Global Config setting. The static default is "default"
func defaultNamespace(f fn.Function, verbose bool) string {
// Specifically-requested
if f.Namespace != "" {
return f.Namespace
}
// Last deployed
if f.Deploy.Namespace != "" {
return f.Deploy.Namespace
}
// Active K8S namespace
namespace, err := k8s.GetDefaultNamespace()
if err != nil {
if verbose {
fmt.Fprintf(os.Stderr, "Unable to get current active kubernetes namespace. Defaults will be used. %v", err)
}
} else if namespace != "" {
return namespace
}
// Globally-defined default in ~/.config/func/config.yaml is next
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(os.Stderr, "error loading global config at '%v'. %v\n", config.File(), err)
} else if cfg.Namespace != "" {
return cfg.Namespace
}
// Static Default is the standard Kubernetes default "default"
return DefaultNamespace
}
// interactiveTerminal returns whether or not the currently attached process
// terminal is interactive. Used for determining whether or not to
// interactively prompt the user to confirm default choices, etc.
func interactiveTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd()))
fi, err := os.Stdin.Stat()
return err == nil && ((fi.Mode() & os.ModeCharDevice) != 0)
}
// cwd returns the current working directory or exits 1 printing the error.
func cwd() (cwd string) {
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to determine current working directory: %v", err)
os.Exit(1)
}
return cwd
}
// configPath is the effective path to the optional config directory used for
// function defaults and extensible templates.
func configPath() (path string) {
if path = os.Getenv("XDG_CONFIG_HOME"); path != "" {
path = filepath.Join(path, "func")
return
}
home, err := homedir.Expand("~")
if err != nil {
fmt.Fprintf(os.Stderr, "could not derive home directory for use as default templates path: %v", err)
path = filepath.Join(".config", "func")
} else {
path = filepath.Join(home, ".config", "func")
}
return
}
// bindFunc which conforms to the cobra PreRunE method signature
type bindFunc func(*cobra.Command, []string) error
// bindEnv returns a bindFunc that binds env vars to the named flags.
// bindEnv returns a bindFunc that binds env vars to the namd flags.
func bindEnv(flags ...string) bindFunc {
return func(cmd *cobra.Command, args []string) (err error) {
for _, flag := range flags {
@ -224,24 +136,55 @@ func bindEnv(flags ...string) bindFunc {
return
}
}
viper.AutomaticEnv() // read in environment variables for FUNC_<flag>
viper.SetEnvPrefix("func") // ensure that all have the prefix
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
return
}
}
type functionOverrides struct {
Image string
Namespace string
Builder string
}
// functionWithOverrides sets the namespace and image strings for the
// Function project at root, if provided, and returns the Function
// configuration values.
// Please note that When this function is called, the overrides are not persisted.
func functionWithOverrides(root string, overrides functionOverrides) (f bosonFunc.Function, err error) {
f, err = bosonFunc.NewFunction(root)
if err != nil {
return
}
overrideMapping := []struct {
src string
dest *string
}{
{overrides.Builder, &f.Builder},
{overrides.Image, &f.Image},
{overrides.Namespace, &f.Namespace},
}
for _, m := range overrideMapping {
if m.src != "" {
*m.dest = m.src
}
}
return
}
// deriveName returns the explicit value (if provided) or attempts to derive
// from the given path. Path is defaulted to current working directory, where
// a function configuration, if it exists and contains a name, is used.
// a Function configuration, if it exists and contains a name, is used.
func deriveName(explicitName string, path string) string {
// If the name was explicitly provided, use it.
if explicitName != "" {
return explicitName
}
// If the directory at path contains an initialized function, use the name therein
f, err := fn.NewFunction(path)
// If the directory at path contains an initialized Function, use the name therein
f, err := bosonFunc.NewFunction(path)
if err == nil && f.Name != "" {
return f.Name
}
@ -249,205 +192,101 @@ func deriveName(explicitName string, path string) string {
return ""
}
// deriveNameAndAbsolutePathFromPath returns resolved function name and absolute path
// to the function project root. The input parameter path could be one of:
// 'relative/path/to/foo', '/absolute/path/to/foo', 'foo' or ”.
// deriveNameAndAbsolutePathFromPath returns resolved Function name and absolute path
// to the Function project root. The input parameter path could be one of:
// 'relative/path/to/foo', '/absolute/path/to/foo', 'foo' or ''
func deriveNameAndAbsolutePathFromPath(path string) (string, string) {
var absPath string
// If path is not specified, we would like to use current working dir
// If path is not specifed, we would like to use current working dir
if path == "" {
path = cwd()
}
// Expand the passed function name to its absolute path
// Expand the passed Function name to its absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return "", ""
}
// Get the name of the function, which equals to name of the current directory
// Get the name of the Function, which equals to name of the current directory
pathParts := strings.Split(strings.TrimRight(path, string(os.PathSeparator)), string(os.PathSeparator))
return pathParts[len(pathParts)-1], absPath
}
func mergeEnvs(envs []fn.Env, envToUpdate *util.OrderedMap, envToRemove []string) ([]fn.Env, int, error) {
updated := sets.NewString()
var counter int
for i := range envs {
if envs[i].Name != nil {
value, present := envToUpdate.GetString(*envs[i].Name)
if present {
envs[i].Value = &value
updated.Insert(*envs[i].Name)
counter++
// deriveImage returns the same image name which will be used if no explicit
// image is provided. I.e. derived from the configured registry (registry
// plus username) and the Function's name.
//
// This is calculated preemptively here in the CLI (prior to invoking the
// client), only in order to provide information to the user via the prompt.
// The client will calculate this same value if the image override is not
// provided.
//
// Derivation logic:
// deriveImage attempts to arrive at a final, full image name:
// format: [registry]/[username]/[FunctionName]:[tag]
// example: quay.io/myname/my.function.name:tag.
//
// Registry can optionally be omitted, in which case DefaultRegistry
// will be prepended.
//
// If the image flag is provided, this value is used directly (the user supplied
// --image or $FUNC_IMAGE). Otherwise, the Function at 'path' is loaded, and
// the Image name therein is used (i.e. it was previously calculated).
// Finally, the default registry is used, which is prepended to the Function
// name, and appended with ':latest':
func deriveImage(explicitImage, defaultRegistry, path string) string {
if explicitImage != "" {
return explicitImage // use the explicit value provided.
}
}
}
it := envToUpdate.Iterator()
for name, value, ok := it.NextString(); ok; name, value, ok = it.NextString() {
if !updated.Has(name) {
n := name
v := value
envs = append(envs, fn.Env{Name: &n, Value: &v})
counter++
}
}
for _, name := range envToRemove {
for i, envVar := range envs {
if *envVar.Name == name {
envs = append(envs[:i], envs[i+1:]...)
counter++
break
}
}
}
errMsg := fn.ValidateEnvs(envs)
if len(errMsg) > 0 {
return []fn.Env{}, 0, fmt.Errorf("error(s) while validating envs: %s", strings.Join(errMsg, "\n"))
}
return envs, counter, nil
}
// addConfirmFlag ensures common text/wording when the --path flag is used
func addConfirmFlag(cmd *cobra.Command, dflt bool) {
cmd.Flags().BoolP("confirm", "c", dflt, "Prompt to confirm options interactively ($FUNC_CONFIRM)")
}
// addPathFlag ensures common text/wording when the --path flag is used
func addPathFlag(cmd *cobra.Command) {
cmd.Flags().StringP("path", "p", "", "Path to the function. Default is current directory ($FUNC_PATH)")
}
// addVerboseFlag ensures common text/wording when the --path flag is used
func addVerboseFlag(cmd *cobra.Command, dflt bool) {
cmd.Flags().BoolP("verbose", "v", dflt, "Print verbose logs ($FUNC_VERBOSE)")
}
// cwd returns the current working directory or exits 1 printing the error.
func cwd() (cwd string) {
cwd, err := os.Getwd()
f, err := bosonFunc.NewFunction(path)
if err != nil {
panic(fmt.Sprintf("Unable to determine current working directory: %v", err))
return "" // unable to derive due to load error (uninitialized?)
}
return cwd
if f.Image != "" {
return f.Image // use value previously provided or derived.
}
derivedValue, _ := bosonFunc.DerivedImage(path, defaultRegistry)
return derivedValue // Use the func system's derivation logic.
}
// Version information populated on build.
type Version struct {
// Version tag of the git commit, or 'tip' if no tag.
Vers string
// Kver is the version of knative in which func was most recently
// If the build is not tagged as being released with a specific Knative
// build, this is the most recent version of knative along with a suffix
// consisting of the number of commits which have been added since it was
// included in Knative.
Kver string
// Hash of the currently active git commit on build.
Hash string
// Verbose printing enabled for the string representation.
Verbose bool
func envFromCmd(cmd *cobra.Command) map[string]string {
envM := make(map[string]string)
if cmd.Flags().Changed("env") {
envA, err := cmd.Flags().GetStringArray("env")
if err == nil {
for _, s := range envA {
kvp := strings.Split(s, "=")
if len(kvp) == 2 && kvp[0] != "" {
envM[kvp[0]] = kvp[1]
} else if len(kvp) == 1 && kvp[0] != "" {
envM[kvp[0]] = ""
}
}
}
}
return envM
}
// Return the stringification of the Version struct.
func (v Version) String() string {
// Initialize the default value to the zero semver with a descriptive
// metadta tag indicating this must have been built from source if
// undefined:
if v.Vers == "" {
v.Vers = DefaultVersion
}
if v.Verbose {
return v.StringVerbose()
}
_ = semver.MustParse(v.Vers)
return v.Vers
}
func mergeEnvMaps(dest, src map[string]string) map[string]string {
result := make(map[string]string, len(dest)+len(src))
// StringVerbose returns the version along with extended version metadata.
func (v Version) StringVerbose() string {
var (
vers = v.Vers
kver = v.Kver
hash = v.Hash
)
if strings.HasPrefix(kver, "knative-") {
kver = strings.Split(kver, "-")[1]
for name, value := range dest {
if strings.HasSuffix(name, "-") {
if _, ok := src[strings.TrimSuffix(name, "-")]; !ok {
result[name] = value
}
} else {
if _, ok := src[name+"-"]; !ok {
result[name] = value
}
}
}
return fmt.Sprintf(
"Version: %s\n"+
"Knative: %s\n"+
"Commit: %s\n"+
"SocatImage: %s\n"+
"TarImage: %s\n",
vers,
kver,
hash,
k8s.SocatImage,
k8s.TarImage)
}
// surveySelectDefault returns 'value' if defined and exists in 'options'.
// Otherwise, options[0] is returned if it exists. Empty string otherwise.
//
// Usage Example:
//
// languages := []string{ "go", "node", "rust" },
// survey.Select{
// Options: options,
// Default: surveySelectDefaut(cfg.Language, languages),
// }
//
// Summary:
//
// This protects against an incorrectly initialized survey.Select when the user
// has provided a nonexistant option (validation is handled elsewhere) or
// when a value is required but there exists no defaults (no default value on
// the associated flag).
//
// Explanation:
//
// The above example chooses the default for the Survey (--confirm) question
// in a way that works with user-provided flag and environment variable values.
//
// `cfg.Language` is the current value set in the config struct, which is
// populated from (in ascending order of precedence):
// static flag default, associated environment variable, or command flag.
// `languages` are the options which are being used by the survey select.
//
// This cascade allows for the Survey questions to be properly pre-initialzed
// with their associated environment variables or flags. For example,
// A user whose default language is set to 'node' using the global environment
// variable FUNC_LANGUAGE will have that option pre-selected when running
// `func create -c`.
//
// The 'survey' package expects the value of the Default member to exist
// in the 'Options' member. This is not possible when user-provided data is
// allowed for the default, hence this logic is necessary.
//
// For example, when the user is using prompts (--confirm) to select from a set
// of options, but the associated flag either has an unrecognized value, or no
// value at all, without this logic the resulting select prompt would be
// initialized with this as the default value, and the act of what appears to
// be choose the first option displayed does not overwrite the invalid default.
// It could perhaps be argued this is a shortcoming in the survey package, but
// it is also clearly an error to provide invalid data for a default.
func surveySelectDefault(value string, options []string) string {
for _, v := range options {
if value == v {
return v // The provided value is acceptable
for name, value := range src {
result[name] = value
}
}
if len(options) > 0 {
return options[0] // Sync with the option which will be shown by the UX
}
// Either the value is not an option or there are no options. Either of
// which should fail proper validation
return ""
return result
}

View File

@ -1,366 +1,58 @@
package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/ory/viper"
"knative.dev/client/pkg/util"
fn "knative.dev/func/pkg/functions"
. "knative.dev/func/pkg/testing"
)
const TestRegistry = "example.com/alice"
func TestRoot_mergeEnvMaps(t *testing.T) {
a := "A"
b := "B"
v1 := "x"
v2 := "y"
func Test_mergeEnvMaps(t *testing.T) {
type args struct {
envs []fn.Env
toUpdate *util.OrderedMap
toRemove []string
dest map[string]string
src map[string]string
}
tests := []struct {
name string
args args
want []fn.Env
want map[string]string
}{
{
"add new var to empty list",
args{
[]fn.Env{},
util.NewOrderedMapWithKVStrings([][]string{{a, v1}}),
[]string{},
},
[]fn.Env{{Name: &a, Value: &v1}},
},
{
"add new var",
args{
[]fn.Env{{Name: &b, Value: &v2}},
util.NewOrderedMapWithKVStrings([][]string{{a, v1}}),
[]string{},
map[string]string{"A": "1"},
map[string]string{"B": "2"},
},
[]fn.Env{{Name: &b, Value: &v2}, {Name: &a, Value: &v1}},
map[string]string{"A": "1", "B": "2"},
},
{
"update var",
args{
[]fn.Env{{Name: &a, Value: &v1}},
util.NewOrderedMapWithKVStrings([][]string{{a, v2}}),
[]string{},
map[string]string{"A": "1"},
map[string]string{"A": "2"},
},
[]fn.Env{{Name: &a, Value: &v2}},
},
{
"update multiple vars",
args{
[]fn.Env{{Name: &a, Value: &v1}, {Name: &b, Value: &v2}},
util.NewOrderedMapWithKVStrings([][]string{{a, v2}, {b, v1}}),
[]string{},
},
[]fn.Env{{Name: &a, Value: &v2}, {Name: &b, Value: &v1}},
map[string]string{"A": "2"},
},
{
"remove var",
args{
[]fn.Env{{Name: &a, Value: &v1}},
util.NewOrderedMap(),
[]string{a},
map[string]string{"A": "1"},
map[string]string{"A-": ""},
},
[]fn.Env{},
map[string]string{"A-": ""},
},
{
"remove multiple vars",
"re-add var",
args{
[]fn.Env{{Name: &a, Value: &v1}, {Name: &b, Value: &v2}},
util.NewOrderedMap(),
[]string{a, b},
map[string]string{"A-": ""},
map[string]string{"A": "1"},
},
[]fn.Env{},
},
{
"update and remove vars",
args{
[]fn.Env{{Name: &a, Value: &v1}, {Name: &b, Value: &v2}},
util.NewOrderedMapWithKVStrings([][]string{{a, v2}}),
[]string{b},
},
[]fn.Env{{Name: &a, Value: &v2}},
map[string]string{"A": "1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, _, err := mergeEnvs(tt.args.envs, tt.args.toUpdate, tt.args.toRemove)
if err != nil {
t.Errorf("mergeEnvs() for initial vars %v and toUpdate %v and toRemove %v got error %v",
tt.args.envs, tt.args.toUpdate, tt.args.toRemove, err)
}
if !reflect.DeepEqual(got, tt.want) {
gotString := "{ "
for _, e := range got {
gotString += fmt.Sprintf("{ %s: %s } ", *e.Name, *e.Value)
}
gotString += "}"
wantString := "{ "
for _, e := range tt.want {
wantString += fmt.Sprintf("{ %s: %s } ", *e.Name, *e.Value)
}
wantString += "}"
t.Errorf("mergeEnvs() = got: %s, want %s", gotString, wantString)
if got := mergeEnvMaps(tt.args.dest, tt.args.src); !reflect.DeepEqual(got, tt.want) {
t.Errorf("mergeEnvMaps() = %v, want %v", got, tt.want)
}
})
}
}
// TestRoot_CommandNameParameterized confirmst that the command name, as
// printed in help text, is parameterized based on the constructor parameters
// of the root command. This allows, for example, to have help text correct
// when both embedded as a plugin or standalone.
func TestRoot_CommandNameParameterized(t *testing.T) {
expectedSynopsis := "%v is the command line interface for"
tests := []string{
"func", // standalone
"kn func", // kn plugin
}
for _, testName := range tests {
var (
cmd = NewRootCmd(RootCommandConfig{Name: testName})
out = strings.Builder{}
)
cmd.SetArgs([]string{}) // Do not use test command args
cmd.SetOut(&out)
if err := cmd.Help(); err != nil {
t.Fatal(err)
}
if cmd.Use != testName {
t.Fatalf("expected command Use '%v', got '%v'", testName, cmd.Use)
}
if !strings.HasPrefix(out.String(), fmt.Sprintf(expectedSynopsis, testName)) {
t.Logf("Testing '%v'\n", testName)
t.Log(out.String())
t.Fatalf("Help text does not include substituted name '%v'", testName)
}
}
}
func TestVerbose(t *testing.T) {
tests := []struct {
name string
args []string
want string
wantLF int
}{
{
name: "verbose as version's flag",
args: []string{"version", "-v"},
want: "Version: v0.42.0",
wantLF: 6,
},
{
name: "no verbose",
args: []string{"version"},
want: "v0.42.0",
wantLF: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
viper.Reset()
var out bytes.Buffer
cmd := NewRootCmd(RootCommandConfig{
Name: "func",
Version: Version{
Vers: "v0.42.0",
Hash: "cafe",
Kver: "v1.10.0",
}})
cmd.SetArgs(tt.args)
cmd.SetOut(&out)
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
outLines := strings.Split(out.String(), "\n")
if len(outLines)-1 != tt.wantLF {
t.Errorf("expected output with %v line breaks but got %v:", tt.wantLF, len(outLines)-1)
}
if outLines[0] != tt.want {
t.Errorf("expected output: %q but got: %q", tt.want, outLines[0])
}
})
}
}
// TestRoot_effectivePath ensures that the path method returns the effective path
// to use with the following precedence: empty by default, then FUNC_PATH
// environment variable, -p flag, or finally --path with the highest precedence.
func TestRoot_effectivePath(t *testing.T) {
args := os.Args
t.Cleanup(func() { os.Args = args })
t.Run("default", func(t *testing.T) {
if effectivePath() != "" {
t.Fatalf("the default path should be '.', got '%v'", effectivePath())
}
})
t.Run("FUNC_PATH", func(t *testing.T) {
t.Setenv("FUNC_PATH", "p1")
if effectivePath() != "p1" {
t.Fatalf("the effetive path did not load the environment variable. Expected 'p1', got '%v'", effectivePath())
}
})
t.Run("--path", func(t *testing.T) {
os.Args = []string{"test", "--path=p2"}
if effectivePath() != "p2" {
t.Fatalf("the effective path did not load the --path flag. Expected 'p2', got '%v'", effectivePath())
}
})
t.Run("-p", func(t *testing.T) {
os.Args = []string{"test", "-p=p3"}
if effectivePath() != "p3" {
t.Fatalf("the effective path did not load the -p flag. Expected 'p3', got '%v'", effectivePath())
}
})
t.Run("short flag precedence", func(t *testing.T) {
t.Setenv("FUNC_PATH", "p1")
os.Args = []string{"test", "-p=p3"}
if effectivePath() != "p3" {
t.Fatalf("the effective path did not load the -p flag with precedence over FUNC_PATH. Expected 'p3', got '%v'", effectivePath())
}
})
t.Run("-p highest precedence", func(t *testing.T) {
t.Setenv("FUNC_PATH", "p1")
os.Args = []string{"test", "--path=p2", "-p=p3"}
if effectivePath() != "p3" {
t.Fatalf("the effective path did not take -p with highest precedence over --path and FUNC_PATH. Expected 'p3', got '%v'", effectivePath())
}
})
t.Run("continues on unrecognized flags", func(t *testing.T) {
os.Args = []string{"test", "-r=repo.example.com/bob", "-p=p3"}
if effectivePath() != "p3" {
t.Fatalf("the effective path did not evaluate when unexpected flags were present")
}
})
}
// Test_defaultNamespace ensures that the order of precedence for
// determining the effective namespace is followed.
// to use for the next deployment.
func Test_defaultNamespace(t *testing.T) {
// Clear non-test envs and set the test KUBECONFIG to nonexistent, but
// save the current working directory for setting kube context in some
// test cases.
cwd := Cwd()
_ = FromTempDirectory(t) // clears non-test envs and enters a temp dir.
t.Setenv("KUBECONFIG", filepath.Join(t.TempDir(), "nonexistent"))
// also clear the test KUBECONFIG env
tests := []struct {
name string
context bool
global bool
expected string
}{
// TODO cases for function state f.Namespace and f.Deploy.Namespace
{
name: "static default",
context: false, // no active kube context
global: false, // no global
expected: DefaultNamespace, // expect static default
}, {
name: "global config",
context: false,
global: true, // see the global defined in FUNC_HOME testdata
expected: "globaldefault", // expect global to override static
}, {
name: "active context",
context: true, // see the config in KUBECONFIG testdata
global: true,
expected: "mynamespace", // active context overrides global default
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.global { // enable a global config setting
t.Setenv("XDG_CONFIG_HOME", filepath.Join(cwd, "testdata", "Test_defaultNamespace"))
}
if test.context { // enable an active kube context
t.Setenv("KUBECONFIG", filepath.Join(cwd, "testdata", "Test_defaultNamespace", "kubeconfig"))
}
namespace := defaultNamespace(fn.Function{}, false)
if namespace != test.expected {
t.Fatalf("%v: expected namespace %q, got %q", test.name, test.expected, namespace)
}
})
}
}
// Helpers
// -------
// pipe the output of stdout to a buffer whose value is returned
// from the returned function. Call pipe() to start piping output
// to the buffer, call the returned function to access the data in
// the buffer.
func piped(t *testing.T) func() string {
t.Helper()
var (
o = os.Stdout
c = make(chan error, 1)
b strings.Builder
)
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
go func() {
_, err := io.Copy(&b, r)
r.Close()
c <- err
}()
return func() string {
os.Stdout = o
w.Close()
err := <-c
if err != nil {
t.Fatal(err)
}
return strings.TrimSpace(b.String())
}
}

View File

@ -1,392 +1,90 @@
package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
"knative.dev/func/pkg/docker"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/oci"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/docker"
)
func NewRunCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Run the function locally",
Long: `
NAME
{{rootCmdUse}} run - Run a function locally
SYNOPSIS
{{rootCmdUse}} run [-r|--registry] [-i|--image] [-e|--env] [--build]
[-b|--builder] [--builder-image] [-c|--confirm]
[--address] [--json] [-v|--verbose]
DESCRIPTION
Run the function locally.
Values provided for flags are not persisted to the function's metadata.
Containerized Runs
You can build your function in a container using the Pack or S2i builders.
On the contrary, non-containerized run is achieved via Host builder which
will use your host OS' environment to build the function. This builder is
currently enabled for Go and Python. Building defaults to using the Host
builder when available. You can alter this by using the --builder flag
eg: --builder=s2i.
Process Scaffolding
This is an Experimental Feature currently available only to Go and Python
projects. When running a function with --builder=host, the function is
first wrapped with code which presents it as a process. This "scaffolding"
is transient, written for each build or run, and should in most cases be
transparent to a function author.
EXAMPLES
o Run the function locally from within its container.
$ {{rootCmdUse}} run
o Run the function locally from within its container, forcing a rebuild
of the container even if no filesystem changes are detected. There are 2
builders available for containerized build - 'pack' and 's2i'.
$ {{rootCmdUse}} run --build=<builder>
o Run the function locally on the host with no containerization (Go/Python only).
$ {{rootCmdUse}} run --builder=host
o Run the function locally on a specific address.
$ {{rootCmdUse}} run --address='[::]:8081'
o Run the function locally and output JSON with the service address.
$ {{rootCmdUse}} run --json
`,
SuggestFor: []string{"rnu"},
PreRunE: bindEnv("build", "builder", "builder-image", "base-image",
"confirm", "env", "image", "path", "registry",
"start-timeout", "verbose", "address", "json"),
RunE: func(cmd *cobra.Command, _ []string) error {
return runRun(cmd, newClient)
},
}
// Global Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
// Function Context
f, _ := fn.NewFunction(effectivePath())
if f.Initialized() {
cfg = cfg.Apply(f)
}
// Flags
//
// Globally-Configurable Flags:
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().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)")
// Function-Context Flags:
// Options whose value is available on the function with context only
// (persisted but not globally configurable)
builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().String("builder-image", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("base-image", "", f.Build.BaseImage,
"Override the base image for your function (host builder only)")
cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag]. This option takes precedence over --registry. Specifying tag is optional. ($FUNC_IMAGE)")
cmd.Flags().StringArrayP("env", "e", []string{},
"Environment variable to set in the form NAME=VALUE. "+
"You may provide this flag multiple times for setting multiple environment variables. "+
func init() {
// Add the run command as a subcommand of root.
root.AddCommand(runCmd)
runCmd.Flags().StringArrayP("env", "e", []string{}, "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-).")
cmd.Flags().Duration("start-timeout", f.Run.StartTimeout, fmt.Sprintf("time this function needs in order to start. If not provided, the client default %v will be in effect. ($FUNC_START_TIMEOUT)", fn.DefaultStartTimeout))
// TODO: Without the "Host" builder enabled, this code-path is unreachable,
// so remove hidden flag when either the Host builder path is available,
// or when containerized runs support start-timeout (and ideally both).
// Also remember to add it to the command help text's synopsis section.
_ = cmd.Flags().MarkHidden("start-timeout")
// Static Flags:
// Options which have static defaults only
// (not globally configurable nor persisted as function metadata)
cmd.Flags().String("build", "auto",
"Build the function. [auto|true|false]. ($FUNC_BUILD)")
cmd.Flags().Lookup("build").NoOptDefVal = "true" // register `--build` as equivalient to `--build=true`
cmd.Flags().String("address", "",
"Interface and port on which to bind and listen. Default is 127.0.0.1:8080, or an available port if 8080 is not available. ($FUNC_ADDRESS)")
cmd.Flags().Bool("json", false, "Output as JSON. ($FUNC_JSON)")
// Oft-shared flags:
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)
// Tab Completion
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
if err := cmd.RegisterFlagCompletionFunc("builder-image", CompleteBuilderImageList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
}
return cmd
runCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
}
func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) {
var (
cfg runConfig
f fn.Function
)
cfg = newRunConfig(cmd) // Will add Prompt on upcoming UX refactor
var runCmd = &cobra.Command{
Use: "run",
Short: "Run the function locally",
Long: `Run the function locally
if f, err = fn.NewFunction(cfg.Path); err != nil {
return
}
if !f.Initialized() {
return fn.NewErrNotInitialized(f.Root)
}
Runs the function locally in the current directory or in the directory
specified by --path flag. The function must already have been built with the 'build' command.
`,
Example: `
# Build function's image first
kn func build
if err = cfg.Validate(cmd, f); err != nil {
return
}
# Run it locally as a container
kn func run
`,
SuggestFor: []string{"rnu"},
PreRunE: bindEnv("path"),
RunE: runRun,
}
if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
return
}
func runRun(cmd *cobra.Command, args []string) (err error) {
config := newRunConfig(cmd)
container := f.Build.Builder != "host"
// Ignore the verbose flag if JSON output
if cfg.JSON {
cfg.Verbose = false
}
// Client
clientOptions, err := cfg.clientOptions()
function, err := bosonFunc.NewFunction(config.Path)
if err != nil {
return
}
if container {
clientOptions = append(clientOptions, fn.WithRunner(docker.NewRunner(cfg.Verbose, os.Stdout, os.Stderr)))
}
if cfg.StartTimeout != 0 {
clientOptions = append(clientOptions, fn.WithStartTimeout(cfg.StartTimeout))
}
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, clientOptions...)
defer done()
function.Env = mergeEnvMaps(function.Env, config.Env)
// Build
//
// If requesting to run via the container, build the container if it is
// either out-of-date or a build was explicitly requested.
if container {
var digested bool
buildOptions, err := cfg.buildOptions()
if err != nil {
return err
}
// if image was specified, check if its digested and do basic validation
if cfg.Image != "" {
digested, err = isDigested(cfg.Image)
if err != nil {
return err
}
// image was parsed and both digested AND undigested imgs are valid
f.Build.Image = cfg.Image
}
// actual build step
if !digested {
if f, _, err = build(cmd, cfg.Build, f, client, buildOptions); err != nil {
return err
}
}
} else { // if !container
// dont run digested image without a container
if cfg.Image != "" {
digested, err := isDigested(cfg.Image)
if err != nil {
return err
}
if digested {
return fmt.Errorf("cannot use digested image with non-containerized builds (--builder=host)")
}
}
}
// Run
//
// Runs the code either via a container or the default host-based runner.
// For the former, build is required and a container runtime. For the
// latter, scaffolding is first applied and the local host must be
// configured to build/run the language of the function.
job, err := client.Run(cmd.Context(), f, fn.RunWithAddress(cfg.Address))
err = function.WriteConfig()
if err != nil {
return
}
defer func() {
if err = job.Stop(); err != nil {
fmt.Fprintf(cmd.OutOrStderr(), "Job stop error. %v", err)
}
}()
// Output based on format
if cfg.JSON {
// Create JSON output structure
output := struct {
Address string `json:"address"`
Host string `json:"host"`
Port string `json:"port"`
}{
Address: fmt.Sprintf("http://%s:%s", job.Host, job.Port),
Host: job.Host,
Port: job.Port,
// Check if the Function has been initialized
if !function.Initialized() {
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
}
jsonData, err := json.Marshal(output)
if err != nil {
return fmt.Errorf("failed to marshal JSON output: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), string(jsonData))
} else {
fmt.Fprintf(cmd.OutOrStderr(), "Function running on %s\n", net.JoinHostPort(job.Host, job.Port))
}
runner := docker.NewRunner()
runner.Verbose = config.Verbose
select {
case <-cmd.Context().Done():
if !errors.Is(cmd.Context().Err(), context.Canceled) {
err = cmd.Context().Err()
}
case err = <-job.Errors:
return
// Bubble up runtime errors on the optional channel used for async job
// such as docker containers.
}
client := bosonFunc.New(
bosonFunc.WithRunner(runner),
bosonFunc.WithVerbose(config.Verbose))
// NOTE: we do not f.Write() here unlike deploy (and build).
// running is ephemeral: a run is not affecting the function itself,
// as opposed to deploy commands, which are actually mutating the current
// state of the function as it exists on the network.
// Another way to think of this is that runs are development-centric tests,
// and thus most likely values changed such as environment variables,
// builder, etc. would not be expected to persist and affect the next deploy.
// Run is ephemeral, deploy is persistent.
err = client.Run(cmd.Context(), config.Path)
return
}
type runConfig struct {
buildConfig // further embeds config.Global
// Path of the Function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// Built instructs building to happen or not
// Can be 'auto' or a truthy value.
Build string
// Verbose logging.
Verbose bool
// Env variables. may include removals using a "-"
Env []string
// StartTimeout optionally adjusts the startup timeout from the client's
// default of fn.DefaultStartTimeout.
StartTimeout time.Duration
// Address is the interface and port to bind (e.g. "0.0.0.0:8081")
Address string
// JSON output format
JSON bool
Env map[string]string
}
func newRunConfig(cmd *cobra.Command) (c runConfig) {
c = runConfig{
buildConfig: newBuildConfig(),
Build: viper.GetString("build"),
Env: viper.GetStringSlice("env"),
StartTimeout: viper.GetDuration("start-timeout"),
Address: viper.GetString("address"),
JSON: viper.GetBool("json"),
func newRunConfig(cmd *cobra.Command) runConfig {
return runConfig{
Path: viper.GetString("path"),
Verbose: viper.GetBool("verbose"), // defined on root
Env: envFromCmd(cmd),
}
// NOTE: .Env should be viper.GetStringSlice, but this returns unparsed
// results and appears to be an open issue since 2017:
// https://github.com/spf13/viper/issues/380
var err error
if c.Env, err = cmd.Flags().GetStringArray("env"); err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error reading envs: %v", err)
}
return
}
// Configure the given function. Updates a function struct with all
// configurable values. Note that the config alrady includes function's
// current state, as they were passed through via flag defaults.
func (c runConfig) Configure(f fn.Function) (fn.Function, error) {
var err error
f = c.buildConfig.Configure(f)
f.Run.StartTimeout = c.StartTimeout
f.Run.Envs, err = applyEnvs(f.Run.Envs, c.Env)
// The other members; build and path; are not part of function
// state, so are not mentioned here in Configure.
return f, err
}
func (c runConfig) Prompt() (runConfig, error) {
var err error
if c.buildConfig, err = c.buildConfig.Prompt(); err != nil {
return c, err
}
if !interactiveTerminal() || !c.Confirm {
return c, nil
}
// TODO: prompt for additional settings here
return c, nil
}
func (c runConfig) Validate(cmd *cobra.Command, f fn.Function) (err error) {
// Bubble
if err = c.buildConfig.Validate(); err != nil {
return
}
// --build can be "auto"|true|false
if c.Build != "auto" {
if _, err := strconv.ParseBool(c.Build); err != nil {
return fmt.Errorf("unrecognized value for --build '%v'. Accepts 'auto', 'true' or 'false' (or similarly truthy value)", c.Build)
}
}
if f.Build.Builder == "host" && !oci.IsSupported(f.Runtime) {
return fmt.Errorf("the %q runtime currently requires being run in a container", f.Runtime)
}
// When the docker runner respects the StartTimeout, this validation check
// can be removed
if c.StartTimeout != 0 && f.Build.Builder != "host" {
return errors.New("the ability to specify the startup timeout for containerized runs is coming soon")
}
return
}

View File

@ -1,603 +0,0 @@
package cmd
import (
"context"
"fmt"
"testing"
"time"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)
func TestRun_Run(t *testing.T) {
tests := []struct {
name string // name of the test
desc string // description of the test
setup func(fn.Function, *testing.T) error // Optionally mutate function
args []string // args for the test case
buildError error // Set the builder to yield this error
runError error // Set the runner to yield this error
buildInvoked bool // should Builder.Build be invoked?
runInvoked bool // should Runner.Run be invoked?
jsonOutput bool // expect JSON output format
}{
{
name: "run and build by default",
desc: "Should run and build when build flag is not specified",
args: []string{},
buildInvoked: true,
runInvoked: true,
},
{
name: "run and build flag",
desc: "Should run and build when build is merely provided (defaults to true on presence)",
args: []string{"--build"},
buildInvoked: true,
runInvoked: true,
},
{
name: "run and build",
desc: "Should run and build when build is specifically requested",
args: []string{"--build=true"},
buildInvoked: true,
runInvoked: true,
},
{
name: "run and build with builder pack",
desc: "Should run and build when build is specifically requested with builder pack",
args: []string{"--build=true", "--builder=pack"},
buildInvoked: true,
runInvoked: true,
},
{
name: "run and build with builder s2i",
desc: "Should run and build when build is specifically requested with builder s2i",
args: []string{"--build=true", "--builder=s2i"},
buildInvoked: true,
runInvoked: true,
},
{
name: "run and build with builder invalid",
desc: "Should run and build when build is specifically requested with builder invalid",
args: []string{"--build=true", "--builder=invalid"},
buildError: fmt.Errorf("\"invalid\" is not a known builder. Available builders are \"pack\" and \"s2i\""),
buildInvoked: true,
runInvoked: true,
},
{
name: "run without build when disabled",
desc: "Should run but not build when build is expressly disabled",
args: []string{"--build=false"}, // can be any truthy value: 0, 'false' etc.
buildInvoked: false,
runInvoked: true,
},
{
name: "run and build on auto",
desc: "Should run and buil when build flag set to auto",
args: []string{"--build=auto"}, // can be any truthy value: 0, 'false' etc.
buildInvoked: true,
runInvoked: true,
},
{
name: "image existence builds",
desc: "Should build when image tag exists",
// The existence of an image tag value does not mean the function
// is built; that is the purvew of the buld stamp staleness check.
setup: func(f fn.Function, t *testing.T) error {
f.Image = "exampleimage"
return f.Write()
},
args: []string{},
buildInvoked: true,
runInvoked: true,
},
{
name: "Build errors return",
desc: "Errors building cause an immediate return with error",
args: []string{},
buildError: fmt.Errorf("generic build error"),
buildInvoked: true,
runInvoked: false,
},
{
name: "run with json output",
desc: "Should output JSON format when --json flag is used",
args: []string{"--json"},
buildInvoked: true,
runInvoked: true,
jsonOutput: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := FromTempDirectory(t)
runner := mock.NewRunner()
if tt.runError != nil {
runner.RunFn = func(context.Context, fn.Function, string, time.Duration) (*fn.Job, error) { return nil, tt.runError }
}
builder := mock.NewBuilder()
if tt.buildError != nil {
builder.BuildFn = func(f fn.Function) error { return tt.buildError }
}
// using a command whose client will be populated with mock
// builder and mock runner, each of which may be set to error if the
// test has an error defined.
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithBuilder(builder),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs(tt.args) // Do not use test command args
// set test case's function instance
f, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
if err != nil {
t.Fatal(err)
}
if tt.setup != nil {
if err := tt.setup(f, t); err != nil {
t.Fatal(err)
}
}
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
t0 := tt // capture tt into closure
_, err := cmd.ExecuteContextC(ctx)
if err != nil && t0.buildError != nil {
// This is an expected error, so simply continue execution ignoring
// the error (send nil on the channel to release the parent routine
runErrCh <- nil
return
} else if err != nil {
runErrCh <- err // error not expected
return
}
// No errors, but an error was expected:
if t0.buildError != nil {
runErrCh <- fmt.Errorf("Expected error: %v but got %v\n", t0.buildError, err)
}
// Ensure invocations match expectations
if builder.BuildInvoked != tt.buildInvoked {
runErrCh <- fmt.Errorf("Function was expected to build is: %v but build execution was: %v", tt.buildInvoked, builder.BuildInvoked)
}
if runner.RunInvoked != tt.runInvoked {
runErrCh <- fmt.Errorf("Function was expected to run is: %v but run execution was: %v", tt.runInvoked, runner.RunInvoked)
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
})
}
}
// TestRun_Images ensures that runnning 'func run' with --image
// (and additional flags) works as intended
func TestRun_Images(t *testing.T) {
tests := []struct {
name string
args []string
buildInvoked bool
runInvoked bool
runError error
buildError error
}{
{
name: "image with digest",
args: []string{"--image", "exampleimage@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"},
runInvoked: true,
buildInvoked: false,
},
{
name: "image with tag direct deploy",
args: []string{"--image", "username/exampleimage:latest", "--build=false"},
runInvoked: true,
buildInvoked: false,
},
{
name: "digested image without container should fail",
args: []string{"--container=false", "--image", "exampleimage@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"},
runInvoked: false,
buildInvoked: false,
buildError: fmt.Errorf("cannot use digested image with --container=false"),
},
{
name: "image should build even with tagged image given",
args: []string{"--image", "username/exampleimage:latest"},
runInvoked: true,
buildInvoked: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := FromTempDirectory(t)
runner := mock.NewRunner()
if tt.runError != nil {
runner.RunFn = func(context.Context, fn.Function, string, time.Duration) (*fn.Job, error) { return nil, tt.runError }
}
builder := mock.NewBuilder()
if tt.buildError != nil {
builder.BuildFn = func(f fn.Function) error { return tt.buildError }
}
// using a command whose client will be populated with mock
// builder and mock runner, each of which may be set to error if the
// test has an error defined.
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithBuilder(builder),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs(tt.args) // Do not use test command args
// set test case's function instance
_, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
t0 := tt // capture tt into closure
_, err := cmd.ExecuteContextC(ctx)
if err != nil && t0.buildError != nil {
// This is an expected error, so simply continue execution ignoring
// the error (send nil on the channel to release the parent routine
runErrCh <- nil
return
} else if err != nil {
runErrCh <- err // error not expected
return
}
// No errors, but an error was expected:
if t0.buildError != nil {
runErrCh <- fmt.Errorf("Expected error: %v but got %v\n", t0.buildError, err)
}
// Ensure invocations match expectations
if builder.BuildInvoked != tt.buildInvoked {
runErrCh <- fmt.Errorf("Function was expected to build is: %v but build execution was: %v", tt.buildInvoked, builder.BuildInvoked)
}
if runner.RunInvoked != tt.runInvoked {
runErrCh <- fmt.Errorf("Function was expected to run is: %v but run execution was: %v", tt.runInvoked, runner.RunInvoked)
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
})
}
}
// TestRun_CorrectImage enusures that correct image gets passed through to the
// runner.
func TestRun_CorrectImage(t *testing.T) {
tests := []struct {
name string
image string
args []string
buildInvoked bool
expectError bool
}{
{
name: "image with digest, auto build",
args: []string{"--image", "exampleimage@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"},
image: "exampleimage@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
buildInvoked: false,
},
{
name: "image with tag direct deploy",
args: []string{"--image", "username/exampleimage:latest", "--build=false"},
image: "username/exampleimage:latest",
buildInvoked: false,
},
{
name: "digested image without container should fail",
args: []string{"--container=false", "--image", "exampleimage@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"},
image: "",
buildInvoked: false,
expectError: true,
},
{
name: "image should build even with tagged image given",
args: []string{"--image", "username/exampleimage:latest"},
image: "username/exampleimage:latest",
buildInvoked: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := FromTempDirectory(t)
runner := mock.NewRunner()
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
// TODO: add if for empty image? -- should fail beforehand
if f.Build.Image != tt.image {
return nil, fmt.Errorf("Expected image: %v but got: %v", tt.image, f.Build.Image)
}
errs := make(chan error, 1)
stop := func() error { return nil }
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)
}
builder := mock.NewBuilder()
if tt.expectError {
builder.BuildFn = func(f fn.Function) error { return fmt.Errorf("expected error") }
}
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithBuilder(builder),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs(tt.args)
// set test case's function instance
_, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
t0 := tt // capture tt into closure
_, err := cmd.ExecuteContextC(ctx)
if err != nil && t0.expectError {
// This is an expected error, so simply continue execution ignoring
// the error (send nil on the channel to release the parent routine
runErrCh <- nil
return
} else if err != nil {
runErrCh <- err // error not expected
return
}
// No errors, but an error was expected:
if t0.expectError {
runErrCh <- fmt.Errorf("Expected error but got '%v'\n", err)
}
// Ensure invocations match expectations
if builder.BuildInvoked != tt.buildInvoked {
runErrCh <- fmt.Errorf("Function was expected to build is: %v but build execution was: %v", tt.buildInvoked, builder.BuildInvoked)
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
})
}
}
// TestRun_DirectOverride tests that an --image passed after a function has
// already been build, the given --image with digest will override built function
func TestRun_DirectOverride(t *testing.T) {
const overrideImage = "registry/myrepo/myimage@sha256:0000000000000000000000000000000000000000000000000000000000000000"
root := FromTempDirectory(t)
runner := mock.NewRunner()
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
if f.Build.Image != overrideImage {
return nil, fmt.Errorf("Expected image to be overridden with '%v' but got: '%v'", overrideImage, f.Build.Image)
}
errs := make(chan error, 1)
stop := func() error { return nil }
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)
}
builder1 := mock.NewBuilder()
// SETUP THE ENVIRONMENT & SITUATION
// create function
_, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
if err != nil {
t.Fatal(err)
}
// build function
cmdBuild := NewBuildCmd(NewTestClient(fn.WithBuilder(builder1), fn.WithRegistry("example.com/ns-to-override")))
if err := cmdBuild.Execute(); err != nil {
t.Fatal(err)
}
// fetch the functions state
_, err = fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
// builder for 'func run' -- shall not be invoked
builder2 := mock.NewBuilder()
builder2.BuildFn = func(f fn.Function) error {
return fmt.Errorf("should not be invoked")
}
// RUN THE ACTUAL TESTED COMMAND
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithBuilder(builder2),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs([]string{fmt.Sprintf("--image=%s", overrideImage)})
// run function with above argument
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
_, err := cmd.ExecuteContextC(ctx)
if err != nil {
runErrCh <- err // error was not expected
return
}
// Ensure invocation doesnt happen for the second time as the image was
// provided with a digest (should not build)
if builder2.BuildInvoked {
runErrCh <- fmt.Errorf("Function was not expected to build again but it did")
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
}
// TestRun_Address ensures that the --address flag is passed to the runner.
func TestRun_Address(t *testing.T) {
root := FromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
if err != nil {
t.Fatal(err)
}
testAddr := "0.0.0.0:1234"
runner := mock.NewRunner()
runner.RunFn = func(_ context.Context, f fn.Function, addr string, _ time.Duration) (*fn.Job, error) {
if addr != testAddr {
return nil, fmt.Errorf("Expected address '%v' but got: '%v'", testAddr, addr)
}
errs := make(chan error, 1)
stop := func() error { return nil }
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)
}
// RUN THE ACTUAL TESTED COMMAND
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs([]string{"--address", testAddr})
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
_, err := cmd.ExecuteContextC(ctx)
if err != nil {
runErrCh <- err // error was not expected
return
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
}
// TestRun_BaseImage ensures that running func run --base-image with various
// other
func TestRun_BaseImage(t *testing.T) {
const baseImage = "example.com/repo/baseImage"
tests := []struct {
name string
runtime string
builder string
expectError bool
}{
{
name: "should-succeed: python-runtime with host-builder",
runtime: "python",
builder: "host",
},
{
name: "should-succeed: go-runtime with host-builder",
runtime: "go",
builder: "host",
},
{
name: "should-fail: python-runtime with pack-builder",
runtime: "python",
builder: "pack",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := FromTempDirectory(t)
runner := mock.NewRunner()
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
errs := make(chan error, 1)
stop := func() error { return nil }
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)
}
builder := mock.NewBuilder()
//if tt.expectError {
// builder.BuildFn = func(f fn.Function) error { return fmt.Errorf("expected error") }
//}
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithBuilder(builder),
fn.WithRegistry(TestRegistry),
))
args := []string{"--build=true", fmt.Sprintf("--builder=%s", tt.builder), fmt.Sprintf("--base-image=%s", baseImage)}
cmd.SetArgs(args)
// set test case's function instance
_, err := fn.New().Init(fn.Function{Root: root, Runtime: tt.runtime})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
t0 := tt // capture tt into closure
_, err := cmd.ExecuteContextC(ctx)
if err != nil && t0.expectError {
// This is an expected error, so simply continue execution ignoring
// the error (send nil on the channel to release the parent routine
runErrCh <- nil
return
} else if err != nil {
runErrCh <- err // error not expected
return
}
// No errors, but an error was expected:
if t0.expectError {
runErrCh <- fmt.Errorf("Expected error but got '%v'\n", err)
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
})
}
}

View File

@ -1,136 +0,0 @@
package cmd
import (
"fmt"
"strings"
"github.com/ory/viper"
"github.com/spf13/cobra"
fn "knative.dev/func/pkg/functions"
)
func NewSubscribeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "subscribe",
Short: "Subscribe a function to events",
Long: `Subscribe a function to events
Subscribe the function to a set of events, matching a set of filters for Cloud Event metadata
and a Knative Broker from where the events are consumed.
`,
Example: `
# Subscribe the function to the 'default' broker where events have 'type' of 'com.example'
and an 'extension' attribute for the value 'my-extension-value'.
{{rootCmdUse}} subscribe --filter type=com.example --filter extension=my-extension-value
# Subscribe the function to the 'my-broker' broker where events have 'type' of 'com.example'
and an 'extension' attribute for the value 'my-extension-value'.
{{rootCmdUse}} subscribe --filter type=com.example --filter extension=my-extension-value --source my-broker
`,
SuggestFor: []string{"subcsribe"}, //nolint:misspell
PreRunE: bindEnv("filter", "source"),
RunE: func(cmd *cobra.Command, _ []string) error {
return runSubscribe(cmd)
},
}
cmd.Flags().StringArrayP("filter", "f", []string{}, "Filter for the Cloud Event metadata")
cmd.Flags().StringP("source", "s", "default", "The source, like a Knative Broker")
addPathFlag(cmd)
return cmd
}
func runSubscribe(cmd *cobra.Command) (err error) {
var (
cfg subscibeConfig
f fn.Function
)
cfg = newSubscribeConfig(cmd)
if f, err = fn.NewFunction(effectivePath()); err != nil {
return
}
if !f.Initialized() {
return fn.NewErrNotInitialized(f.Root)
}
if !f.Initialized() {
return fn.NewErrNotInitialized(f.Root)
}
// add subscription to function
f.Deploy.Subscriptions = updateOrAddSubscription(f.Deploy.Subscriptions, cfg)
// pump it
return f.Write()
}
func extractFilterMap(filters []string) map[string]string {
subscriptionFilters := make(map[string]string)
for _, filter := range filters {
kv := strings.Split(filter, "=")
if len(kv) != 2 {
fmt.Println("Invalid pair:", filter)
continue
}
key := kv[0]
value := kv[1]
subscriptionFilters[key] = value
}
return subscriptionFilters
}
type subscibeConfig struct {
Filter []string
Source string
}
func updateOrAddSubscription(subscriptions []fn.KnativeSubscription, cfg subscibeConfig) []fn.KnativeSubscription {
found := false
newFilters := extractFilterMap(cfg.Filter)
// Iterate over subscriptions to find if one with the same source already exists
for i, subscription := range subscriptions {
if subscription.Source == cfg.Source {
found = true
if subscription.Filters == nil {
subscription.Filters = make(map[string]string)
}
// Update filters. Override if the key already exists.
for newKey, newValue := range newFilters {
subscription.Filters[newKey] = newValue
}
subscriptions[i] = subscription // Reassign the updated subscription
break
}
}
// If a subscription with the source was not found, add a new one
if !found {
subscriptions = append(subscriptions, fn.KnativeSubscription{
Source: cfg.Source,
Filters: newFilters,
})
}
return subscriptions
}
func newSubscribeConfig(cmd *cobra.Command) (c subscibeConfig) {
c = subscibeConfig{
Filter: viper.GetStringSlice("filter"),
Source: viper.GetString("source"),
}
// NOTE: .Filter should be viper.GetStringSlice, but this returns unparsed
// results and appears to be an open issue since 2017:
// https://github.com/spf13/viper/issues/380
var err error
if c.Filter, err = cmd.Flags().GetStringArray("filter"); err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error reading filter arguments: %v", err)
}
return
}

View File

@ -1,288 +0,0 @@
package cmd
import (
"testing"
fn "knative.dev/func/pkg/functions"
. "knative.dev/func/pkg/testing"
)
func TestSubscribeWithAll(t *testing.T) {
root := FromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
if err != nil {
t.Fatal(err)
}
cmd := NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err := fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "my-broker" {
t.Fatalf("Expected subscription for broker to be 'my-broker', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "go" {
t.Fatalf("Expected subscription filter for 'foo' to be 'go', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
}
func TestSubscribeWithMultiple(t *testing.T) {
root := FromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
if err != nil {
t.Fatal(err)
}
cmd := NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err := fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "my-broker" {
t.Fatalf("Expected subscription for broker to be 'my-broker', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "go" {
t.Fatalf("Expected subscription filter for 'foo' to be 'go', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
cmd = NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "bar=foo"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err = fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "my-broker" {
t.Fatalf("Expected subscription for broker to be 'my-broker', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "go" {
t.Fatalf("Expected subscription filter for 'foo' to be 'go', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
if f.Deploy.Subscriptions[0].Filters["bar"] != "foo" {
t.Fatalf("Expected subscription filter for 'bar' to be 'foo', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
}
func TestSubscribeWithMultipleBrokersAndOverride(t *testing.T) {
root := FromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
if err != nil {
t.Fatal(err)
}
cmd := NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err := fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "my-broker" {
t.Fatalf("Expected subscription for broker to be 'my-broker', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "go" {
t.Fatalf("Expected subscription filter for 'foo' to be 'go', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
cmd = NewSubscribeCmd()
cmd.SetArgs([]string{"--filter", "bar=foo"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err = fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[1].Source != "default" {
t.Fatalf("Expected subscription for broker to be 'default', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[1].Filters["bar"] != "foo" {
t.Fatalf("Expected subscription filter for 'bar' to be 'foo', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
cmd = NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=golang"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err = fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "my-broker" {
t.Fatalf("Expected subscription for broker to be 'my-broker', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "golang" {
t.Fatalf("Expected subscription filter for 'foo' to be 'golang', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
}
func TestSubscribeWithNoExplicitSourceAll(t *testing.T) {
root := FromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
if err != nil {
t.Fatal(err)
}
cmd := NewSubscribeCmd()
cmd.SetArgs([]string{"--filter", "foo=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err := fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "default" {
t.Fatalf("Expected subscription for broker to be 'default', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "go" {
t.Fatalf("Expected subscription filter for 'foo' to be 'go', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
}
func TestSubscribeWithDuplicated(t *testing.T) {
root := FromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
if err != nil {
t.Fatal(err)
}
cmd := NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err := fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if f.Deploy.Subscriptions == nil {
t.Fatal("Expected subscription to be present ")
}
if f.Deploy.Subscriptions[0].Source != "my-broker" {
t.Fatalf("Expected subscription for broker to be 'my-broker', but got '%v'", f.Deploy.Subscriptions[0].Source)
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "go" {
t.Fatalf("Expected subscription filter for 'foo' to be 'go', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
// call it again with same
cmd = NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err = fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if len(f.Deploy.Subscriptions) > 1 {
t.Fatal("Expected only one subscription to be present ")
}
// call it again and override
cmd = NewSubscribeCmd()
cmd.SetArgs([]string{"--source", "my-broker", "--filter", "foo=gogo"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Now load the function and ensure that the subscription is set correctly.
f, err = fn.NewFunction(root)
if err != nil {
t.Fatal(err)
}
if len(f.Deploy.Subscriptions) > 1 {
t.Fatal("Expected only one subscription to be present ")
}
if f.Deploy.Subscriptions[0].Filters["foo"] != "gogo" {
t.Fatalf("Expected subscription filter for 'foo' to be 'gogo', but got '%v'", f.Deploy.Subscriptions[0].Filters["foo"])
}
}

View File

@ -1,180 +0,0 @@
package cmd
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"text/tabwriter"
"github.com/ory/viper"
"github.com/spf13/cobra"
"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
)
// ErrTemplateRepoDoesNotExist is a sentinel error if a template repository responds with 404 status code
var ErrTemplateRepoDoesNotExist = errors.New("template repo does not exist")
func NewTemplatesCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "templates",
Short: "List available function source templates",
Long: `
NAME
{{rootCmdUse}} templates - list available function source templates
SYNOPSIS
{{rootCmdUse}} templates [language] [--json] [-r|--repository]
DESCRIPTION
List all templates available, optionally for a specific language runtime.
To specify a URI of a single, specific repository for which templates
should be displayed, use the --repository flag.
Installed repositories are by default located at ~/.func/repositories
($XDG_CONFIG_HOME/.func/repositories). This can be overridden with
$FUNC_REPOSITORIES_PATH.
To see all available language runtimes, see the 'languages' command.
EXAMPLES
o Show a list of all available templates grouped by language runtime
$ {{rootCmdUse}} templates
o Show a list of all templates for the Go runtime
$ {{rootCmdUse}} templates go
o Return a list of all template runtimes in JSON output format
$ {{rootCmdUse}} templates --json
o Return Go templates in a specific repository
$ {{rootCmdUse}} templates go --repository=https://github.com/boson-project/templates
`,
PreRunE: bindEnv("json", "repository", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runTemplates(cmd, args, newClient)
},
}
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}
cmd.Flags().Bool("json", false, "Set output to JSON format. (Env: $FUNC_JSON)")
cmd.Flags().StringP("repository", "r", "", "URI to a specific repository to consider ($FUNC_REPOSITORY)")
addVerboseFlag(cmd, cfg.Verbose)
return cmd
}
func runTemplates(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
// Gather config
cfg, err := newTemplatesConfig()
if err != nil {
return
}
// Simple ping to the repo to avoid subsequent errors from http package if it does not exist
if cfg.Repository != "" {
res, err := http.Get(cfg.Repository)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return ErrTemplateRepoDoesNotExist
}
}
// Client which will provide data
client, done := newClient(
ClientConfig{Verbose: cfg.Verbose},
fn.WithRepository(cfg.Repository))
defer done()
// For a single language runtime
// -------------------
if len(args) == 1 {
templates, err := client.Templates().List(args[0])
if err != nil {
return err
}
if cfg.JSON {
s, err := json.MarshalIndent(templates, "", " ")
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), string(s))
} else {
for _, template := range templates {
fmt.Fprintln(cmd.OutOrStdout(), template)
}
}
return nil
} else if len(args) > 1 {
return errors.New("unexpected extra arguments")
}
// All language runtimes
// ------------
runtimes, err := client.Runtimes()
if err != nil {
return
}
if cfg.JSON {
// Gather into a single data structure for printing as json
templateMap := make(map[string][]string)
for _, runtime := range runtimes {
templates, err := client.Templates().List(runtime)
if err != nil {
return err
}
templateMap[runtime] = templates
}
s, err := json.MarshalIndent(templateMap, "", " ")
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), string(s))
} else {
// print using a formatted writer (sorted)
builder := strings.Builder{}
writer := tabwriter.NewWriter(&builder, 0, 0, 3, ' ', 0)
fmt.Fprint(writer, "LANGUAGE\tTEMPLATE\n")
for _, runtime := range runtimes {
templates, err := client.Templates().List(runtime)
if err != nil {
return err
}
for _, template := range templates {
fmt.Fprintf(writer, "%v\t%v\n", runtime, template)
}
}
writer.Flush()
fmt.Fprint(cmd.OutOrStdout(), builder.String())
}
return
}
type templatesConfig struct {
Verbose bool
Repository string // Consider only a specific repository (URI)
JSON bool // output as JSON
}
func newTemplatesConfig() (cfg templatesConfig, err error) {
cfg = templatesConfig{
Verbose: viper.GetBool("verbose"),
Repository: viper.GetString("repository"),
JSON: viper.GetBool("json"),
}
return
}

View File

@ -1,59 +0,0 @@
// Copyright © 2020 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package templates
import (
"text/template"
"github.com/spf13/cobra"
)
// CommandGroup is for grouping together commands
type CommandGroup struct {
// Title for command group shown in help/usage messages
Header string
// List of commands for this group
Commands []*cobra.Command
}
type CommandGroups []CommandGroup
// AddTo adds all commands from this group slice to the given command
func (g CommandGroups) AddTo(cmd *cobra.Command) {
for _, group := range g {
for _, sub := range group.Commands {
cmd.AddCommand(sub)
}
}
}
// SetRootUsage sets our own help and usage function messages to the root command
func (g CommandGroups) SetRootUsage(rootCmd *cobra.Command, extraTemplateFunctions *template.FuncMap) {
engine := newTemplateEngine(rootCmd, g, extraTemplateFunctions)
setHelpFlagsToSubCommands(rootCmd)
rootCmd.SetUsageFunc(engine.usageFunc())
rootCmd.SetHelpFunc(engine.helpFunc())
}
func setHelpFlagsToSubCommands(parent *cobra.Command) {
for _, cmd := range parent.Commands() {
if cmd.HasSubCommands() {
setHelpFlagsToSubCommands(cmd)
}
cmd.DisableFlagsInUseLine = true
}
}

View File

@ -1,69 +0,0 @@
// Copyright © 2020 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package templates
import (
"fmt"
"testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"knative.dev/client/pkg/util"
"knative.dev/client/pkg/util/test"
)
var groups = CommandGroups{
{
"header-1",
[]*cobra.Command{{Use: "c0"}, {Use: "c1"}},
},
{
"header-2",
[]*cobra.Command{{Use: "c2"}},
},
}
func TestAddTo(t *testing.T) {
rootCmd := &cobra.Command{Use: "root"}
groups.AddTo(rootCmd)
for idx, cmd := range rootCmd.Commands() {
assert.Equal(t, cmd.Name(), fmt.Sprintf("c%d", idx))
}
}
func TestSetUsage(t *testing.T) {
rootCmd := &cobra.Command{Use: "root", Short: "root", Run: func(cmd *cobra.Command, args []string) {}}
groups.AddTo(rootCmd)
groups.SetRootUsage(rootCmd, nil)
for _, cmd := range rootCmd.Commands() {
assert.Assert(t, cmd.DisableFlagsInUseLine)
}
capture := test.CaptureOutput(t)
err := (rootCmd.UsageFunc())(rootCmd)
assert.NilError(t, err)
stdOut, stdErr := capture.Close()
assert.Equal(t, stdErr, "")
assert.Assert(t, util.ContainsAll(stdOut, "header-1", "header-2"))
capture = test.CaptureOutput(t)
(rootCmd.HelpFunc())(rootCmd, nil)
stdOut, stdErr = capture.Close()
assert.Equal(t, stdErr, "")
assert.Assert(t, util.ContainsAll(stdOut, "root", "header-1", "header-2"))
}

View File

@ -1,212 +0,0 @@
// Copyright © 2020 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package templates
import (
"bytes"
"fmt"
"os"
"strings"
"text/template"
"unicode"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"golang.org/x/term"
)
type templateEngine struct {
RootCmd *cobra.Command
CommandGroups
functions template.FuncMap
}
// Create new template engine
func newTemplateEngine(rootCmd *cobra.Command, g CommandGroups, extraFunctions *template.FuncMap) templateEngine {
engine := templateEngine{
RootCmd: rootCmd,
CommandGroups: g,
}
engine.functions = engine.templateFunctions()
if extraFunctions != nil {
for name, function := range *extraFunctions {
engine.functions[name] = function
}
}
return engine
}
func (e templateEngine) usageFunc() func(*cobra.Command) error {
return func(c *cobra.Command) error {
return e.fillTemplate("usage", c, usageTemplate())
}
}
func (e templateEngine) helpFunc() func(*cobra.Command, []string) {
return func(c *cobra.Command, s []string) {
err := e.fillTemplate("help", c, helpTemplate(c.Long))
if err != nil {
c.Println(err)
}
}
}
func (e templateEngine) fillTemplate(name string, c *cobra.Command, templ string) error {
t := template.New(name)
t.Funcs(e.functions)
_, err := t.Parse(templ)
if err != nil {
fmt.Fprintf(c.ErrOrStderr(), "\nINTERNAL: >>>>> %v\n", err)
return err
}
return t.Execute(c.OutOrStdout(), c)
}
// ======================================================================================
// Template helper functions
func (e templateEngine) templateFunctions() template.FuncMap {
return template.FuncMap{
"cmdGroupsString": e.cmdGroupsString,
"subCommandsString": e.subCommandsString,
"useLine": useLine,
"visibleFlags": visibleFlags,
"rpad": rpad,
"rootCmdName": e.rootCmdName,
"rootCmdUse": e.rootCmdUse,
"isRootCmd": e.isRootCmd,
"flagsUsages": flagsUsagesCobra, // or use flagsUsagesKubectl for kubectl like flag styles
"trim": strings.TrimSpace,
"trimRight": func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) },
"trimLeft": func(s string) string { return strings.TrimLeftFunc(s, unicode.IsSpace) },
"execTemplate": e.executeTemplate,
}
}
func (e templateEngine) executeTemplate(tbody string, data any) (string, error) {
t, err := template.New("").Funcs(e.templateFunctions()).Parse(tbody)
if err != nil {
return "", err
}
buf := &strings.Builder{}
err = t.Execute(buf, data)
return buf.String(), err
}
func (e templateEngine) cmdGroupsString() string {
groups := make([]string, 0, len(e.CommandGroups))
for _, cmdGroup := range e.CommandGroups {
groups = append(groups, formatCommandGroup(cmdGroup))
}
return strings.Join(groups, "\n\n")
}
func (e templateEngine) subCommandsString(c *cobra.Command) string {
return formatCommandGroup(CommandGroup{
Header: "Available Commands:",
Commands: c.Commands(),
})
}
func (e templateEngine) rootCmdName() string {
return e.RootCmd.CommandPath()
}
func (e templateEngine) rootCmdUse() string {
return e.RootCmd.Use
}
func (e templateEngine) isRootCmd(c *cobra.Command) bool {
return e.RootCmd == c
}
func visibleFlags(c *cobra.Command) *flag.FlagSet {
ret := flag.NewFlagSet("filtered", flag.ContinueOnError)
local := c.LocalFlags()
persistent := c.PersistentFlags()
local.VisitAll(func(flag *flag.Flag) {
if flag.Name != "help" && persistent.Lookup(flag.Name) == nil {
ret.AddFlag(flag)
}
})
return ret
}
func useLine(c *cobra.Command) string {
var useLine string
var suffix string
if c.HasParent() {
useLine = c.Parent().CommandPath() + " " + c.Use
suffix = "[flags]"
} else {
useLine = c.Use
suffix = "[command]"
}
if c.HasFlags() && !strings.Contains(useLine, suffix) {
useLine += " " + suffix
}
return useLine
}
func formatCommandGroup(cmdGroup CommandGroup) string {
cmds := []string{cmdGroup.Header}
for _, cmd := range cmdGroup.Commands {
if cmd.IsAvailableCommand() {
cmds = append(cmds, " "+rpad(cmd.Name(), cmd.NamePadding())+" "+cmd.Short)
}
}
return strings.Join(cmds, "\n")
}
func rpad(s string, padding int) string {
t := fmt.Sprintf("%%-%ds", padding)
return fmt.Sprintf(t, s)
}
// flagsUsagesCobra formats flags in Cobra style
func flagsUsagesCobra(f *flag.FlagSet) string {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err == nil {
return f.FlagUsagesWrapped(width)
} else {
return f.FlagUsages()
}
}
// flagsUsagesKubectl formats the flags like kubectl does
func flagsUsagesKubectl(f *flag.FlagSet) string {
x := new(bytes.Buffer)
f.VisitAll(func(flag *flag.Flag) {
if flag.Hidden {
return
}
format := "--%s=%s: %s\n"
if flag.Value.Type() == "string" {
format = "--%s='%s': %s\n"
}
if len(flag.Shorthand) > 0 {
format = " -%s, " + format
} else {
format = " %s " + format
}
fmt.Fprintf(x, format, flag.Shorthand, flag.Name, flag.DefValue, flag.Usage)
})
return x.String()
}

View File

@ -1,172 +0,0 @@
// Copyright © 2020 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package templates
import (
"strings"
"testing"
flag "github.com/spf13/pflag"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"knative.dev/client/pkg/util"
"knative.dev/client/pkg/util/test"
)
type testData struct {
cmd *cobra.Command
validate func(*testing.T, string, *cobra.Command)
}
func TestUsageFunc(t *testing.T) {
rootCmd, engine := newTestTemplateEngine()
subCmdWithSubs, _, _ := rootCmd.Find([]string{"g1.1"})
subCmd, _, _ := rootCmd.Find([]string{"g2.1"})
data := []testData{
{
rootCmd,
func(t *testing.T, out string, command *cobra.Command) {
validateRootUsageOutput(t, out)
},
},
{
subCmd,
func(t *testing.T, out string, command *cobra.Command) {
validateSubUsageOutput(t, out, command)
},
},
{
subCmdWithSubs,
func(t *testing.T, out string, command *cobra.Command) {
validateSubUsageOutput(t, out, command)
subsub := command.Commands()[0]
assert.Assert(t, util.ContainsAll(out, subsub.Name(), subsub.Short, "Available Commands:"))
},
},
}
for _, d := range data {
capture := test.CaptureOutput(t)
err := (engine.usageFunc())(d.cmd)
assert.NilError(t, err)
stdOut, stdErr := capture.Close()
assert.Equal(t, stdErr, "")
d.validate(t, stdOut, d.cmd)
}
}
func TestHelpFunc(t *testing.T) {
rootCmd, engine := newTestTemplateEngine()
subCmd := rootCmd.Commands()[0]
data := []testData{
{
rootCmd,
func(t *testing.T, out string, command *cobra.Command) {
validateRootUsageOutput(t, out)
assert.Assert(t, strings.Contains(out, command.Long))
},
},
{
subCmd,
func(t *testing.T, out string, command *cobra.Command) {
validateSubUsageOutput(t, out, command)
assert.Assert(t, strings.Contains(out, command.Long))
},
},
}
for _, d := range data {
capture := test.CaptureOutput(t)
(engine.helpFunc())(d.cmd, []string{})
stdOut, stdErr := capture.Close()
assert.Equal(t, stdErr, "")
d.validate(t, stdOut, d.cmd)
}
}
func TestUsageFlags(t *testing.T) {
f := flag.NewFlagSet("test", flag.ContinueOnError)
f.StringP("test", "t", "default", "test-option")
usage := flagsUsagesKubectl(f)
assert.Equal(t, usage, " -t, --test='default': test-option\n")
usage = flagsUsagesCobra(f)
assert.Equal(t, usage, " -t, --test string test-option (default \"default\")\n")
// test for flag with no shorthand
fl := f.Lookup("test")
assert.Assert(t, fl != nil)
fl.Shorthand = ""
usage = flagsUsagesKubectl(f)
assert.Equal(t, usage, " --test='default': test-option\n")
// test for hidden flag
err := f.MarkHidden("test")
assert.NilError(t, err)
usage = flagsUsagesKubectl(f)
assert.Equal(t, usage, "")
}
func validateRootUsageOutput(t *testing.T, stdOut string) {
assert.Assert(t, util.ContainsAll(stdOut, "root"))
assert.Assert(t, util.ContainsAll(stdOut, "header-1", "g1.1", "desc-g1.1", "g1.2", "desc-g1.2"))
assert.Assert(t, util.ContainsAll(stdOut, "header-2", "g2.1", "desc-g2.1", "g2.2", "desc-g2.2", "g2.3", "desc-g2.3"))
assert.Assert(t, util.ContainsAll(stdOut, "Use", "root", "--help"))
assert.Assert(t, util.ContainsAll(stdOut, "Use", "root", "<command>"))
}
func validateSubUsageOutput(t *testing.T, stdOut string, cmd *cobra.Command) {
assert.Assert(t, util.ContainsAll(stdOut, "Usage", cmd.CommandPath()+" [flags]"))
assert.Assert(t, util.ContainsAll(stdOut, "Flags", "--local-opt", "local option"))
assert.Assert(t, util.ContainsAll(stdOut, "Aliases", "alias"))
}
func newTestTemplateEngine() (*cobra.Command, templateEngine) {
rootCmd := &cobra.Command{Use: "root", Short: "desc-root", Long: "longdesc-root"}
rootCmd.PersistentFlags().String("global-opt", "", "global option")
cmdGroups := CommandGroups{
{
"header-1",
[]*cobra.Command{newCmd("g1.1"), newCmd("g1.2")},
},
{
"header-2",
[]*cobra.Command{newCmd("g2.1"), newCmd("g2.2"), newCmd("g2.3")},
},
}
engine := newTemplateEngine(rootCmd, cmdGroups, nil)
cmdGroups.AddTo(rootCmd)
// Add a sub-command to first command
cmd, _, _ := rootCmd.Find([]string{"g1.1"})
cmd.AddCommand(newCmd("g1.1.1"))
rootCmd.SetUsageFunc(engine.usageFunc())
return rootCmd, engine
}
func newCmd(name string) *cobra.Command {
ret := &cobra.Command{
Use: name,
Short: "desc-" + name,
Long: "longdesc-" + name,
Run: func(cmd *cobra.Command, args []string) {},
Aliases: []string{"alias"},
}
ret.Flags().String("local-opt", "", "local option")
return ret
}

View File

@ -1,93 +0,0 @@
// Copyright © 2020 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package templates
import (
"strings"
"unicode"
)
// Templates for help & usage messages. These have been initially taken over from
// https://github.com/kubernetes/kubectl/blob/f9e4fa6b9cff11b6e2949b76680b8cd5b8192eab/pkg/util/templates/templates.go
// and adapted to the specific needs of `kn`
const (
// sectionUsage is the help template section that displays the command's usage.
sectionUsage = `{{if and .Runnable (ne .UseLine "") (not (isRootCmd .))}}Usage:
{{useLine .}}
{{end}}`
// sectionAliases is the help template section that displays the command's aliases.
sectionAliases = `{{if ne (len .Aliases) 0}}Aliases:
{{.NameAndAliases}}
{{end}}`
// sectionExamples is the help template section that displays command examples.
sectionExamples = `{{if .HasExample}}Examples:
{{trimRight (execTemplate .Example .)}}
{{end}}`
// sectionCommandGroups is the grouped help message
sectionCommandGroups = `{{if isRootCmd .}}{{cmdGroupsString}}
{{end}}`
// sectionSubCommands is the help template section that displays the command's subcommands.
sectionSubCommands = `{{if and (not (isRootCmd .)) .HasAvailableSubCommands}}{{subCommandsString .}}
{{end}}`
// sectionFlags is the help template section that displays the command's flags.
sectionFlags = `{{$visibleFlags := visibleFlags .}}{{ if $visibleFlags.HasFlags}}Flags:
{{trimRight (flagsUsages $visibleFlags)}}
{{end}}`
// sectionGlobalFlags is the help template section that displays inherited flags.
sectionGlobalFlags = `{{if .HasInheritedFlags}}Global Flags:
{{trimRight (flagsUsages .InheritedFlags)}}
{{end}}`
// sectionTipsHelp is the help template section that displays the '--help' hint.
sectionTipsHelp = `{{if .HasSubCommands}}Use "{{rootCmdName}} <command> --help" for more information about a given command.
{{end}}`
)
// usageTemplate if the template for 'usage' used by most commands.
func usageTemplate() string {
sections := []string{
sectionUsage,
sectionAliases,
sectionExamples,
sectionCommandGroups,
sectionSubCommands,
sectionFlags,
sectionGlobalFlags,
sectionTipsHelp,
}
return strings.TrimRightFunc(strings.Join(sections, ""), unicode.IsSpace) + "\n"
}
// helpTemplate is the template for 'help' used by most commands.
func helpTemplate(message string) string {
if len(message) == 0 {
message = `{{with or .Long .Short }}{{. | trim}}{{end}}`
}
return message + "\n\n" + `{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
}

Some files were not shown because too many files have changed in this diff Show More