├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ ├── Dockerfile │ ├── commit.yaml │ ├── internal-images.yml │ ├── packaging.yaml │ ├── release.yaml │ └── release_notes.sh ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── .licenserignore ├── .mailmap ├── .travis.yml ├── CONTRIBUTING.md ├── DEVELOPER.md ├── LICENSE ├── Makefile ├── RATIONALE.md ├── README.md ├── Tools.mk ├── USAGE.md ├── api ├── run.go └── run_test.go ├── codecov.yml ├── e2e ├── README.md ├── admin_test.go ├── func-e_run_test.go ├── func-e_test.go ├── func-e_use_test.go ├── func-e_versions_test.go ├── func-e_which_test.go ├── main_test.go └── static-filesystem.yaml ├── go.mod ├── go.sum ├── internal ├── cmd │ ├── app.go │ ├── app_test.go │ ├── errors.go │ ├── help_test.go │ ├── man_test.go │ ├── run.go │ ├── run_cmd_test.go │ ├── run_test.go │ ├── testdata │ │ ├── .editorconfig │ │ ├── func-e_help.txt │ │ ├── func-e_run_help.txt │ │ ├── func-e_use_help.txt │ │ ├── func-e_versions_help.txt │ │ └── func-e_which_help.txt │ ├── usage_md_test.go │ ├── use.go │ ├── use_test.go │ ├── versions.go │ ├── versions_cmd_test.go │ ├── versions_test.go │ ├── which.go │ └── which_test.go ├── envoy │ ├── http.go │ ├── http_test.go │ ├── install.go │ ├── install_test.go │ ├── run.go │ ├── run_test.go │ ├── runtime.go │ ├── runtime_test.go │ ├── shutdown.go │ ├── shutdown │ │ ├── admin.go │ │ ├── admin_test.go │ │ ├── node.go │ │ ├── node_test.go │ │ └── shutdown.go │ ├── version.go │ ├── version_test.go │ ├── versions.go │ └── versions_test.go ├── globals │ └── globals.go ├── moreos │ ├── moreos.go │ ├── moreos_func-e_test.go │ ├── moreos_test.go │ ├── proc.go │ ├── proc_attr_darwin.go │ ├── proc_attr_linux.go │ ├── proc_windows.go │ └── testdata │ │ └── fake_func-e.go ├── tar │ ├── tar.go │ ├── tar_test.go │ └── testdata │ │ ├── empty.tar │ │ ├── empty.tar.gz │ │ ├── empty.tar.xz │ │ ├── foo │ │ ├── bar.sh │ │ └── bar │ │ │ ├── baz.txt │ │ │ └── empty.txt │ │ ├── test.tar │ │ ├── test.tar.gz │ │ └── test.tar.xz ├── test │ ├── envoy.go │ ├── fakebinary │ │ ├── fake_binary.go │ │ └── testdata │ │ │ └── fake_envoy.go │ ├── morerequire │ │ └── morerequire.go │ └── server.go └── version │ ├── .editorconfig │ ├── last_known_envoy.txt │ ├── version.go │ └── version_test.go ├── lint └── last_known_envoy_test.go ├── main.go ├── main_test.go ├── netlify.toml ├── packaging ├── icon@48w.ico ├── msi │ ├── func-e.p12 │ ├── func-e.wxs │ ├── msi_product_code.ps1 │ ├── verify_msi.cmd │ └── winget_manifest.sh └── nfpm │ ├── func-e.8 │ ├── nfpm.yaml │ ├── verify_deb.sh │ └── verify_rpm.sh └── site ├── .gitignore ├── README.md ├── config.toml ├── content ├── _index.md └── learn.md ├── layouts └── partials │ ├── extended_head.html │ └── footer.html └── static ├── favicon.ico ├── icons ├── icon@16w.png ├── icon@180w.png ├── icon@192w.png ├── icon@32w.png └── icon@512w.png ├── install.sh ├── manifest.webmanifest └── robots.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://editorconfig.org/#file-format-details 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # prevents windows runners from failing on lint 2 | * text eol=lf 3 | 4 | *.ico binary 5 | *.png binary 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 2 | 3 | * @codefromthecrypt @mathetake 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve func-e. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Run Envoy '...' 16 | 2. Make request '....' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment (please complete the relevant information):** 26 | - OS: [e.g. Ubuntu 16:04] 27 | - Envoy Version: [e.g. 1.10.0] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea or new platform for func-e to support. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/Dockerfile: -------------------------------------------------------------------------------- 1 | # This builds Docker images similar to the GitHub Actions Virtual Environments, 2 | # with the dependencies we need for end-to-end (e2e) tests. 3 | # 4 | # The runtime user `runner` is setup the same as GitHub Actions also. Notably, 5 | # this allows passwordless `sudo` for RPM and Debian testing. 6 | # 7 | # `make e2e` requires make and go, but `CGO=0` means gcc isn't needed. Ubuntu 8 | # installs more packages, notably windows, for the `check` and `dist` targets. 9 | # To run RPM tests on CentOS, you must build them first on Ubuntu. 10 | # 11 | # This build is intended for use in a matrix, testing all major Linux platforms 12 | # supported by Envoy: Ubuntu and CentOS * amd64 and arm64. Notably, this adds 13 | # CentOS and arm64 which aren't available natively on GitHub Actions. It is 14 | # intended to run arm64 with Travis (as opposed to via emulation). In any case, 15 | # all matrixes should be pushed for local debugging. 16 | # 17 | # e.g. Build the images: 18 | # ```bash 19 | # parent_image=quay.io/centos/centos:stream9 20 | # image_tag=centos-9 21 | # docker-buildx build \ 22 | # --platform linux/amd64 \ 23 | # --build-arg parent_image=${parent_image} \ 24 | # --build-arg go_stable_release=1_19 \ 25 | # --build-arg go_stable_revision=1.19.4 \ 26 | # --build-arg go_prior_release=1_18 \ 27 | # --build-arg go_prior_revision=1.18.8 \ 28 | # -t func-e-internal:${image_tag} .github/workflows 29 | # ``` 30 | # 31 | # e.g. Build func-e on Ubuntu, then end-to-end test on CentOS 32 | # ```bash 33 | # $ docker run --rm -v $PWD:/work func-e-internal:ubuntu-20.04 dist 34 | # $ docker run --rm -v $PWD:/work func-e-internal:centos-9 -o build/func-e_linux_amd64/func-e e2e 35 | # ``` 36 | # 37 | # You can troubleshoot like this: 38 | # ```bash 39 | # $ docker run --rm -v $PWD:/work -it --entrypoint /bin/bash func-e-internal:centos-9 40 | # ``` 41 | # https://quay.io/repository/centos/centos?tag=stream9&tab=tags 42 | ARG parent_image=quay.io/centos/centos:stream9 43 | 44 | # This section looks odd, but it is needed to match conventions of the GitHub 45 | # Actions runner. For example, TARGETARCH in Docker is "amd64" whereas GitHub 46 | # actions uses "x64". Moreover, depending on use, case format will change. 47 | # Docker lacks variable substitution options to do this, so we fake it with 48 | # stages. See https://github.com/moby/moby/issues/42904 49 | FROM $parent_image as base-amd64 50 | ARG arch=X64 51 | ARG arch_lc=x64 52 | 53 | ARG LINUX 54 | FROM $parent_image as base-arm64 55 | ARG arch=ARM64 56 | ARG arch_lc=arm64 57 | 58 | FROM base-${TARGETARCH} 59 | 60 | # CentOS runs e2e, but can't run dist as Windows packages are not available. 61 | # While it is possible to build osslsigncode on CentOS, msitools can't due to 62 | # missing libgcab1-devel package. The workaround is to `make dist` with Ubuntu. 63 | ARG centos_packages="make sudo" 64 | # Ubuntu runs check, dist, and e2e, so needs more packages. 65 | ARG ubuntu_packages="make sudo curl git zip wixl msitools osslsigncode" 66 | RUN if [ -f /etc/centos-release ]; then \ 67 | # Use Dandified YUM on CentOS >=8. 68 | dnf="dnf -qy" && ${dnf} install ${centos_packages} && ${dnf} clean all; \ 69 | else \ 70 | # Use noninteractive to prevent hangs asking about timezone on Ubuntu. 71 | export DEBIAN_FRONTEND=noninteractive && apt_get="apt-get -qq -y" && \ 72 | ${apt_get} update && ${apt_get} install ${ubuntu_packages} && ${apt_get} clean; \ 73 | fi 74 | 75 | # See https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope 76 | ARG TARGETARCH 77 | 78 | # This installs two GOROOTs: the stable and prior release. Two allows pull 79 | # requests to update from a stale release to current without a chicken-egg 80 | # problem or the skew and install time risks of on-demand installation. This 81 | # publishes only two versions as more would bloat the image. 82 | # 83 | # Build args control the precise GOROOTs to install, and should be taken from 84 | # the current GitHub Actions runner. Doing so allows version coherency between 85 | # normal runners and Docker, with version skew bounded by image push frequency. 86 | # See https://github.com/actions/virtual-environments for current versions. 87 | # 88 | # go_XXX_release is the underscore delimited release version. e.g. "1_19" 89 | # go_XXX_revision is the full version number. e.g. "1.19.4" 90 | # 91 | # These are used along with the architecture to build GOROOT variables. 92 | # e.g. GOROOT_1_19_X64=/opt/hostedtoolcache/go/1.19.4/x64 93 | ARG go_stable_revision 94 | ARG go_stable_url=https://golang.org/dl/go${go_stable_revision}.linux-${TARGETARCH}.tar.gz 95 | ARG goroot_stable=${runner_tool_cache}/go/${go_stable_revision}/${arch_lc} 96 | RUN mkdir -p ${goroot_stable} && curl -sSL ${go_stable_url} | tar --strip-components 1 -C ${goroot_stable} -xzpf - 97 | 98 | # Dockerfile doesn't support iteration, so repeat above for the prior release. 99 | ARG go_prior_revision 100 | ARG go_prior_url=https://golang.org/dl/go${go_prior_revision}.linux-${TARGETARCH}.tar.gz 101 | ARG goroot_prior=${runner_tool_cache}/go/${go_prior_revision}/${arch_lc} 102 | RUN mkdir -p ${goroot_prior} && curl -sSL ${go_prior_url} | tar --strip-components 1 -C ${goroot_prior} -xzpf - 103 | 104 | # Add and switch to the same user as the GitHub Actions runner. This prevents 105 | # ownership problems writing to volumes from the host to docker and visa versa. 106 | ARG user=runner 107 | ARG uid=1001 108 | ARG gid=121 109 | RUN groupadd -f -g ${gid} docker && \ 110 | useradd -u ${uid} -g ${gid} -md /home/runner -s /bin/bash -N ${user} && \ 111 | echo "${user} ALL=NOPASSWD: ALL" >> /etc/sudoers 112 | USER ${user} 113 | 114 | # Setup ENV variables used in make that match the GitHub Actions runner. 115 | ENV RUNNER_TOOL_CACHE ${runner_tool_cache} 116 | ARG go_stable_release 117 | ENV GOROOT_${go_stable_release}_${arch} ${goroot_stable} 118 | ARG go_prior_release 119 | ENV GOROOT_${go_prior_release}_${arch} ${goroot_prior} 120 | 121 | # Disable gcc to avoid a build dependency on gcc: its glibc might affect Envoy. 122 | ENV CGO_ENABLED 0 123 | 124 | # Set CWD to the work directory to avoid overlaps with $HOME. 125 | WORKDIR /work 126 | 127 | # Almost everything uses make, but you can override `--entrypoint /bin/bash`. 128 | ENTRYPOINT ["/usr/bin/make"] 129 | CMD ["help"] 130 | -------------------------------------------------------------------------------- /.github/workflows/commit.yaml: -------------------------------------------------------------------------------- 1 | # `name` value will appear "as is" in the badge. 2 | # See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository 3 | # yamllint --format github .github/workflows/commit.yaml 4 | --- 5 | name: "build" 6 | 7 | on: 8 | push: # We run tests on non-tagged pushes to master 9 | tags: '' 10 | branches: master 11 | # ignore docs as they are built with Netlify. Ignore travis-related changes, too. 12 | # func-e.wxs is used for the Windows Installer, so tested with msi.yaml 13 | paths-ignore: 14 | - '**/*.md' 15 | - 'site/**' 16 | - 'netlify.toml' 17 | - '.travis.yml' 18 | - 'packaging/msi/*' 19 | - 'packaging/icon@48w.ico' 20 | - '.github/workflows/msi.yaml' 21 | pull_request: # We also run tests on pull requests targeted at the master branch. 22 | branches: master 23 | paths-ignore: 24 | - '**/*.md' 25 | - 'site/**' 26 | - 'netlify.toml' 27 | - '.travis.yml' 28 | - 'packaging/msi/*' 29 | - 'packaging/icon@48w.ico' 30 | - '.github/workflows/msi.yaml' 31 | # workflow_dispatch will let us manually trigger the workflow from GitHub actions dashboard. 32 | # For example, you can try to build a branch without raising a pull request. 33 | # See https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/manually-running-a-workflow 34 | workflow_dispatch: 35 | 36 | defaults: 37 | run: # use bash for all operating systems unless overridden 38 | shell: bash 39 | 40 | jobs: 41 | test: 42 | name: "Run unit tests (${{ matrix.os }})" 43 | runs-on: ${{ matrix.os }} 44 | timeout-minutes: 90 # instead of 360 by default 45 | strategy: 46 | fail-fast: false # don't fail fast as sometimes failures are operating system specific 47 | matrix: # use latest available versions and be consistent on all workflows! 48 | os: [ubuntu-latest, macos-latest] 49 | 50 | steps: 51 | - name: "Checkout" 52 | uses: actions/checkout@v4 53 | 54 | - uses: actions/setup-go@v5 55 | with: 56 | cache: false 57 | go-version-file: go.mod 58 | 59 | - name: "Cache Go" 60 | uses: actions/cache@v4 61 | with: 62 | path: ~/go/pkg/mod 63 | # go.mod for go release version, go.sum for modules used, and Tools.mk for 'go run' tools 64 | key: test-${{ runner.os }}-go-${{ hashFiles('go.mod', 'go.sum', 'Tools.mk') }} 65 | restore-keys: test-${{ runner.os }}-go- 66 | 67 | - name: "Cache Envoy binaries" 68 | uses: actions/cache@v4 69 | with: # ~/.func-e/versions is cached so that we only re-download once: for TestFuncEInstall 70 | path: ~/.func-e/versions 71 | key: test-${{ runner.os }}-envoy-${{ hashFiles('internal/version/last_known_envoy.txt') }} 72 | restore-keys: test-${{ runner.os }}-envoy- 73 | 74 | - name: "Verify clean check-in" 75 | run: make check 76 | 77 | - name: "Run unit tests" 78 | run: make test 79 | 80 | - name: "Build the `func-e` binary" 81 | run: make build 82 | 83 | - name: "Run e2e tests using the `func-e` binary" 84 | run: make e2e 85 | -------------------------------------------------------------------------------- /.github/workflows/internal-images.yml: -------------------------------------------------------------------------------- 1 | # yamllint --format github .github/workflows/internal-images.yml 2 | --- 3 | name: internal-images 4 | 5 | # Refresh the tags once a day. This limits impact of rate-limited images. See RATIONALE.md 6 | on: 7 | schedule: 8 | - cron: "23 3 * * *" 9 | workflow_dispatch: # Allows manual refresh 10 | 11 | # This builds images and pushes them to ghcr.io/tetratelabs/func-e-internal:$tag 12 | # Using these in tests and as a parent (FROM) avoids docker.io rate-limits particularly on pull requests. 13 | # 14 | # To test this, try running end-to-end (e2e) tests! 15 | # ```bash 16 | # $ docker run --pull always --rm -v $PWD:/work ghcr.io/tetratelabs/func-e-internal:centos-9 e2e 17 | # ``` 18 | # 19 | # Make is the default entrypoint. To troubleshoot, use /bin/bash: 20 | # ```bash 21 | # $ docker run --pull always --rm -v $PWD:/work -it --entrypoint /bin/bash ghcr.io/tetratelabs/func-e-internal:centos-9 22 | # [runner@babce89b5580 work]$ 23 | # ``` 24 | jobs: 25 | build-and-push-images: 26 | runs-on: ubuntu-20.04 # Hard-coding an LTS means maintenance, but only once each 2 years! 27 | strategy: 28 | matrix: 29 | include: 30 | - parent_image: quay.io/centos/centos:stream9 # Envoy requires CentOS >=9. 31 | image_tag: centos-9 32 | - parent_image: ubuntu:20.04 # Always match runs-on! 33 | image_tag: ubuntu-20.04 34 | 35 | steps: 36 | # Same as doing this locally: echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_TOKEN}" --password-stdin 37 | - name: "Login into GitHub Container Registry" 38 | uses: docker/login-action@v1 39 | with: 40 | registry: ghcr.io 41 | username: ${{ github.repository_owner }} 42 | # GHCR_TOKEN= 43 | # - pushes Docker images to ghcr.io 44 | # - create via https://github.com/settings/tokens 45 | # - assign via https://github.com/organizations/tetratelabs/settings/secrets/actions 46 | # - needs repo:status, public_repo, write:packages, delete:packages 47 | password: ${{ secrets.GHCR_TOKEN }} 48 | 49 | # We need QEMU and Buildx for multi-platform (amd64+arm64) image push. 50 | # Note: arm64 is run only by Travis. See RATIONALE.md 51 | - name: "Setup QEMU" 52 | uses: docker/setup-qemu-action@v2 53 | 54 | - name: "Setup Buildx" 55 | uses: docker/setup-buildx-action@v2 56 | 57 | - name: "Checkout" 58 | uses: actions/checkout@v3 59 | 60 | # This finds the last two GOROOT variables and parses them into Docker 61 | # build args, so that the resulting image has them at the same location. 62 | # 63 | # We do this to allow pull requests to update go.mod with a new Golang 64 | # release without worrying if the Docker image has it, yet. 65 | # 66 | # Ex. GOROOT_1_19_X64=/opt/hostedtoolcache/go/1.19.4/x64 -> 67 | # GO_STABLE_RELEASE=1_19, GO_STABLE_REVISION=1.19.4 68 | - name: "Find and parse last two GOROOTs" 69 | run: | # Until Go releases hit triple digits, we can use simple ordering. 70 | goroot_stable_env=$(env|grep GOROOT_|sort -n|tail -1) 71 | echo "GO_STABLE_RELEASE=$(echo ${goroot_stable_env}|cut -d_ -f2,3)" >> $GITHUB_ENV 72 | echo "GO_STABLE_REVISION=$(echo ${goroot_stable_env}|cut -d/ -f5)" >> $GITHUB_ENV 73 | 74 | goroot_prior_env=$(env|grep GOROOT_|sort -n|tail -2|head -1) 75 | echo "GO_PRIOR_RELEASE=$(echo ${goroot_prior_env}|cut -d_ -f2,3)" >> $GITHUB_ENV 76 | echo "GO_PRIOR_REVISION=$(echo ${goroot_prior_env}|cut -d/ -f5)" >> $GITHUB_ENV 77 | 78 | - name: "Build and push" 79 | run: | 80 | docker_tag=ghcr.io/${{ github.repository_owner }}/func-e-internal:${IMAGE_TAG} 81 | docker buildx build --push \ 82 | --platform linux/amd64,linux/arm64 \ 83 | --build-arg parent_image=${PARENT_IMAGE} \ 84 | --build-arg go_stable_release=${GO_STABLE_RELEASE} \ 85 | --build-arg go_stable_revision=${GO_STABLE_REVISION} \ 86 | --build-arg go_prior_release=${GO_PRIOR_RELEASE} \ 87 | --build-arg go_prior_revision=${GO_PRIOR_REVISION} \ 88 | -t ${docker_tag} .github/workflows 89 | env: 90 | PARENT_IMAGE: ${{ matrix.parent_image }} 91 | IMAGE_TAG: ${{ matrix.image_tag }} 92 | -------------------------------------------------------------------------------- /.github/workflows/packaging.yaml: -------------------------------------------------------------------------------- 1 | # `name` value will appear "as is" in the badge. 2 | # See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository 3 | # yamllint --format github .github/workflows/packaging.yaml 4 | --- 5 | name: "packaging" 6 | 7 | on: 8 | push: # We run tests on non-tagged pushes to master 9 | tags: '' 10 | branches: master 11 | paths: 12 | - 'packaging/msi/*' 13 | - 'packaging/nfpm/*' 14 | - 'packaging/icon@48w.ico' 15 | - '.github/workflows/packaging.yaml' 16 | - 'Makefile' 17 | - 'Tools.mk' 18 | pull_request: # We also run tests on pull requests targeted at the master branch 19 | branches: master 20 | paths: 21 | - 'packaging/msi/*' 22 | - 'packaging/nfpm/*' 23 | - 'packaging/icon@48w.ico' 24 | - '.github/workflows/packaging.yaml' 25 | - 'Makefile' 26 | - 'Tools.mk' 27 | # workflow_dispatch will let us manually trigger the workflow from GitHub actions dashboard. 28 | # For example, you can try to build a branch without raising a pull request. 29 | # See https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/manually-running-a-workflow 30 | workflow_dispatch: 31 | 32 | defaults: 33 | run: # use bash for all operating systems unless overridden 34 | shell: bash 35 | 36 | jobs: 37 | packaging: 38 | name: "Test packaging build (${{ matrix.os }})" 39 | runs-on: windows-2022 40 | strategy: 41 | fail-fast: false # don't fail fast as sometimes failures are operating system specific 42 | 43 | steps: 44 | - name: "Setup msitools, wixtoolset, osslsigncode" 45 | run: | 46 | choco install osslsigncode -y 47 | choco install zip -y 48 | echo "$WIX\\bin" >> $GITHUB_PATH 49 | echo "${HOME}\\osslsigncode" >> $GITHUB_PATH 50 | 51 | env: # `gh` requires auth even on public releases 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: "Checkout" 55 | uses: actions/checkout@v4 56 | 57 | - name: "Cache Go" 58 | uses: actions/cache@v4 59 | with: 60 | path: ~/go/pkg/mod 61 | # go.mod for go release version, go.sum for modules used, and Tools.mk for 'go run' tools 62 | key: test-${{ runner.os }}-go-${{ hashFiles('go.mod', 'go.sum', 'Tools.mk') }} 63 | restore-keys: test-${{ runner.os }}-go- 64 | 65 | - name: "Build packages (Windows Installer, Debian, RPM)" 66 | run: make dist 67 | 68 | # This tests the manifest via yamllint because validation via winget requires too much setup. 69 | # See https://github.com/microsoft/winget-cli/issues/754#issuecomment-896475895 70 | - name: "Test winget manifest generation" 71 | run: | 72 | ./packaging/msi/winget_manifest.sh > Tetrate.func-e.yaml 73 | yamllint -sd '{extends: default, rules: {line-length: disable}}' Tetrate.func-e.yaml 74 | 75 | 76 | # In order to share the built artifacts in the subsequent tests, we use cache instead of actions/upload-artifacts. 77 | # The reason is that upload-artifacts are not globally consistent and sometimes pre_release_test won't be able to 78 | # find the artifacts uploaded here. See https://github.com/actions/upload-artifact/issues/21 for more context. 79 | # Downside of this is that, we pressure the cache capacity set per repository. We delete all caches created 80 | # on PRs on close. See .github/workflows/clear_cache.yaml. On main branch, in any way this cache will be deleted 81 | # in 7 days, also this at most a few MB, so this won't be an issue. 82 | - uses: actions/cache@v4 83 | id: cache 84 | with: 85 | # Use share the cache containing archives across OSes. 86 | enableCrossOsArchive: true 87 | # Note: this creates a cache per run. 88 | key: release-artifacts-${{ github.run_id }} 89 | path: 90 | dist/ 91 | 92 | # pre_release_test tests the artifacts built by pre_release in the OS dependent way. 93 | pre_release_test: 94 | needs: pre_release 95 | name: Pre-release test (${{ matrix.os }}) 96 | runs-on: ${{ matrix.os }} 97 | strategy: 98 | fail-fast: false # don't fail fast as sometimes failures are arch/OS specific 99 | matrix: 100 | os: [ubuntu-22.04, macos-12, windows-2022] 101 | 102 | steps: 103 | - uses: actions/checkout@v4 104 | 105 | - uses: actions/cache@v4 106 | id: cache 107 | with: 108 | # We need this cache to run tests. 109 | fail-on-cache-miss: true 110 | enableCrossOsArchive: true 111 | key: release-artifacts-${{ github.run_id }} 112 | path: 113 | dist/ 114 | 115 | # This only checks the installer when built on Windows as it is simpler than switching OS. 116 | # refreshenv is from choco, and lets you reload ENV variables (used here for PATH). 117 | - name: "Test Windows Installer (Windows)" 118 | if: runner.os == 'Windows' 119 | run: call packaging\msi\verify_msi.cmd 120 | shell: cmd 121 | 122 | - name: "Test Debian package" 123 | if: runner.os == 'Linux' 124 | run: packaging/nfpm/verify_deb.sh 125 | 126 | - name: "Test RPM package (CentOS)" 127 | if: runner.os == 'Linux' 128 | run: docker run --rm -v $PWD:/work --entrypoint packaging/nfpm/verify_rpm.sh ${CENTOS_IMAGE} 129 | env: # CENTOS_IMAGE was built by internal-images.yaml 130 | CENTOS_IMAGE: ghcr.io/tetratelabs/func-e-internal:centos-9 131 | -------------------------------------------------------------------------------- /.github/workflows/release_notes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ue 2 | # 3 | # This script generates the release notes "func-e" for a specific release tag. 4 | # .github/workflows/release_notes.sh v1.3.0 5 | 6 | tag=$1 7 | prior_tag=$(git tag -l 'v*'|sed "/${tag}/,+10d"|tail -1) 8 | if [ -n "${prior_tag}" ]; then 9 | range="${prior_tag}..${tag}" 10 | else 11 | range=${tag} 12 | fi 13 | 14 | git config log.mailmap true 15 | changelog=$(git log --format='%h %s %aN, %(trailers:key=co-authored-by)' "${range}") 16 | 17 | # strip the v off the tag name more shell portable than ${tag:1} 18 | version=$(echo "${tag}" | cut -c2-100) || exit 1 19 | cat < section "Tags". 101 | enabled-tags: [ "performance" ] 102 | 103 | settings: # settings passed to gocritic 104 | captLocal: # must be valid enabled check name 105 | paramsOnly: true 106 | rangeValCopy: 107 | sizeThreshold: 32 108 | 109 | issues: 110 | # List of regexps of issue texts to exclude, empty list by default. 111 | # But independently from this option we use default exclude patterns, 112 | # it can be disabled by `exclude-use-default: false`. To list all 113 | # excluded by default patterns execute `golangci-lint run --help` 114 | exclude: [ ] 115 | 116 | # Excluding configuration per-path, per-linter, per-text and per-source 117 | exclude-rules: 118 | # Exclude some linters from running on tests files. 119 | - path: _test\.go 120 | linters: 121 | - errcheck 122 | - dupl 123 | - gosec 124 | - lll 125 | 126 | # Independently from option `exclude` we use default exclude patterns, 127 | # it can be disabled by this option. To list all 128 | # excluded by default patterns execute `golangci-lint run --help`. 129 | # Default value for this option is true. 130 | exclude-use-default: false 131 | 132 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 133 | max-issues-per-linter: 0 134 | 135 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 136 | max-same-issues: 0 137 | -------------------------------------------------------------------------------- /.licenserignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | 3 | .editorconfig 4 | codecov.yml 5 | 6 | /LICENSE 7 | 8 | go.mod 9 | go.sum 10 | 11 | *.pem 12 | 13 | .bingo 14 | .idea 15 | 16 | last_known_envoy.txt 17 | 18 | testdata/ 19 | site/*.json 20 | e2e/*.yaml 21 | 22 | netlify.toml 23 | site/ 24 | 25 | *.png 26 | *.ico 27 | *.p12 28 | 29 | # until https://github.com/liamawhite/licenser/issues/13 30 | *.wxs 31 | # https://github.com/liamawhite/licenser/issues/14 32 | *.ps1 33 | *.cmd 34 | .mailmap 35 | Tools.mk 36 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Adrian Cole 2 | Adrian Cole <64215+codefromthecrypt@users.noreply.github.com> 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.travis-ci.com/user/reference/overview/#for-a-particular-travisyml-configuration 2 | # run `travis lint` prior to check-in! 3 | os: linux # required for arch different than amd64 4 | arch: arm64-graviton2 # we only only test archs not already tested with GH actions 5 | group: edge # required for arm64-graviton2 6 | virt: lxd # faster starting 7 | language: bash 8 | services: docker 9 | 10 | cache: 11 | directories: # ~/.func-e/versions is cached so that we only re-download once: for TestFuncEInstall 12 | - $HOME/.func-e/versions 13 | - $HOME/go/pkg/mod 14 | 15 | git: 16 | depth: false # TRAVIS_COMMIT_RANGE requires full commit history. 17 | 18 | if: (type = push AND branch = master) OR type = pull_request 19 | 20 | before_install: | # Prevent test build of a documentation or GitHub Actions only change. 21 | if [ -n "${TRAVIS_COMMIT_RANGE}" ] && ! git diff --name-only "${TRAVIS_COMMIT_RANGE}" -- \ 22 | grep -qvE '(\.md)$|^(packaging\/)$|^(site\/)|^(netlify.toml)|^(.github\/)'; then 23 | echo "Stopping job as changes are tested with GitHub Actions" 24 | travis_terminate 0 25 | fi 26 | make check || travis_terminate 1 27 | 28 | env: # CENTOS_IMAGE and UBUNTU_IMAGE were built by internal-images.yaml; E2E_FUNC_E_PATH was built via `make dist` 29 | global: 30 | - CENTOS_IMAGE=ghcr.io/tetratelabs/func-e-internal:centos-8 31 | - UBUNTU_IMAGE=ghcr.io/tetratelabs/func-e-internal:ubuntu-20.04 32 | # Read/Write volume mounts for ~/go/pkg/mod and ~/.func-e assist in caching while $PWD shares build outputs 33 | - DOCKER_ARGS="docker run --rm -v $HOME/go/pkg/mod:/home/runner/go/pkg/mod:rw -v $HOME/.func-e/versions:/home/runner/.func-e/versions:rw -v $PWD:/work:rw" 34 | - E2E_FUNC_E_PATH=build/func-e_linux_arm64 35 | 36 | script: # Since files below are only written by Docker, there should be no uid/gid conflicts. 37 | # Obviate tests on bad commit (Ubuntu) 38 | - ${DOCKER_ARGS} ${UBUNTU_IMAGE} check 39 | # Build the `func-e` binary (Ubuntu) 40 | - ${DOCKER_ARGS} ${UBUNTU_IMAGE} dist 41 | # Run e2e tests using the `func-e` binary (Ubuntu) 42 | - ${DOCKER_ARGS} ${UBUNTU_IMAGE} -o ${E2E_FUNC_E_PATH}/func-e e2e 43 | # Run e2e tests using the `func-e` binary (CentOS) 44 | - ${DOCKER_ARGS} ${CENTOS_IMAGE} -o ${E2E_FUNC_E_PATH}/func-e e2e 45 | # Test Debian package (Ubuntu) 46 | - ${DOCKER_ARGS} --entrypoint packaging/nfpm/verify_deb.sh ${UBUNTU_IMAGE} 47 | # Test RPM package (CentOS) 48 | - ${DOCKER_ARGS} --entrypoint packaging/nfpm/verify_rpm.sh ${CENTOS_IMAGE} 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions from the community. Please read the following guidelines carefully to maximize the chances of your PR being merged. 4 | 5 | ## Coding Style 6 | 7 | - The code is linted using a relatively stringent [golang-ci config](./.golangci.yml). To run this linter (and a few others) use run `make check`. To format your files, you can run `make format`. 8 | - We follow standard Go table-driven tests and use the [`testify/require`](https://github.com/stretchr/testify#require-package) library to assert correctness. To verify all tests pass, you can run `make test`. 9 | 10 | ## DCO 11 | 12 | We require DCO signoff line in every commit to this repo. 13 | 14 | The sign-off is a simple line at the end of the explanation for the 15 | patch, which certifies that you wrote it or otherwise have the right to 16 | pass it on as an open-source patch. The rules are pretty simple: if you 17 | can certify the below (from 18 | [developercertificate.org](https://developercertificate.org/)): 19 | 20 | ``` 21 | Developer Certificate of Origin 22 | Version 1.1 23 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 24 | 660 York Street, Suite 102, 25 | San Francisco, CA 94110 USA 26 | Everyone is permitted to copy and distribute verbatim copies of this 27 | license document, but changing it is not allowed. 28 | Developer's Certificate of Origin 1.1 29 | By making a contribution to this project, I certify that: 30 | (a) The contribution was created in whole or in part by me and I 31 | have the right to submit it under the open source license 32 | indicated in the file; or 33 | (b) The contribution is based upon previous work that, to the best 34 | of my knowledge, is covered under an appropriate open source 35 | license and I have the right under that license to submit that 36 | work with modifications, whether created in whole or in part 37 | by me, under the same open source license (unless I am 38 | permitted to submit under a different license), as indicated 39 | in the file; or 40 | (c) The contribution was provided directly to me by some other 41 | person who certified (a), (b) or (c) and I have not modified 42 | it. 43 | (d) I understand and agree that this project and the contribution 44 | are public and that a record of the contribution (including all 45 | personal information I submit with it, including my sign-off) is 46 | maintained indefinitely and may be redistributed consistent with 47 | this project or the open source license(s) involved. 48 | ``` 49 | 50 | then you just add a line to every git commit message: 51 | 52 | Signed-off-by: Joe Smith 53 | 54 | using your real name (sorry, no pseudonyms or anonymous contributions.) 55 | 56 | You can add the sign off when creating the git commit via `git commit -s`. 57 | 58 | ## Code Reviews 59 | 60 | * Indicate the priority of each comment, following this 61 | [feedback ladder](https://www.netlify.com/blog/2020/03/05/feedback-ladders-how-we-encode-code-reviews-at-netlify/). 62 | If none was indicated it will be treated as `[dust]`. 63 | * A single approval is sufficient to merge, except when the change cuts 64 | across several components; then it should be approved by at least one owner 65 | of each component. If a reviewer asks for changes in a PR they should be 66 | addressed before the PR is merged, even if another reviewer has already 67 | approved the PR. 68 | * During the review, address the comments and commit the changes _without_ squashing the commits. 69 | This facilitates incremental reviews since the reviewer does not go through all the code again to 70 | find out what has changed since the last review. 71 | -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Developer Documentation 2 | 3 | ## How To 4 | 5 | ### How to Build 6 | 7 | Make sure you are running the same version of go as indicated in [go.mod](go.mod). 8 | 9 | #### func-e binary 10 | 11 | Run: 12 | ```shell 13 | make build 14 | ``` 15 | which will produce a binary at `./build/bin/$(go env GOOS)/$(go env GOARCH)/func-e`. 16 | 17 | ### How to run Unit Tests 18 | 19 | Run: 20 | ```shell 21 | make test 22 | ``` 23 | 24 | ### How to collect Test Coverage 25 | 26 | Run: 27 | ```shell 28 | make coverage 29 | ``` 30 | 31 | ### How to run end-to-end Tests 32 | 33 | See [test/e2e](e2e) for how to develop or run end-to-end tests 34 | 35 | ### How to test the website 36 | 37 | Run below, then view with http://localhost:1313/ 38 | ```shell 39 | make site 40 | ``` 41 | 42 | ### How to generate release assets 43 | 44 | To generate release assets, run the below: 45 | ```shell 46 | make dist 47 | ``` 48 | 49 | The contents will be in the 'dist/' folder and include the same files as a 50 | [release](https://github.com/tetratelabs/func-e/releases) would, except 51 | signatures would not be the same as production. 52 | 53 | Note: this step requires prerequisites for Windows packaging to work. Look at 54 | [msi.yaml](.github/workflows/msi.yaml) for what's needed per-platform. 55 | -------------------------------------------------------------------------------- /RATIONALE.md: -------------------------------------------------------------------------------- 1 | # Notable rationale of func-e 2 | 3 | ## Why do we use Travis and GitHub Actions instead of only GitHub Actions? 4 | We use Travis to run CentOS integration tests on arm64 until [GitHub Actions supports it](https://github.com/actions/virtual-environments/issues/2552). 5 | 6 | This is an alternative to using emulation instead. Using emulation (ex via `setup-qemu-action` and 7 | `setup-buildx-action`) re-introduces problems we eliminated with Travis. For example, not only would 8 | runners take longer to execute (as emulation is slower than native arch), but there is more setup, 9 | and that setup executes on every change. This setup takes time and uses rate-limited registries. It 10 | also introduces potential for compatibility issues when we move off Docker due to its recent 11 | [licensing changes](https://www.docker.com/pricing). 12 | 13 | It is true that Travis has a different syntax and that also could fail. However, the risk of 14 | failure is low. What we gain from running end-to-end or packaging tests on Travis is knowledge that 15 | we broke our [Makefile](Makefile) or that there's an unknown new dependency of Envoy® (such as a 16 | change to the floor version of glibc). While these are unlikely to occur, running tests are still 17 | important. 18 | 19 | The only place we use emulation is [publishing internal images](.github/workflows/internal-images.yml). 20 | This is done for convenience and occurs once per day, so duration and rate limiting effects are 21 | not important. If something breaks here, the existing images would become stale, so it isn't as 22 | much an emergency as if we put emulation in the critical path (ex in PR tests.) 23 | 24 | At the point when GitHub Actions supports free arm64 runners, we can simplify by removing Travis. 25 | Azure DevOps Pipelines already supports arm64, so it is possible GitHub Actions will in the future. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/tetratelabs/func-e/workflows/build/badge.svg)](https://github.com/tetratelabs/func-e) 2 | [![Coverage](https://codecov.io/gh/tetratelabs/func-e/branch/master/graph/badge.svg)](https://codecov.io/gh/tetratelabs/func-e) 3 | [![Report Card](https://goreportcard.com/badge/github.com/tetratelabs/func-e)](https://goreportcard.com/report/github.com/tetratelabs/func-e) 4 | [![Downloads](https://img.shields.io/github/downloads/tetratelabs/func-e/total.svg)](https://github.com/tetratelabs/func-e/releases) 5 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) 6 | 7 | # func-e 8 | 9 | func-e (pronounced funky) makes running [Envoy®](https://www.envoyproxy.io/) easy. 10 | 11 | The quickest way to try the command-line interface is an in-lined configuration. 12 | ```bash 13 | # Download the latest release as /usr/local/bin/func-e https://github.com/tetratelabs/func-e/releases 14 | $ curl https://func-e.io/install.sh | bash -s -- -b /usr/local/bin 15 | # Run the admin server on http://localhost:9901 16 | $ func-e run --config-yaml "admin: {address: {socket_address: {address: '127.0.0.1', port_value: 9901}}}" 17 | ``` 18 | 19 | Run `func-e help` or have a look at the [Usage Docs](USAGE.md) for more information. 20 | 21 | ----- 22 | Envoy® is a registered trademark of The Linux Foundation in the United States and/or other countries 23 | -------------------------------------------------------------------------------- /Tools.mk: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Tetrate 2 | # Licensed under the Apache License, Version 2.0 (the "License") 3 | 4 | golangci_lint := github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.6 5 | goimports := golang.org/x/tools/cmd/goimports@v0.5.0 6 | # sync this with netlify.toml! 7 | hugo := github.com/gohugoio/hugo@v0.109.0 8 | licenser := github.com/liamawhite/licenser@v0.6.0 9 | nfpm := github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.23.0 10 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # func-e Overview 2 | To run Envoy, execute `func-e run -c your_envoy_config.yaml`. This 3 | downloads and installs the latest version of Envoy for you. 4 | 5 | To list versions of Envoy you can use, execute `func-e versions -a`. To 6 | choose one, invoke `func-e use 1.33.0`. This installs into 7 | `$FUNC_E_HOME/versions/1.33.0`, if not already present. You may also use 8 | minor version, such as `func-e use 1.33`. 9 | 10 | You may want to override `$ENVOY_VERSIONS_URL` to supply custom builds or 11 | otherwise control the source of Envoy binaries. When overriding, validate 12 | your JSON first: https://archive.tetratelabs.io/release-versions-schema.json 13 | 14 | Advanced: 15 | `FUNC_E_PLATFORM` overrides the host OS and architecture of Envoy binaries. 16 | This is used when emulating another platform, e.g. x86 on Apple Silicon M1. 17 | Note: Changing the OS value can cause problems as Envoy has dependencies, 18 | such as glibc. This value must be constant within a `$FUNC_E_HOME`. 19 | 20 | # Commands 21 | 22 | | Name | Usage | 23 | | ---- | ----- | 24 | | help | Shows how to use a [command] | 25 | | run | Run Envoy with the given [arguments...] until interrupted | 26 | | versions | List Envoy versions | 27 | | use | Sets the current [version] used by the "run" command | 28 | | which | Prints the path to the Envoy binary used by the "run" command | 29 | | --version, -v | Print the version of func-e | 30 | 31 | # Environment Variables 32 | 33 | | Name | Usage | Default | 34 | | ---- | ----- | ------- | 35 | | FUNC_E_HOME | func-e home directory (location of installed versions and run archives) | ${HOME}/.func-e | 36 | | ENVOY_VERSIONS_URL | URL of Envoy versions JSON | https://archive.tetratelabs.io/envoy/envoy-versions.json | 37 | | FUNC_E_PLATFORM | the host OS and architecture of Envoy binaries. Ex. darwin/arm64 | $GOOS/$GOARCH | 38 | -------------------------------------------------------------------------------- /api/run.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package api allows go projects to use func-e as a library. 16 | package api 17 | 18 | import ( 19 | "context" 20 | "io" 21 | "os" 22 | "runtime" 23 | 24 | "github.com/tetratelabs/func-e/internal/cmd" 25 | "github.com/tetratelabs/func-e/internal/globals" 26 | "github.com/tetratelabs/func-e/internal/version" 27 | ) 28 | 29 | // HomeDir is an absolute path which most importantly contains "versions" 30 | // installed from EnvoyVersionsURL. Defaults to "${HOME}/.func-e" 31 | func HomeDir(homeDir string) RunOption { 32 | return func(o *runOpts) { 33 | o.homeDir = homeDir 34 | } 35 | } 36 | 37 | // EnvoyVersionsURL is the path to the envoy-versions.json. 38 | // Defaults to "https://archive.tetratelabs.io/envoy/envoy-versions.json" 39 | func EnvoyVersionsURL(envoyVersionsURL string) RunOption { 40 | return func(o *runOpts) { 41 | o.envoyVersionsURL = envoyVersionsURL 42 | } 43 | } 44 | 45 | // EnvoyVersion overrides the version of Envoy to run. Defaults to the 46 | // contents of "$HomeDir/versions/version". 47 | // 48 | // When that file is missing, it is generated from ".latestVersion" from the 49 | // EnvoyVersionsURL. Its value can be in full version major.minor.patch format, 50 | // e.g. 1.18.1 or without patch component, major.minor, e.g. 1.18. 51 | func EnvoyVersion(envoyVersion string) RunOption { 52 | return func(o *runOpts) { 53 | o.envoyVersion = envoyVersion 54 | } 55 | } 56 | 57 | // Out is where status messages are written. Defaults to os.Stdout 58 | func Out(out io.Writer) RunOption { 59 | return func(o *runOpts) { 60 | o.out = out 61 | } 62 | } 63 | 64 | // RunOption is configuration for Run. 65 | type RunOption func(*runOpts) 66 | 67 | type runOpts struct { 68 | homeDir string 69 | envoyVersion string 70 | envoyVersionsURL string 71 | out io.Writer 72 | } 73 | 74 | // Run downloads Envoy and runs it as a process with the arguments 75 | // passed to it. Use RunOption for configuration options. 76 | // 77 | // This blocks until the context is done or the process exits. The error might be 78 | // context.Canceled if the context is done or an error from the process. 79 | func Run(ctx context.Context, args []string, options ...RunOption) error { 80 | ro := &runOpts{ 81 | homeDir: globals.DefaultHomeDir, 82 | envoyVersion: "", // default to lookup 83 | envoyVersionsURL: globals.DefaultEnvoyVersionsURL, 84 | out: os.Stdout, 85 | } 86 | for _, option := range options { 87 | option(ro) 88 | } 89 | 90 | o := globals.GlobalOpts{ 91 | HomeDir: ro.homeDir, 92 | EnvoyVersion: version.PatchVersion(ro.envoyVersion), 93 | EnvoyVersionsURL: ro.envoyVersion, 94 | Out: ro.out, 95 | } 96 | 97 | funcECmd := cmd.NewApp(&o) 98 | funcERunArgs := []string{"func-e", "--platform", runtime.GOOS + "/" + runtime.GOARCH, "run"} 99 | funcERunArgs = append(funcERunArgs, args...) 100 | return funcECmd.RunContext(ctx, funcERunArgs) // This will block until the context is done or the process exits. 101 | } 102 | -------------------------------------------------------------------------------- /api/run_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "os" 21 | "path/filepath" 22 | "strconv" 23 | "testing" 24 | "time" 25 | 26 | "github.com/stretchr/testify/require" 27 | 28 | "github.com/tetratelabs/func-e/internal/test" 29 | "github.com/tetratelabs/func-e/internal/version" 30 | ) 31 | 32 | var ( 33 | runArgs = []string{"--version"} 34 | ) 35 | 36 | func TestRunWithCtxDone(t *testing.T) { 37 | tmpDir := t.TempDir() 38 | envoyVersion := version.LastKnownEnvoy 39 | versionsServer := test.RequireEnvoyVersionsTestServer(t, envoyVersion) 40 | defer versionsServer.Close() 41 | envoyVersionsURL := versionsServer.URL + "/envoy-versions.json" 42 | 43 | err := Run(t.Context(), runArgs, HomeDir(tmpDir), EnvoyVersionsURL(envoyVersionsURL)) 44 | require.NoError(t, err) 45 | // Run the same test multiple times to ensure that the Envoy process is cleaned up properly with the context cancellation. 46 | for i := range 10 { 47 | t.Run(strconv.Itoa(i), func(t *testing.T) { 48 | start := time.Now() 49 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 50 | defer cancel() 51 | // This will return right after the context is done. 52 | err := Run(ctx, []string{ 53 | "--log-level", "info", 54 | "--config-yaml", "admin: {address: {socket_address: {address: '127.0.0.1', port_value: 9901}}}", 55 | }, HomeDir(tmpDir), EnvoyVersionsURL(envoyVersionsURL)) 56 | require.Greater(t, time.Since(start).Seconds(), 2.0) 57 | require.NoError(t, err) // If the address is already in use, the exit code will be 1. 58 | }) 59 | } 60 | } 61 | 62 | func TestRunToCompletion(t *testing.T) { 63 | 64 | tmpDir := t.TempDir() 65 | envoyVersion := version.LastKnownEnvoy 66 | versionsServer := test.RequireEnvoyVersionsTestServer(t, envoyVersion) 67 | defer versionsServer.Close() 68 | envoyVersionsURL := versionsServer.URL + "/envoy-versions.json" 69 | b := bytes.NewBufferString("") 70 | 71 | require.Equal(t, 0, b.Len()) 72 | 73 | ctx := context.Background() 74 | // Set a large ctx timeout value 75 | ctx, cancel := context.WithTimeout(ctx, 1000*time.Minute) 76 | defer cancel() 77 | 78 | err := Run(ctx, runArgs, Out(b), HomeDir(tmpDir), EnvoyVersionsURL(envoyVersionsURL)) 79 | require.NoError(t, err) 80 | 81 | require.NotEqual(t, 0, b.Len()) 82 | _, err = os.Stat(filepath.Join(tmpDir, "versions")) 83 | require.NoError(t, err) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # Codecov for main is visible here https://app.codecov.io/gh/tetratelabs/func-e 2 | 3 | # We use codecov only as a UI, so we disable PR comments. 4 | # See https://docs.codecov.com/docs/pull-request-comments 5 | comment: false 6 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # func-e End-to-end (e2e) tests 2 | 3 | This directory holds the end-to-end tests for `func-e`. 4 | 5 | By default, end-to-end (e2e) tests verify a `func-e` binary built from [main.go](../main.go). 6 | 7 | ## Using native go commands: 8 | End-to-end tests default to look for `func-e` (or `func-e.exe` in Windows), in the project root (current directory). 9 | 10 | ```bash 11 | go build --ldflags '-s -w' . 12 | go test -parallel 1 -v -failfast ./e2e 13 | ``` 14 | 15 | ## Using `make` 16 | When run via `make`, `func-e` is built on-demand by `$(current_binary)` target (same as `make build`). 17 | Ex. `$PWD/build/func-e_darwin_amd64/func-e` 18 | 19 | It is also a good idea to override `FUNC_E_HOME` when running e2e, since by default it uses `$HOME/.func-e`. 20 | 21 | ```bash 22 | FUNC_E_HOME=/tmp/test make e2e 23 | ``` 24 | 25 | ## Envoy version list 26 | If the `func-e` version is a snapshot and "envoy-versions.json" exists, tests run against the local. This allows local 27 | development and pull requests to verify changes not yet [published](https://archive.tetratelabs.io/envoy/envoy-versions.json) 28 | or those that effect the [schema](https://archive.tetratelabs.io/release-versions-schema.json). 29 | 30 | ## Version of Envoy under test 31 | The envoy version used in tests default to [/internal/version/last_known_envoy.txt](../internal/version/last_known_envoy.txt). 32 | 33 | ## Development Notes 34 | 35 | ### Don't share add code to /internal only used here 36 | This is an end-to-end test of the `func-e` binary: it is easy to get confused about what is happening when some code 37 | is in the binary and other shared. To avoid confusion, only use code in [/internal](../internal) on an exception basis. 38 | 39 | We historically added functions into main only for e2e and left them after they became unused. Adding code into 40 | /internal also effects main code health statistics. Hence, we treat e2e as a separate project even though it shares a 41 | [go.mod](../go.mod) with /internal. Specifically, we don't add code into /internal which only needs to exist here. 42 | 43 | ### Be careful when adding dependencies 44 | Currently, e2e shares [go.mod](../go.mod) with [/internal](../internal). This is for simplification in build config and 45 | details such as linters. However, we carry a risk of dependencies used here ending up accidentally used in /internal. 46 | The IDE will think this is the same project as /internal and suggest libraries with auto-complete. 47 | 48 | For example, if /internal used "archiver/v3" accidentally, it would bloat the binary by almost 3MB. For this reason, 49 | please be careful and only add dependencies absolutely needed. 50 | 51 | If go.mod ever supports test-only scope, this risk would go away, because IDEs could hide test dependencies from main 52 | auto-complete suggestions. However, it is unlikely go will allow a test scope: https://github.com/golang/go/issues/26913. 53 | -------------------------------------------------------------------------------- /e2e/admin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e2e 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net" 23 | "net/http" 24 | ) 25 | 26 | // newAdminClient returns a new client for Envoy Admin API. 27 | func newAdminClient(address string) (*adminClient, error) { 28 | host, port, err := net.SplitHostPort(address) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &adminClient{baseURL: fmt.Sprintf("http://%s:%s", host, port)}, nil 33 | } 34 | 35 | type adminClient struct { 36 | baseURL string 37 | } 38 | 39 | func (c *adminClient) isReady(ctx context.Context) bool { 40 | _, err := httpGet(ctx, c.baseURL+"/ready") 41 | return err == nil 42 | } 43 | 44 | func (c *adminClient) getMainListenerURL(ctx context.Context) (string, error) { 45 | var s map[string]interface{} 46 | if err := c.getJSON(ctx, "/listeners", &s); err != nil { 47 | return "", err 48 | } 49 | 50 | // The json structure is deep, so parsing instead of many nested structs 51 | for _, s := range s["listener_statuses"].([]interface{}) { 52 | l := s.(map[string]interface{}) 53 | if l["name"] != "main" { 54 | continue 55 | } 56 | port := l["local_address"].(map[string]interface{})["socket_address"].(map[string]interface{})["port_value"] 57 | return fmt.Sprintf("http://127.0.0.1:%d", int(port.(float64))), nil 58 | } 59 | return "", fmt.Errorf("didn't find main port in %+v", s) 60 | } 61 | 62 | func (c *adminClient) getJSON(ctx context.Context, path string, v interface{}) error { 63 | body, err := httpGet(ctx, c.baseURL+path+"?format=json") 64 | if err != nil { 65 | return err 66 | } 67 | return json.Unmarshal(body, v) 68 | } 69 | 70 | func httpGet(ctx context.Context, url string) ([]byte, error) { 71 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 72 | if err != nil { 73 | return nil, err 74 | } 75 | resp, err := http.DefaultClient.Do(req) 76 | if err != nil { 77 | return nil, err 78 | } 79 | defer resp.Body.Close() 80 | if resp.StatusCode != http.StatusOK { 81 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 82 | } 83 | return io.ReadAll(resp.Body) 84 | } 85 | -------------------------------------------------------------------------------- /e2e/func-e_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e2e 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestFuncEVersion(t *testing.T) { 24 | t.Parallel() 25 | 26 | stdout, stderr, err := funcEExec("--version") 27 | 28 | require.Regexp(t, `^func-e version ([^\s]+)\r?\n$`, stdout) 29 | require.Empty(t, stderr) 30 | require.NoError(t, err) 31 | } 32 | -------------------------------------------------------------------------------- /e2e/func-e_versions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e2e 16 | 17 | import ( 18 | "bufio" 19 | "strings" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/tetratelabs/func-e/internal/moreos" 25 | "github.com/tetratelabs/func-e/internal/version" 26 | ) 27 | 28 | func TestFuncEVersions_NothingYet(t *testing.T) { 29 | homeDir := t.TempDir() 30 | 31 | stdout, stderr, err := funcEExec("--home-dir", homeDir, "versions") 32 | 33 | require.NoError(t, err) 34 | require.Empty(t, stdout) 35 | require.Empty(t, stderr) 36 | } 37 | 38 | func TestFuncEVersions(t *testing.T) { 39 | t.Parallel() 40 | 41 | stdout, stderr, err := funcEExec("versions") 42 | 43 | // Depending on ~/func-e/version, what's selected may not be the latest version or even installed at all. 44 | require.Regexp(t, moreos.Sprintf("[ *] [1-9][0-9]*\\.[0-9]+\\.[0-9]+(_debug)? 202[1-9]-[01][0-9]-[0-3][0-9].*\n"), stdout) 45 | require.Empty(t, stderr) 46 | require.NoError(t, err) 47 | } 48 | 49 | func TestFuncEVersions_All(t *testing.T) { 50 | t.Parallel() 51 | 52 | stdout, stderr, err := funcEExec("versions", "-a") 53 | 54 | require.Regexp(t, moreos.Sprintf("[ *] %s 202[1-9]-[01][0-9]-[0-3][0-9].*\n", version.LastKnownEnvoy), stdout) 55 | require.Empty(t, stderr) 56 | require.NoError(t, err) 57 | } 58 | 59 | func TestFuncEVersions_AllIncludesInstalled(t *testing.T) { 60 | t.Parallel() 61 | 62 | // Cheap test that one includes the other. It doesn't actually parse the output, but the above tests prove the 63 | // latest version is in each deviation. 64 | allVersions, _, err := funcEExec("versions", "-a") 65 | require.NoError(t, err) 66 | installedVersions, _, err := funcEExec("versions") 67 | require.NoError(t, err) 68 | 69 | require.Greater(t, countLines(allVersions), countLines(installedVersions), "expected more versions available than installed") 70 | } 71 | 72 | func countLines(stdout string) (count int) { 73 | s := bufio.NewScanner(strings.NewReader(stdout)) 74 | for s.Scan() { 75 | count++ 76 | } 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /e2e/func-e_which_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e2e 16 | 17 | import ( 18 | "path/filepath" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/tetratelabs/func-e/internal/moreos" 24 | "github.com/tetratelabs/func-e/internal/version" 25 | ) 26 | 27 | // TestFuncEWhich ensures the command can show the current version in use. This can't use version.LastKnownEnvoy without 28 | // explicitly downloading it first, because the latest version on Linux and Windows can be ahead of that due to routine 29 | // lagging on Homebrew maintenance (OS/x), or lag in someone re-releasing on archive-envoy after Homebrew is updated. 30 | func TestFuncEWhich(t *testing.T) { // not parallel as it can end up downloading concurrently 31 | // Explicitly issue "use" for the last known version to ensure when latest is ahead of this, the test doesn't fail. 32 | _, _, err := funcEExec("use", version.LastKnownEnvoy.String()) 33 | require.NoError(t, err) 34 | 35 | stdout, stderr, err := funcEExec("which") 36 | relativeEnvoyBin := filepath.Join("versions", version.LastKnownEnvoy.String(), "bin", "envoy"+moreos.Exe) 37 | require.Contains(t, stdout, moreos.Sprintf("%s\n", relativeEnvoyBin)) 38 | require.Empty(t, stderr) 39 | require.NoError(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /e2e/static-filesystem.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | address: 3 | socket_address: 4 | address: '127.0.0.1' 5 | port_value: 0 6 | 7 | static_resources: 8 | listeners: 9 | - name: main 10 | address: 11 | socket_address: 12 | address: '127.0.0.1' 13 | port_value: 0 14 | filter_chains: 15 | - filters: 16 | - name: envoy.http_connection_manager 17 | typed_config: 18 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 19 | stat_prefix: ingress_http 20 | codec_type: auto 21 | route_config: 22 | name: local_route 23 | virtual_hosts: 24 | - name: local_service 25 | domains: 26 | - "*" 27 | routes: 28 | - match: 29 | prefix: "/" 30 | direct_response: 31 | status: 200 32 | body: 33 | filename: "response.txt" 34 | http_filters: 35 | - name: envoy.filters.http.router 36 | typed_config: 37 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/func-e 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/shirou/gopsutil/v3 v3.24.5 7 | github.com/stretchr/testify v1.10.0 8 | github.com/ulikunitz/xz v0.5.12 9 | github.com/urfave/cli/v2 v2.27.6 10 | golang.org/x/sync v0.12.0 11 | ) 12 | 13 | require ( 14 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-ole/go-ole v1.2.6 // indirect 17 | github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect 20 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 21 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 22 | github.com/tklauser/go-sysconf v0.3.12 // indirect 23 | github.com/tklauser/numcpus v0.6.1 // indirect 24 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 25 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 26 | golang.org/x/sys v0.20.0 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 6 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 7 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= 11 | github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= 15 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 16 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 17 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 18 | github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= 19 | github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= 20 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 21 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 22 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 23 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 24 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 25 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 27 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 28 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 29 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 30 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 31 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 32 | github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= 33 | github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 34 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 35 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 36 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 37 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 38 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 39 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 40 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 45 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /internal/cmd/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | // NewValidationError generates an error with a given format string. 18 | // As noted on ValidationError, this is used by main.go to tell the difference between a validation failure vs a runtime 19 | // error. We don't want to clutter output with help suggestions if Envoy failed due to a runtime concern. 20 | func NewValidationError(format string) error { 21 | return &ValidationError{format} 22 | } 23 | 24 | // ValidationError is a marker of a validation error vs an execution one. 25 | type ValidationError struct { 26 | string 27 | } 28 | 29 | // Error implements the error interface. 30 | func (e *ValidationError) Error() string { 31 | return e.string 32 | } 33 | -------------------------------------------------------------------------------- /internal/cmd/help_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd_test 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/require" 26 | 27 | "github.com/tetratelabs/func-e/internal/globals" 28 | "github.com/tetratelabs/func-e/internal/moreos" 29 | "github.com/tetratelabs/func-e/internal/version" 30 | ) 31 | 32 | func TestFuncEHelp(t *testing.T) { 33 | for _, command := range []string{"", "use", "versions", "run", "which"} { 34 | command := command 35 | t.Run(command, func(t *testing.T) { 36 | c, stdout, _ := newApp(&globals.GlobalOpts{Version: "1.0"}) 37 | args := []string{"func-e"} 38 | if command != "" { 39 | args = []string{"func-e", "help", command} 40 | } 41 | require.NoError(t, c.Run(args)) 42 | 43 | expected := "func-e_help.txt" 44 | if command != "" { 45 | expected = fmt.Sprintf("func-e_%s_help.txt", command) 46 | } 47 | bytes, err := os.ReadFile(filepath.Join("testdata", expected)) 48 | require.NoError(t, err) 49 | expectedStdout := string(bytes) 50 | expectedStdout = strings.ReplaceAll(expectedStdout, "1.99.0", version.LastKnownEnvoy.String()) 51 | expectedStdout = strings.ReplaceAll(expectedStdout, "1.99", version.LastKnownEnvoyMinor.String()) 52 | if runtime.GOOS == moreos.OSWindows { 53 | expectedStdout = strings.ReplaceAll(expectedStdout, "/", "\\") 54 | // As most maintainers don't use Windows, it is easier to revert piece-wise 55 | for _, original := range []string{ 56 | globals.DefaultEnvoyVersionsURL, 57 | globals.DefaultEnvoyVersionsSchemaURL, 58 | "darwin/arm64", 59 | "$GOOS/$GOARCH", 60 | } { 61 | toRevert := strings.ReplaceAll(original, "/", "\\") 62 | expectedStdout = strings.ReplaceAll(expectedStdout, toRevert, original) 63 | } 64 | } 65 | require.Equal(t, expectedStdout, stdout.String()) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/cmd/man_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "os" 19 | "runtime" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/tetratelabs/func-e/internal/globals" 25 | "github.com/tetratelabs/func-e/internal/moreos" 26 | ) 27 | 28 | const siteManpageFile = "../../packaging/nfpm/func-e.8" 29 | 30 | func TestManPageMatchesCommands(t *testing.T) { 31 | if runtime.GOOS == moreos.OSWindows { 32 | t.SkipNow() 33 | } 34 | 35 | app := NewApp(&globals.GlobalOpts{}) 36 | 37 | expected, err := app.ToMan() 38 | require.NoError(t, err) 39 | 40 | actual, err := os.ReadFile(siteManpageFile) 41 | require.NoError(t, err) 42 | require.Equal(t, expected, string(actual)) 43 | } 44 | -------------------------------------------------------------------------------- /internal/cmd/testdata/.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://github.com/mvdan/sh#shfmt 2 | [*.{txt,md}] 3 | insert_final_newline = false 4 | trim_trailing_whitespace = false 5 | -------------------------------------------------------------------------------- /internal/cmd/testdata/func-e_help.txt: -------------------------------------------------------------------------------- 1 | NAME: 2 | func-e - Install and run Envoy 3 | 4 | USAGE: 5 | To run Envoy, execute `func-e run -c your_envoy_config.yaml`. This 6 | downloads and installs the latest version of Envoy for you. 7 | 8 | To list versions of Envoy you can use, execute `func-e versions -a`. To 9 | choose one, invoke `func-e use 1.33.0`. This installs into 10 | `$FUNC_E_HOME/versions/1.33.0`, if not already present. You may also use 11 | minor version, such as `func-e use 1.33`. 12 | 13 | You may want to override `$ENVOY_VERSIONS_URL` to supply custom builds or 14 | otherwise control the source of Envoy binaries. When overriding, validate 15 | your JSON first: https://archive.tetratelabs.io/release-versions-schema.json 16 | 17 | Advanced: 18 | `FUNC_E_PLATFORM` overrides the host OS and architecture of Envoy binaries. 19 | This is used when emulating another platform, e.g. x86 on Apple Silicon M1. 20 | Note: Changing the OS value can cause problems as Envoy has dependencies, 21 | such as glibc. This value must be constant within a `$FUNC_E_HOME`. 22 | 23 | VERSION: 24 | 1.0 25 | 26 | COMMANDS: 27 | help Shows how to use a [command] 28 | run Run Envoy with the given [arguments...] until interrupted 29 | versions List Envoy versions 30 | use Sets the current [version] used by the "run" command 31 | which Prints the path to the Envoy binary used by the "run" command 32 | 33 | GLOBAL OPTIONS: 34 | --home-dir value func-e home directory (location of installed versions and run archives) (default: ${HOME}/.func-e) [$FUNC_E_HOME] 35 | --envoy-versions-url value URL of Envoy versions JSON (default: https://archive.tetratelabs.io/envoy/envoy-versions.json) [$ENVOY_VERSIONS_URL] 36 | --platform value the host OS and architecture of Envoy binaries. Ex. darwin/arm64 (default: $GOOS/$GOARCH) [$FUNC_E_PLATFORM] 37 | --version, -v print the version 38 | -------------------------------------------------------------------------------- /internal/cmd/testdata/func-e_run_help.txt: -------------------------------------------------------------------------------- 1 | NAME: 2 | func-e run - Run Envoy with the given [arguments...] until interrupted 3 | 4 | USAGE: 5 | func-e run [arguments...] 6 | 7 | DESCRIPTION: 8 | To run Envoy, execute `func-e run -c your_envoy_config.yaml`. 9 | 10 | The first version in the below is run, controllable by the "use" command: 11 | ``` 12 | $ENVOY_VERSION, $PWD/.envoy-version, $FUNC_E_HOME/version 13 | ``` 14 | The version to use is downloaded and installed, if necessary. 15 | 16 | Envoy interprets the '[arguments...]' and runs in the current working 17 | directory (aka $PWD) until func-e is interrupted (ex Ctrl+C, Ctrl+Break). 18 | 19 | Envoy's process ID and console output write to "envoy.pid", stdout.log" and 20 | "stderr.log" in the run directory (`$FUNC_E_HOME/runs/$epochtime`). 21 | When interrupted, shutdown hooks write files including network and process 22 | state. On exit, these archive into `$FUNC_E_HOME/runs/$epochtime.tar.gz` 23 | -------------------------------------------------------------------------------- /internal/cmd/testdata/func-e_use_help.txt: -------------------------------------------------------------------------------- 1 | NAME: 2 | func-e use - Sets the current [version] used by the "run" command 3 | 4 | USAGE: 5 | func-e use [version] 6 | 7 | DESCRIPTION: 8 | The '[version]' is from the "versions -a" command. 9 | The Envoy [version] installs on-demand into $FUNC_E_HOME/versions/[version] 10 | if needed. You may also exclude the patch component of the [version] 11 | to use the latest patch version or to download the binary if it is 12 | not already downloaded. 13 | 14 | This updates $PWD/.envoy-version or $FUNC_E_HOME/version with [version], 15 | depending on which is present. 16 | 17 | Example: 18 | $ func-e use 1.33.0 19 | $ func-e use 1.33 20 | -------------------------------------------------------------------------------- /internal/cmd/testdata/func-e_versions_help.txt: -------------------------------------------------------------------------------- 1 | NAME: 2 | func-e versions - List Envoy versions 3 | 4 | USAGE: 5 | func-e versions [command options] 6 | 7 | OPTIONS: 8 | --all, -a Show all versions including ones not yet installed (default: false) 9 | -------------------------------------------------------------------------------- /internal/cmd/testdata/func-e_which_help.txt: -------------------------------------------------------------------------------- 1 | NAME: 2 | func-e which - Prints the path to the Envoy binary used by the "run" command 3 | 4 | USAGE: 5 | func-e which 6 | 7 | DESCRIPTION: 8 | The binary is downloaded as necessary. The version is controllable by the "use" command 9 | -------------------------------------------------------------------------------- /internal/cmd/usage_md_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "os" 19 | "runtime" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | "github.com/urfave/cli/v2" 24 | 25 | "github.com/tetratelabs/func-e/internal/globals" 26 | "github.com/tetratelabs/func-e/internal/moreos" 27 | ) 28 | 29 | const siteMarkdownFile = "../../USAGE.md" 30 | 31 | // TestUsageMarkdownMatchesCommands is in the "cmd" package because changes here will drift siteMarkdownFile. 32 | func TestUsageMarkdownMatchesCommands(t *testing.T) { 33 | if runtime.GOOS == moreos.OSWindows { 34 | t.SkipNow() 35 | } 36 | 37 | // Use a custom markdown template 38 | old := cli.MarkdownDocTemplate 39 | defer func() { cli.MarkdownDocTemplate = old }() 40 | cli.MarkdownDocTemplate = `# func-e Overview 41 | {{ .App.UsageText }} 42 | 43 | # Commands 44 | 45 | | Name | Usage | 46 | | ---- | ----- | 47 | {{range $index, $cmd := .App.VisibleCommands}}{{if $index}} 48 | {{end}}| {{$cmd.Name}} | {{$cmd.Usage}} |{{end}} 49 | | --version, -v | Print the version of func-e | 50 | 51 | # Environment Variables 52 | 53 | | Name | Usage | Default | 54 | | ---- | ----- | ------- | 55 | {{range $index, $option := .App.VisibleFlags}}{{if $index}} 56 | {{end}}| {{index $option.EnvVars 0}} | {{$option.Usage}} | {{$option.DefaultText}} |{{end}} 57 | ` 58 | a := NewApp(&globals.GlobalOpts{}) 59 | expected, err := a.ToMarkdown() 60 | require.NoError(t, err) 61 | 62 | actual, err := os.ReadFile(siteMarkdownFile) 63 | require.NoError(t, err) 64 | require.Equal(t, expected, string(actual)) 65 | } 66 | -------------------------------------------------------------------------------- /internal/cmd/use.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "github.com/urfave/cli/v2" 19 | 20 | "github.com/tetratelabs/func-e/internal/envoy" 21 | "github.com/tetratelabs/func-e/internal/globals" 22 | "github.com/tetratelabs/func-e/internal/moreos" 23 | "github.com/tetratelabs/func-e/internal/version" 24 | ) 25 | 26 | // NewUseCmd create a command responsible for downloading and extracting Envoy 27 | func NewUseCmd(o *globals.GlobalOpts) *cli.Command { 28 | versionsDir := moreos.ReplacePathSeparator("$FUNC_E_HOME/versions/") 29 | currentVersionWorkingDirFile := moreos.ReplacePathSeparator(envoy.CurrentVersionWorkingDirFile) 30 | currentVersionHomeDirFile := moreos.ReplacePathSeparator(envoy.CurrentVersionHomeDirFile) 31 | 32 | var v version.Version 33 | return &cli.Command{ 34 | Name: "use", 35 | Usage: `Sets the current [version] used by the "run" command`, 36 | ArgsUsage: "[version]", 37 | Description: moreos.Sprintf(`The '[version]' is from the "versions -a" command. 38 | The Envoy [version] installs on-demand into `+versionsDir+`[version] 39 | if needed. You may also exclude the patch component of the [version] 40 | to use the latest patch version or to download the binary if it is 41 | not already downloaded. 42 | 43 | This updates %s or %s with [version], 44 | depending on which is present. 45 | 46 | Example: 47 | $ func-e use %s 48 | $ func-e use %s`, currentVersionWorkingDirFile, currentVersionHomeDirFile, version.LastKnownEnvoy, version.LastKnownEnvoyMinor), 49 | Before: func(c *cli.Context) (err error) { 50 | if v, err = version.NewVersion("[version] argument", c.Args().First()); err != nil { 51 | err = NewValidationError(err.Error()) 52 | } 53 | return 54 | }, 55 | Action: func(c *cli.Context) (err error) { 56 | // The argument could be a MinorVersion (ex. 1.19) or a PatchVersion (ex. 1.19.3) 57 | // We need to download and install a patch version 58 | if o.EnvoyVersion, err = ensurePatchVersion(c.Context, o, v); err != nil { 59 | return err 60 | } 61 | if _, err = envoy.InstallIfNeeded(c.Context, o); err != nil { 62 | return err 63 | } 64 | // Persist the input precision. This allows those specifying a MinorVersion to always get the latest patch. 65 | return envoy.WriteCurrentVersion(v, o.HomeDir) 66 | }, 67 | CustomHelpTemplate: cli.CommandHelpTemplate, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/cmd/versions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "sort" 22 | "text/tabwriter" 23 | "time" 24 | 25 | "github.com/urfave/cli/v2" 26 | 27 | "github.com/tetratelabs/func-e/internal/envoy" 28 | "github.com/tetratelabs/func-e/internal/globals" 29 | "github.com/tetratelabs/func-e/internal/moreos" 30 | "github.com/tetratelabs/func-e/internal/version" 31 | ) 32 | 33 | // NewVersionsCmd returns command that lists available Envoy versions for the current platform. 34 | func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command { 35 | return &cli.Command{ 36 | Name: "versions", 37 | Usage: "List Envoy versions", 38 | Flags: []cli.Flag{ 39 | &cli.BoolFlag{ 40 | Name: "all", 41 | Aliases: []string{"a"}, 42 | Usage: "Show all versions including ones not yet installed", 43 | }}, 44 | Action: func(c *cli.Context) error { 45 | rows, err := getInstalledVersions(o.HomeDir) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | currentVersion, currentVersionSource, err := envoy.CurrentVersion(o.HomeDir) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if c.Bool("all") { 56 | if evs, err := o.GetEnvoyVersions(c.Context); err != nil { 57 | return err 58 | } else if err := addAvailableVersions(&rows, evs.Versions, o.Platform); err != nil { 59 | return err 60 | } 61 | } 62 | 63 | // Sort so that new release dates appear first and on conflict choosing the higher version 64 | sort.Slice(rows, func(i, j int) bool { 65 | if rows[i].releaseDate == rows[j].releaseDate { 66 | return rows[i].version > rows[j].version 67 | } 68 | return rows[i].releaseDate > rows[j].releaseDate 69 | }) 70 | 71 | // We use a tab writer to ensure we can format the current version 72 | w := tabwriter.NewWriter(c.App.Writer, 0, 0, 1, ' ', tabwriter.AlignRight) 73 | for _, vr := range rows { //nolint:gocritic 74 | // TODO: handle when currentVersion is a MinorVersion 75 | pv, ok := currentVersion.(version.PatchVersion) 76 | if ok && vr.version == pv { 77 | moreos.Fprintf(w, "* %s %s (set by %s)\n", vr.version, vr.releaseDate, currentVersionSource) 78 | } else { 79 | moreos.Fprintf(w, " %s %s\n", vr.version, vr.releaseDate) 80 | } 81 | } 82 | return w.Flush() 83 | }, 84 | CustomHelpTemplate: cli.CommandHelpTemplate, 85 | } 86 | } 87 | 88 | type versionReleaseDate struct { 89 | version version.PatchVersion 90 | releaseDate version.ReleaseDate 91 | } 92 | 93 | func getInstalledVersions(homeDir string) ([]versionReleaseDate, error) { 94 | var rows []versionReleaseDate 95 | files, err := os.ReadDir(filepath.Join(homeDir, "versions")) 96 | if os.IsNotExist(err) { 97 | return rows, nil 98 | } else if err != nil { 99 | return nil, err 100 | } 101 | 102 | for _, f := range files { 103 | pv := version.NewPatchVersion(f.Name()) 104 | if i, err := f.Info(); f.IsDir() && pv != "" && err == nil { 105 | rows = append(rows, versionReleaseDate{ 106 | pv, 107 | version.ReleaseDate(i.ModTime().Format("2006-01-02")), 108 | }) 109 | } 110 | } 111 | return rows, nil 112 | } 113 | 114 | // addAvailableVersions adds remote Envoy versions valid for this platform to "rows", if they don't already exist 115 | func addAvailableVersions(rows *[]versionReleaseDate, remote map[version.PatchVersion]version.Release, p version.Platform) error { 116 | existingVersions := make(map[version.PatchVersion]bool) 117 | for _, v := range *rows { //nolint:gocritic 118 | existingVersions[v.version] = true 119 | } 120 | 121 | for k, v := range remote { 122 | if _, ok := v.Tarballs[p]; ok && !existingVersions[k] { 123 | if _, err := time.Parse("2006-01-02", string(v.ReleaseDate)); err != nil { 124 | return fmt.Errorf("invalid releaseDate of version %q for platform %q: %w", k, p, err) 125 | } 126 | *rows = append(*rows, versionReleaseDate{k, v.ReleaseDate}) 127 | } 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/cmd/which.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "github.com/urfave/cli/v2" 19 | 20 | "github.com/tetratelabs/func-e/internal/envoy" 21 | "github.com/tetratelabs/func-e/internal/globals" 22 | "github.com/tetratelabs/func-e/internal/moreos" 23 | ) 24 | 25 | // NewWhichCmd create a command responsible for downloading printing the path to the Envoy binary 26 | func NewWhichCmd(o *globals.GlobalOpts) *cli.Command { 27 | return &cli.Command{ 28 | Name: "which", 29 | Usage: `Prints the path to the Envoy binary used by the "run" command`, 30 | Description: `The binary is downloaded as necessary. The version is controllable by the "use" command`, 31 | Before: func(c *cli.Context) error { 32 | // no logging on version query/download. This is deferred until we know we are executing "which" 33 | o.Quiet = true 34 | return ensureEnvoyVersion(c, o) 35 | }, 36 | Action: func(c *cli.Context) error { 37 | ev, err := envoy.InstallIfNeeded(c.Context, o) 38 | if err != nil { 39 | return err 40 | } 41 | moreos.Fprintf(o.Out, "%s\n", ev) 42 | return nil 43 | }, 44 | CustomHelpTemplate: cli.CommandHelpTemplate, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/cmd/which_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd_test 16 | 17 | import ( 18 | "context" 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/tetratelabs/func-e/internal/moreos" 25 | ) 26 | 27 | func (r *runner) Which(ctx context.Context, args []string) error { 28 | return r.c.RunContext(ctx, args) 29 | } 30 | 31 | func TestFuncEWhich(t *testing.T) { 32 | o := setupTest(t) 33 | 34 | c, stdout, stderr := newApp(o) 35 | 36 | require.NoError(t, c.Run([]string{"func-e", "which"})) 37 | envoyPath := filepath.Join(o.HomeDir, "versions", o.EnvoyVersion.String(), "bin", "envoy"+moreos.Exe) 38 | require.Equal(t, moreos.Sprintf("%s\n", envoyPath), stdout.String()) 39 | require.Empty(t, stderr) 40 | } 41 | -------------------------------------------------------------------------------- /internal/envoy/http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/http" 21 | "strings" 22 | 23 | "github.com/tetratelabs/func-e/internal/version" 24 | ) 25 | 26 | // httpGet adds the userAgent header to the request, so that we can tell what is a dev build vs release. 27 | func httpGet(ctx context.Context, url string, p version.Platform, v string) (*http.Response, error) { 28 | // #nosec -> url can be anywhere by design 29 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 30 | if err != nil { 31 | return nil, err 32 | } 33 | req.Header.Add("User-Agent", userAgent(p, v)) 34 | return http.DefaultClient.Do(req) 35 | } 36 | 37 | // userAgent returns the 'User-Agent' header value used in HTTP requests. This is useful in log, metrics, analytics, or 38 | // request filtering. As this is a CLI, the best 'User-Agent' is the binary version including platform. 39 | // 40 | // The returned value limits cardinality to formal release * platform or one value for all non-releases. 41 | // 42 | // Note: Analytics may not work out-of-box. For example, Netlify does not support server-side analytics on 'User-Agent', 43 | // and even its 'Referer' analytics are limited to requests to HTML resources. 44 | func userAgent(p version.Platform, v string) string { 45 | if !strings.HasPrefix(v, "v") || strings.Contains(v, "SNAPSHOT") { 46 | return "func-e/dev" 47 | } 48 | return fmt.Sprintf("func-e/%s (%s)", v, p) 49 | } 50 | -------------------------------------------------------------------------------- /internal/envoy/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "net/http" 20 | "net/http/httptest" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/require" 24 | 25 | "github.com/tetratelabs/func-e/internal/globals" 26 | ) 27 | 28 | func TestHttpGet_AddsDefaultHeaders(t *testing.T) { 29 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | for k, v := range map[string]string{"User-Agent": "func-e/dev"} { 31 | require.Equal(t, v, r.Header.Get(k)) 32 | } 33 | })) 34 | defer ts.Close() 35 | 36 | res, err := httpGet(context.Background(), ts.URL, globals.DefaultPlatform, "dev") 37 | require.NoError(t, err) 38 | 39 | defer res.Body.Close() 40 | require.Equal(t, 200, res.StatusCode) 41 | } 42 | -------------------------------------------------------------------------------- /internal/envoy/install.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/http" 21 | "os" 22 | "path" 23 | "path/filepath" 24 | "time" 25 | 26 | "github.com/tetratelabs/func-e/internal/globals" 27 | "github.com/tetratelabs/func-e/internal/moreos" 28 | "github.com/tetratelabs/func-e/internal/tar" 29 | "github.com/tetratelabs/func-e/internal/version" 30 | ) 31 | 32 | var binEnvoy = filepath.Join("bin", "envoy"+moreos.Exe) 33 | 34 | // InstallIfNeeded downloads an Envoy binary corresponding to globals.GlobalOpts and returns a path to it or an error. 35 | func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts) (string, error) { 36 | v := o.EnvoyVersion 37 | installPath := filepath.Join(o.HomeDir, "versions", v.String()) 38 | envoyPath := filepath.Join(installPath, binEnvoy) 39 | _, err := os.Stat(envoyPath) 40 | switch { 41 | case os.IsNotExist(err): 42 | var evs *version.ReleaseVersions // Get version metadata for what we will install 43 | evs, err = o.GetEnvoyVersions(ctx) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | tarballURL := evs.Versions[v].Tarballs[o.Platform] // Ensure there is a version for this platform 49 | if tarballURL == "" { 50 | return "", fmt.Errorf("couldn't find version %q for platform %q", v, o.Platform) 51 | } 52 | 53 | tarball := version.Tarball(path.Base(string(tarballURL))) 54 | sha256Sum := evs.SHA256Sums[tarball] 55 | if len(sha256Sum) != 64 { 56 | return "", fmt.Errorf("couldn't find sha256Sum of version %q for platform %q: %w", v, o.Platform, err) 57 | } 58 | 59 | var mtime time.Time // Create a directory for the version, preserving the release date as its mtime 60 | if mtime, err = time.Parse("2006-01-02", string(evs.Versions[v].ReleaseDate)); err != nil { 61 | return "", fmt.Errorf("couldn't find releaseDate of version %q for platform %q: %w", v, o.Platform, err) 62 | } 63 | if err = os.MkdirAll(installPath, 0o750); err != nil { 64 | return "", fmt.Errorf("unable to create directory %q: %w", installPath, err) 65 | } 66 | o.Logf("downloading %s\n", tarballURL) 67 | if err = untarEnvoy(ctx, installPath, tarballURL, sha256Sum, o.Platform, o.Version); err != nil { //nolint 68 | return "", err 69 | } 70 | if err = os.Chtimes(installPath, mtime, mtime); err != nil { // overwrite the mtime to preserve it in the list 71 | return "", fmt.Errorf("unable to set date of directory %q: %w", installPath, err) 72 | } 73 | case err == nil: 74 | o.Logf("%s is already downloaded\n", v) 75 | default: 76 | // TODO: figure out how to get a stat error that isn't file not exist so we can test this 77 | return "", err 78 | } 79 | return verifyEnvoy(installPath) 80 | } 81 | 82 | func verifyEnvoy(installPath string) (string, error) { 83 | envoyPath := filepath.Join(installPath, binEnvoy) 84 | stat, err := os.Stat(envoyPath) 85 | if err != nil { 86 | return "", err 87 | } 88 | if !moreos.IsExecutable(stat) { 89 | return "", fmt.Errorf("envoy binary not executable at %q", envoyPath) 90 | } 91 | return envoyPath, nil 92 | } 93 | 94 | func untarEnvoy(ctx context.Context, dst string, src version.TarballURL, // dst, src order like io.Copy 95 | sha256Sum version.SHA256Sum, p version.Platform, v string) error { 96 | res, err := httpGet(ctx, string(src), p, v) 97 | if err != nil { 98 | return err 99 | } 100 | defer res.Body.Close() //nolint 101 | 102 | if res.StatusCode != http.StatusOK { 103 | return fmt.Errorf("received %v status code from %s", res.StatusCode, src) 104 | } 105 | if err = tar.UntarAndVerify(dst, res.Body, sha256Sum); err != nil { 106 | return fmt.Errorf("error untarring %s: %w", src, err) 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/envoy/run.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "strconv" 23 | "time" 24 | 25 | "github.com/tetratelabs/func-e/internal/moreos" 26 | ) 27 | 28 | // Run execs the binary at the path with the args passed. It is a blocking function that can be shutdown via ctx. 29 | // 30 | // This will exit either `ctx` is done, or the process exits. 31 | func (r *Runtime) Run(ctx context.Context, args []string) error { 32 | // We can't use CommandContext even if that seems correct here. The reason is that we need to invoke shutdown hooks, 33 | // and they expect the process to still be running. For example, this allows admin API hooks. 34 | cmd := exec.Command(r.opts.EnvoyPath, args...) // #nosec -> users can run whatever binary they like! 35 | cmd.Stdout = r.Out 36 | cmd.Stderr = r.Err 37 | cmd.SysProcAttr = moreos.ProcessGroupAttr() 38 | r.cmd = cmd 39 | 40 | if err := r.ensureAdminAddressPath(); err != nil { 41 | return err 42 | } 43 | 44 | // Print the binary path to the user for debugging purposes. 45 | moreos.Fprintf(r.Out, "starting: %s with --admin-address-path %s\n", r.opts.EnvoyPath, r.adminAddressPath) //nolint 46 | if err := cmd.Start(); err != nil { 47 | return fmt.Errorf("unable to start Envoy process: %w", err) 48 | } 49 | 50 | // Warn, but don't fail if we can't write the pid file for some reason 51 | r.maybeWarn(os.WriteFile(r.pidPath, []byte(strconv.Itoa(cmd.Process.Pid)), 0o600)) 52 | 53 | // Wait in a goroutine. We may need to kill the process if a signal occurs first. 54 | // 55 | // Note: do not wrap the original context, otherwise "<-cmdExitWait.Done()" won't block until the process exits 56 | // if the original context is done. 57 | cmdExitWait, cmdExit := context.WithCancel(context.Background()) 58 | defer cmdExit() 59 | go func() { 60 | defer cmdExit() 61 | _ = r.cmd.Wait() 62 | }() 63 | 64 | awaitAdminAddress(ctx, r) 65 | 66 | // Block until the process exits or the original context is done. 67 | select { 68 | case <-ctx.Done(): 69 | // When original context is done, we need to shutdown the process by ourselves. 70 | // Run the shutdown hooks and wait for them to complete. 71 | r.handleShutdown() 72 | // Then wait for the process to exit. 73 | <-cmdExitWait.Done() 74 | case <-cmdExitWait.Done(): 75 | } 76 | 77 | // Warn, but don't fail on error archiving the run directory 78 | if !r.opts.DontArchiveRunDir { 79 | r.maybeWarn(r.archiveRunDir()) 80 | } 81 | 82 | if cmd.ProcessState.ExitCode() > 0 { 83 | return fmt.Errorf("envoy exited with status: %d", cmd.ProcessState.ExitCode()) 84 | } 85 | return nil 86 | } 87 | 88 | // awaitAdminAddress waits up to 2 seconds for the admin address to be available and logs it. 89 | // See https://github.com/envoyproxy/envoy/issues/16050 for moving this logging upstream. 90 | func awaitAdminAddress(sigCtx context.Context, r *Runtime) { 91 | for i := 0; i < 10 && sigCtx.Err() == nil; i++ { 92 | adminAddress, adminErr := r.GetAdminAddress() 93 | if adminErr == nil { 94 | moreos.Fprintf(r.Out, "discovered admin address: %s\n", adminAddress) 95 | return 96 | } 97 | time.Sleep(200 * time.Millisecond) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/envoy/run_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "testing" 25 | "time" 26 | 27 | "github.com/shirou/gopsutil/v3/process" 28 | "github.com/stretchr/testify/require" 29 | 30 | "github.com/tetratelabs/func-e/internal/globals" 31 | "github.com/tetratelabs/func-e/internal/moreos" 32 | "github.com/tetratelabs/func-e/internal/test" 33 | "github.com/tetratelabs/func-e/internal/test/fakebinary" 34 | ) 35 | 36 | func TestRuntime_Run(t *testing.T) { 37 | tempDir := t.TempDir() 38 | 39 | runsDir := filepath.Join(tempDir, "runs") 40 | runDir := filepath.Join(runsDir, "1619574747231823000") // fake a realistic value 41 | 42 | fakeEnvoy := filepath.Join(tempDir, "envoy"+moreos.Exe) 43 | fakebinary.RequireFakeEnvoy(t, fakeEnvoy) 44 | 45 | tests := []struct { 46 | name string 47 | args []string 48 | shutdown bool 49 | timeout time.Duration 50 | expectedStderr string 51 | expectedErr string 52 | wantShutdownHook bool 53 | }{ 54 | { 55 | name: "func-e Ctrl+C", 56 | args: []string{"-c", "envoy.yaml"}, 57 | timeout: time.Second, 58 | // Don't warn the user when they exited the process 59 | expectedStderr: moreos.Sprintf("initializing epoch 0\nstarting main dispatch loop\ncaught SIGINT\nexiting\n"), 60 | wantShutdownHook: true, 61 | }, 62 | // We don't test envoy dying from an external signal as it isn't reported back to the func-e process and 63 | // Envoy returns exit status zero on anything except kill -9. We can't test kill -9 with a fake shell script. 64 | { 65 | name: "Envoy exited with error", 66 | args: []string{}, // no config file! 67 | expectedStderr: moreos.Sprintf("initializing epoch 0\nexiting\nAt least one of --config-path or --config-yaml or Options::configProto() should be non-empty\n"), 68 | expectedErr: "envoy exited with status: 1", 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | tc := tt 74 | t.Run(tc.name, func(t *testing.T) { 75 | o := &globals.RunOpts{EnvoyPath: fakeEnvoy, RunDir: runDir} 76 | require.NoError(t, os.MkdirAll(runDir, 0o750)) 77 | 78 | stdout := new(bytes.Buffer) 79 | stderr := new(bytes.Buffer) 80 | 81 | r := NewRuntime(o) 82 | r.Out = stdout 83 | r.Err = stderr 84 | var haveShutdownHook bool 85 | r.RegisterShutdownHook(func(_ context.Context) error { 86 | pid := requireEnvoyPid(t, r) 87 | 88 | // Validate envoy.pid was written 89 | pidText, err := os.ReadFile(r.pidPath) 90 | require.NoError(t, err) 91 | require.Equal(t, strconv.Itoa(pid), string(pidText)) 92 | require.Greater(t, pid, 1) 93 | 94 | // Ensure the process can still be looked up (ex it didn't die from accidental signal propagation) 95 | _, err = process.NewProcess(int32(pid)) // because os.FindProcess is no-op in Linux! 96 | require.NoError(t, err, "shutdownHook called after process shutdown") 97 | 98 | haveShutdownHook = true 99 | return nil 100 | }) 101 | 102 | // tee the error stream so we can look for the "starting main dispatch loop" line without consuming it. 103 | errCopy := new(bytes.Buffer) 104 | r.Err = io.MultiWriter(r.Err, errCopy) 105 | err := test.RequireRun(t, tc.timeout, r, errCopy, tc.args...) 106 | 107 | if tc.expectedErr == "" { 108 | require.NoError(t, err) 109 | } else { 110 | require.EqualError(t, err, tc.expectedErr) 111 | } 112 | 113 | // Ensure envoy was run with the expected environment 114 | require.Empty(t, r.cmd.Dir) // envoy runs in the same directory as func-e 115 | expectedArgs := append([]string{fakeEnvoy}, tc.args...) 116 | expectedArgs = append(expectedArgs, "--admin-address-path", filepath.Join(runDir, "admin-address.txt")) 117 | require.Equal(t, expectedArgs, r.cmd.Args) 118 | 119 | // Assert appropriate hooks are called 120 | require.Equal(t, tc.wantShutdownHook, haveShutdownHook) 121 | 122 | // Validate we ran what we thought we did 123 | require.Contains(t, stdout.String(), moreos.Sprintf("starting: %s", fakeEnvoy)) 124 | require.Contains(t, stderr.String(), tc.expectedStderr) 125 | 126 | // Ensure the working directory was deleted, and the "run" directory only contains the archive 127 | files, err := os.ReadDir(runsDir) 128 | require.NoError(t, err) 129 | require.Equal(t, 1, len(files)) 130 | archive := filepath.Join(runsDir, files[0].Name()) 131 | require.Equal(t, runDir+".tar.gz", archive) 132 | 133 | // Cleanup for the next run 134 | require.NoError(t, os.Remove(archive)) 135 | }) 136 | } 137 | } 138 | 139 | func requireEnvoyPid(t *testing.T, r *Runtime) int { 140 | if r.cmd == nil || r.cmd.Process == nil { 141 | t.Fatal("envoy process not yet started") 142 | } 143 | return r.cmd.Process.Pid 144 | } 145 | -------------------------------------------------------------------------------- /internal/envoy/runtime.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "net" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "syscall" 26 | "time" 27 | 28 | "github.com/tetratelabs/func-e/internal/globals" 29 | "github.com/tetratelabs/func-e/internal/moreos" 30 | ) 31 | 32 | const ( 33 | // Don't wait forever. This has hung on macOS before 34 | shutdownTimeout = 5 * time.Second 35 | // Match envoy's log format field 36 | dateFormat = "[2006-01-02 15:04:05.999]" 37 | ) 38 | 39 | // NewRuntime creates a new Runtime that runs envoy in globals.RunOpts RunDir 40 | // opts allows a user running envoy to control the working directory by ID or path, allowing explicit cleanup. 41 | func NewRuntime(opts *globals.RunOpts) *Runtime { 42 | return &Runtime{opts: opts, pidPath: filepath.Join(opts.RunDir, "envoy.pid")} 43 | } 44 | 45 | // Runtime manages an Envoy lifecycle 46 | type Runtime struct { 47 | opts *globals.RunOpts 48 | 49 | cmd *exec.Cmd 50 | Out, Err io.Writer 51 | OutFile, ErrFile *os.File 52 | 53 | adminAddress, adminAddressPath, pidPath string 54 | 55 | shutdownHooks []func(context.Context) error 56 | } 57 | 58 | // String is only used in tests. It is slow, but helps when debugging CI failures 59 | func (r *Runtime) String() string { 60 | exitStatus := -1 61 | if r.cmd != nil && r.cmd.ProcessState != nil { 62 | if ws, ok := r.cmd.ProcessState.Sys().(syscall.WaitStatus); ok { 63 | exitStatus = ws.ExitStatus() 64 | } 65 | } 66 | 67 | return fmt.Sprintf("{exitStatus: %d}", exitStatus) 68 | } 69 | 70 | // GetRunDir returns the run-specific directory files can be written to. 71 | func (r *Runtime) GetRunDir() string { 72 | return r.opts.RunDir 73 | } 74 | 75 | // maybeWarn writes a warning message to Runtime.Out when the error isn't nil 76 | func (r *Runtime) maybeWarn(err error) { 77 | if err != nil { 78 | moreos.Fprintf(r.Out, "warning: %s\n", err) //nolint 79 | } 80 | } 81 | 82 | // ensureAdminAddressPath sets the "--admin-address-path" flag so that it can be used in /ready checks. If a value 83 | // already exists, it will be returned. Otherwise, the flag will be set to the file "admin-address.txt" in the 84 | // run directory. We don't use the working directory as sometimes that is a source directory. 85 | // 86 | // Notably, this allows ephemeral admin ports via bootstrap configuration admin/port_value=0 (minimum Envoy 1.12 for macOS support) 87 | func (r *Runtime) ensureAdminAddressPath() error { 88 | args := r.cmd.Args 89 | flag := `--admin-address-path` 90 | for i, a := range args { 91 | if a == flag { 92 | if i+1 == len(args) || args[i+1] == "" { 93 | return fmt.Errorf(`missing value to argument %q`, flag) 94 | } 95 | r.adminAddressPath = args[i+1] 96 | return nil 97 | } 98 | } 99 | // Envoy's run directory is mutable, so it is fine to write the admin address there. 100 | r.adminAddressPath = filepath.Join(r.opts.RunDir, "admin-address.txt") 101 | r.cmd.Args = append(r.cmd.Args, flag, r.adminAddressPath) 102 | return nil 103 | } 104 | 105 | // GetAdminAddress returns the current admin address in host:port format, or empty if not yet available. 106 | // Exported for shutdown.enableEnvoyAdminDataCollection, which is always on due to shutdown.EnableHooks. 107 | func (r *Runtime) GetAdminAddress() (string, error) { 108 | if r.adminAddress != "" { // We don't expect the admin address to change once written, so cache it. 109 | return r.adminAddress, nil 110 | } 111 | adminAddress, err := os.ReadFile(r.adminAddressPath) 112 | if err != nil { 113 | return "", fmt.Errorf("unable to read %s: %w", r.adminAddressPath, err) 114 | } 115 | if _, _, err := net.SplitHostPort(string(adminAddress)); err != nil { 116 | return "", fmt.Errorf("invalid admin address in %s: %w", r.adminAddressPath, err) 117 | } 118 | r.adminAddress = string(adminAddress) 119 | return r.adminAddress, nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/envoy/runtime_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "os/exec" 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/tetratelabs/func-e/internal/globals" 25 | "github.com/tetratelabs/func-e/internal/moreos" 26 | ) 27 | 28 | func TestEnsureAdminAddressPath(t *testing.T) { 29 | runDir := t.TempDir() 30 | 31 | runAdminAddressPath := filepath.Join(runDir, "admin-address.txt") 32 | tests := []struct { 33 | name string 34 | args []string 35 | wantAdminAddressPath string 36 | wantArgs []string 37 | }{ 38 | { 39 | name: "no args", 40 | args: []string{"envoy"}, 41 | wantAdminAddressPath: runAdminAddressPath, 42 | wantArgs: []string{"envoy", "--admin-address-path", runAdminAddressPath}, 43 | }, 44 | { 45 | name: "args", 46 | args: []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml"}, 47 | wantAdminAddressPath: runAdminAddressPath, 48 | wantArgs: []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml", "--admin-address-path", runAdminAddressPath}, 49 | }, 50 | { 51 | name: "already", 52 | args: []string{"envoy", "--admin-address-path", "/tmp/admin.txt", "-c", "/tmp/google_com_proxy.v2.yaml"}, 53 | wantAdminAddressPath: "/tmp/admin.txt", 54 | wantArgs: []string{"envoy", "--admin-address-path", "/tmp/admin.txt", "-c", "/tmp/google_com_proxy.v2.yaml"}, 55 | }, 56 | } 57 | 58 | for _, tt := range tests { 59 | tt := tt 60 | t.Run(tt.name, func(t *testing.T) { 61 | r := NewRuntime(&globals.RunOpts{RunDir: runDir}) 62 | r.cmd = exec.Command(tt.args[0], tt.args[1:]...) 63 | 64 | err := r.ensureAdminAddressPath() 65 | require.NoError(t, err) 66 | require.Equal(t, tt.wantAdminAddressPath, r.adminAddressPath) 67 | require.Equal(t, tt.wantArgs, r.cmd.Args) 68 | }) 69 | } 70 | } 71 | 72 | func TestEnsureAdminAddressPath_ValidateExisting(t *testing.T) { 73 | tests := []struct { 74 | name string 75 | args []string 76 | expectedErr string 77 | }{ 78 | { 79 | name: "value empty", 80 | args: []string{"envoy", "--admin-address-path", "", "-c", "/tmp/google_com_proxy.v2.yaml"}, 81 | expectedErr: `missing value to argument "--admin-address-path"`, 82 | }, 83 | { 84 | name: "value missing", 85 | args: []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml", "--admin-address-path"}, 86 | expectedErr: `missing value to argument "--admin-address-path"`, 87 | }, 88 | } 89 | 90 | for _, tt := range tests { 91 | tt := tt 92 | t.Run(tt.name, func(t *testing.T) { 93 | r := NewRuntime(&globals.RunOpts{}) 94 | r.cmd = exec.Command(tt.args[0], tt.args[1:]...) 95 | 96 | err := r.ensureAdminAddressPath() 97 | require.Equal(t, tt.args, r.cmd.Args) 98 | require.Empty(t, r.adminAddressPath) 99 | require.EqualError(t, err, tt.expectedErr) 100 | }) 101 | } 102 | } 103 | 104 | func TestPidFilePath(t *testing.T) { 105 | r := NewRuntime(&globals.RunOpts{RunDir: "run"}) 106 | require.Equal(t, filepath.Join("run", "envoy.pid"), r.pidPath) 107 | } 108 | 109 | func TestString(t *testing.T) { 110 | cmdExited := NewRuntime(&globals.RunOpts{}) 111 | cmdExited.cmd = exec.Command("echo") 112 | require.NoError(t, cmdExited.cmd.Run()) 113 | 114 | cmdFailed := NewRuntime(&globals.RunOpts{}) 115 | cmdFailed.cmd = exec.Command("cat"+moreos.Exe, "icecream") 116 | require.Error(t, cmdFailed.cmd.Run()) 117 | 118 | // Fork a process that hangs 119 | cmdRunning := NewRuntime(&globals.RunOpts{}) 120 | cmdRunning.cmd = exec.Command("cat" + moreos.Exe) 121 | cmdRunning.cmd.SysProcAttr = moreos.ProcessGroupAttr() 122 | require.NoError(t, cmdRunning.cmd.Start()) 123 | defer cmdRunning.cmd.Process.Kill() 124 | 125 | tests := []struct { 126 | name string 127 | runtime *Runtime 128 | expected string 129 | }{ 130 | { 131 | name: "command exited", 132 | runtime: cmdExited, 133 | expected: "{exitStatus: 0}", 134 | }, 135 | { 136 | name: "command failed", 137 | runtime: cmdFailed, 138 | expected: "{exitStatus: 1}", 139 | }, 140 | { 141 | name: "command running", 142 | runtime: cmdRunning, 143 | expected: "{exitStatus: -1}", 144 | }, 145 | } 146 | 147 | for _, tt := range tests { 148 | tt := tt 149 | t.Run(tt.name, func(t *testing.T) { 150 | require.Equal(t, tt.expected, tt.runtime.String()) 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /internal/envoy/shutdown.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | "sync" 23 | "time" 24 | 25 | "github.com/tetratelabs/func-e/internal/moreos" 26 | "github.com/tetratelabs/func-e/internal/tar" 27 | ) 28 | 29 | // RegisterShutdownHook registers the passed functions to be run after Envoy has started 30 | // and just before func-e instructs Envoy to exit 31 | func (r *Runtime) RegisterShutdownHook(f func(context.Context) error) { 32 | r.shutdownHooks = append(r.shutdownHooks, f) 33 | } 34 | 35 | func (r *Runtime) handleShutdown() { 36 | defer r.interruptEnvoy() // Ensure the SIGINT forwards to Envoy even if a shutdown hook panics 37 | 38 | deadline := time.Now().Add(shutdownTimeout) 39 | timeout, cancel := context.WithDeadline(context.Background(), deadline) 40 | defer cancel() 41 | 42 | moreos.Fprintf(r.Out, "invoking shutdown hooks with deadline %s\n", deadline.Format(dateFormat)) 43 | 44 | // Run each hook in parallel, logging each error 45 | var wg sync.WaitGroup 46 | wg.Add(len(r.shutdownHooks)) 47 | for _, f := range r.shutdownHooks { 48 | f := f // pin! see https://github.com/kyoh86/scopelint for why 49 | go func() { 50 | defer wg.Done() 51 | if err := f(timeout); err != nil { 52 | moreos.Fprintf(r.Out, "failed shutdown hook: %s\n", err) 53 | } 54 | }() 55 | } 56 | wg.Wait() 57 | } 58 | 59 | func (r *Runtime) interruptEnvoy() { 60 | p := r.cmd.Process 61 | moreos.Fprintf(r.Out, "sending interrupt to envoy (pid=%d)\n", p.Pid) 62 | r.maybeWarn(moreos.Interrupt(p)) 63 | } 64 | 65 | func (r *Runtime) archiveRunDir() error { 66 | // Ensure logs are closed before we try to archive them, particularly important in Windows. 67 | if r.OutFile != nil { 68 | r.OutFile.Close() //nolint 69 | } 70 | if r.ErrFile != nil { 71 | r.ErrFile.Close() //nolint 72 | } 73 | if r.opts.DontArchiveRunDir { 74 | return nil 75 | } 76 | 77 | // Given ~/.func-e/debug/1620955405964267000 78 | dirName := filepath.Dir(r.GetRunDir()) // ~/.func-e/runs 79 | baseName := filepath.Base(r.GetRunDir()) // 1620955405964267000 80 | targzName := filepath.Join(dirName, baseName+".tar.gz") // ~/.func-e/runs/1620955405964267000.tar.gz 81 | 82 | if err := tar.TarGz(targzName, r.GetRunDir()); err != nil { 83 | return fmt.Errorf("unable to archive run directory %v: %w", r.GetRunDir(), err) 84 | } 85 | return os.RemoveAll(r.GetRunDir()) 86 | } 87 | -------------------------------------------------------------------------------- /internal/envoy/shutdown/admin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shutdown 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "net/http" 22 | "os" 23 | "path/filepath" 24 | "time" 25 | 26 | "golang.org/x/sync/errgroup" 27 | 28 | "github.com/tetratelabs/func-e/internal/envoy" 29 | ) 30 | 31 | var adminAPIPaths = map[string]string{ 32 | "certs": "certs.json", 33 | "clusters": "clusters.txt", 34 | "config_dump": "config_dump.json", 35 | "contention": "contention.txt", 36 | "listeners": "listeners.txt", 37 | "memory": "memory.json", 38 | "server_info": "server_info.json", 39 | "stats?format=json": "stats.json", 40 | "runtime": "runtime.json", 41 | } 42 | 43 | // enableEnvoyAdminDataCollection is a preset option that registers collection of Envoy Admin API information 44 | func enableEnvoyAdminDataCollection(r *envoy.Runtime) error { 45 | e := envoyAdminDataCollection{r.GetAdminAddress, r.GetRunDir()} 46 | r.RegisterShutdownHook(e.retrieveAdminAPIData) 47 | return nil 48 | } 49 | 50 | type envoyAdminDataCollection struct { 51 | getAdminAddress func() (string, error) 52 | workingDir string 53 | } 54 | 55 | func (e *envoyAdminDataCollection) retrieveAdminAPIData(ctx context.Context) error { 56 | adminAddress, err := e.getAdminAddress() 57 | if err != nil { 58 | return fmt.Errorf("unable to capture Envoy configuration and metrics: %w", err) 59 | } 60 | 61 | // Save each admin API path to a file in parallel returning on first error 62 | // Execute all admin fetches in parallel 63 | g, ctx := errgroup.WithContext(ctx) 64 | for p, f := range adminAPIPaths { 65 | url := fmt.Sprintf("http://%s/%v", adminAddress, p) 66 | file := filepath.Join(e.workingDir, f) 67 | 68 | g.Go(func() error { 69 | ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 70 | defer cancel() 71 | return copyURLToFile(ctx, url, file) 72 | }) 73 | } 74 | return g.Wait() // first error 75 | } 76 | 77 | func copyURLToFile(ctx context.Context, url, fullPath string) error { 78 | // #nosec -> e.workingDir is allowed to be anywhere 79 | f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0o600) 80 | if err != nil { 81 | return fmt.Errorf("could not open %q: %w", fullPath, err) 82 | } 83 | defer f.Close() //nolint 84 | 85 | // #nosec -> adminAddress is written by Envoy and the paths are hard-coded 86 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 87 | if err != nil { 88 | return fmt.Errorf("could not create request %v: %w", url, err) 89 | } 90 | res, err := http.DefaultClient.Do(req) 91 | if err != nil { 92 | return fmt.Errorf("could not read %v: %w", url, err) 93 | } 94 | defer res.Body.Close() //nolint 95 | 96 | if res.StatusCode != http.StatusOK { 97 | return fmt.Errorf("received %v from %v", res.StatusCode, url) 98 | } 99 | if _, err := io.Copy(f, res.Body); err != nil { 100 | return fmt.Errorf("could not write response body of %v: %w", url, err) 101 | } 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /internal/envoy/shutdown/admin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shutdown 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | "time" 24 | 25 | "github.com/stretchr/testify/require" 26 | 27 | "github.com/tetratelabs/func-e/internal/envoy" 28 | "github.com/tetratelabs/func-e/internal/globals" 29 | "github.com/tetratelabs/func-e/internal/moreos" 30 | "github.com/tetratelabs/func-e/internal/test" 31 | "github.com/tetratelabs/func-e/internal/test/fakebinary" 32 | ) 33 | 34 | func TestEnableEnvoyAdminDataCollection(t *testing.T) { 35 | runDir := t.TempDir() 36 | 37 | require.NoError(t, runWithShutdownHook(t, runDir, enableEnvoyAdminDataCollection)) 38 | 39 | for _, filename := range adminAPIPaths { 40 | path := filepath.Join(runDir, filename) 41 | f, err := os.Stat(path) 42 | require.NoError(t, err, "error stating %v", path) 43 | require.NotEmpty(t, f.Size(), "file %v was empty", path) 44 | } 45 | } 46 | 47 | // runWithShutdownHook is like RequireRun, except invokes the hook on shutdown 48 | func runWithShutdownHook(t *testing.T, runDir string, hook func(r *envoy.Runtime) error) error { 49 | fakeEnvoy := filepath.Join(runDir, "envoy"+moreos.Exe) 50 | fakebinary.RequireFakeEnvoy(t, fakeEnvoy) 51 | 52 | o := &globals.RunOpts{EnvoyPath: fakeEnvoy, RunDir: runDir, DontArchiveRunDir: true} 53 | 54 | stderr := new(bytes.Buffer) 55 | r := envoy.NewRuntime(o) 56 | r.Out = io.Discard 57 | r.Err = stderr 58 | require.NoError(t, hook(r)) 59 | 60 | return test.RequireRun(t, 10*time.Second, r, stderr, "-c", "envoy.yaml") 61 | } 62 | -------------------------------------------------------------------------------- /internal/envoy/shutdown/node_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shutdown 16 | 17 | import ( 18 | "encoding/json" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestEnableNodeCollection(t *testing.T) { 28 | runDir := t.TempDir() 29 | 30 | require.NoError(t, runWithShutdownHook(t, runDir, enableNodeCollection)) 31 | 32 | files := [...]string{"node/ps.txt", "node/network_interface.json", "node/connections.json"} 33 | for _, file := range files { 34 | path := filepath.Join(runDir, file) 35 | f, err := os.Stat(path) 36 | require.NoError(t, err, "error stating %v", path) 37 | 38 | // While usually not, ps can be empty due to deadline timeout getting a ps listing. Instead of flakey tests, we 39 | // don't enforce this. 40 | if file != "node/ps.txt" { 41 | require.NotEmpty(t, f.Size(), "file %v was empty", path) 42 | } 43 | 44 | if strings.HasSuffix(file, ".json") { 45 | raw, err := os.ReadFile(path) 46 | require.NoError(t, err, "error to read the file %v", path) 47 | var is []interface{} 48 | 49 | err = json.Unmarshal(raw, &is) 50 | require.NoError(t, err, "error to unmarshal json string, %v: \"%v\"", err, raw) 51 | require.NotEmpty(t, len(is), "unmarshalled content is empty, expected to be a non-empty array: \"%v\"", raw) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/envoy/shutdown/shutdown.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shutdown 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "os" 22 | "strings" 23 | 24 | "github.com/tetratelabs/func-e/internal/envoy" 25 | ) 26 | 27 | // EnableHooks is a list of functions that enable shutdown hooks 28 | var EnableHooks = []func(*envoy.Runtime) error{enableEnvoyAdminDataCollection, enableNodeCollection} 29 | 30 | // wrapError wraps an error from using "gopsutil" or returns nil on "not implemented yet". 31 | // We don't err on unimplemented because we don't want to disturb users for unresolvable reasons. 32 | func wrapError(ctx context.Context, err error, field string, pid int32) error { 33 | if err == nil { 34 | err = ctx.Err() 35 | } 36 | if err != nil && err.Error() != "not implemented yet" { // don't log if it will never work 37 | return fmt.Errorf("unable to retrieve %s of pid %d: %w", field, pid, err) 38 | } 39 | return nil 40 | } 41 | 42 | // writeJSON centralizes logic to avoid writing empty files. 43 | func writeJSON(result interface{}, filename string) error { 44 | sb := new(strings.Builder) 45 | if err := json.NewEncoder(sb).Encode(result); err != nil { 46 | return fmt.Errorf("error serializing %v as JSON: %w", sb, err) 47 | } 48 | return os.WriteFile(filename, []byte(sb.String()), 0o600) 49 | } 50 | -------------------------------------------------------------------------------- /internal/envoy/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | 23 | "github.com/tetratelabs/func-e/internal/moreos" 24 | "github.com/tetratelabs/func-e/internal/version" 25 | ) 26 | 27 | const ( 28 | currentVersionVar = "$ENVOY_VERSION" 29 | // CurrentVersionWorkingDirFile is used for stable "versions" and "help" output 30 | CurrentVersionWorkingDirFile = "$PWD/.envoy-version" 31 | // CurrentVersionHomeDirFile is used for stable "versions" and "help" output 32 | CurrentVersionHomeDirFile = "$FUNC_E_HOME/version" 33 | ) 34 | 35 | // WriteCurrentVersion writes the version to CurrentVersionWorkingDirFile or CurrentVersionHomeDirFile depending on 36 | // if the former is present. 37 | func WriteCurrentVersion(v version.Version, homeDir string) error { 38 | if _, err := os.Stat(".envoy-version"); os.IsNotExist(err) { 39 | if e := os.MkdirAll(homeDir, 0o750); e != nil { 40 | return e 41 | } 42 | return os.WriteFile(filepath.Join(homeDir, "version"), []byte(v.String()), 0o600) 43 | } else if err != nil { 44 | return err 45 | } 46 | return os.WriteFile(".envoy-version", []byte(v.String()), 0o600) 47 | } 48 | 49 | // CurrentVersion returns the first version in priority of VersionUsageList and its source or an error. The "source" 50 | // and error messages returned include unexpanded variables to clarify the intended context. 51 | // In the case no version was found, the version returned will be nil, not an error. 52 | func CurrentVersion(homeDir string) (v version.Version, source string, err error) { 53 | s, source, err := getCurrentVersion(homeDir) 54 | v, err = verifyVersion(s, source, err) 55 | return 56 | } 57 | 58 | func verifyVersion(v, source string, err error) (version.Version, error) { 59 | if err != nil { 60 | return nil, moreos.Errorf(`couldn't read version from %s: %w`, source, err) 61 | } else if v == "" && source == CurrentVersionHomeDirFile { // don't error on initial state 62 | return nil, nil 63 | } 64 | return version.NewVersion(fmt.Sprintf("version in %q", source), v) 65 | } 66 | 67 | func getCurrentVersion(homeDir string) (v, source string, err error) { 68 | // Priority 1: $ENVOY_VERSION 69 | if ev, ok := os.LookupEnv("ENVOY_VERSION"); ok { 70 | v = ev 71 | source = currentVersionVar 72 | return 73 | } 74 | 75 | // Priority 2: $PWD/.envoy-version 76 | data, err := os.ReadFile(".envoy-version") 77 | if err == nil { 78 | v = strings.TrimSpace(string(data)) 79 | source = CurrentVersionWorkingDirFile 80 | return 81 | } else if !os.IsNotExist(err) { 82 | return "", CurrentVersionWorkingDirFile, err 83 | } 84 | 85 | // Priority 3: $FUNC_E_HOME/version 86 | source = CurrentVersionHomeDirFile 87 | v, err = getHomeVersion(homeDir) 88 | return 89 | } 90 | 91 | func getHomeVersion(homeDir string) (v string, err error) { 92 | var data []byte 93 | if data, err = os.ReadFile(filepath.Join(homeDir, "version")); err == nil { //nolint:gosec 94 | v = strings.TrimSpace(string(data)) 95 | } else if os.IsNotExist(err) { 96 | err = nil // ok on file-not-found 97 | } 98 | return 99 | } 100 | 101 | // VersionUsageList is the priority order of Envoy version sources. 102 | // This includes unresolved variables as it is both used statically for markdown generation, and also at runtime. 103 | func VersionUsageList() string { 104 | return moreos.ReplacePathSeparator( 105 | strings.Join([]string{currentVersionVar, CurrentVersionWorkingDirFile, CurrentVersionHomeDirFile}, ", "), 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /internal/envoy/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/require" 24 | 25 | "github.com/tetratelabs/func-e/internal/moreos" 26 | "github.com/tetratelabs/func-e/internal/test/morerequire" 27 | "github.com/tetratelabs/func-e/internal/version" 28 | ) 29 | 30 | func TestVersionUsageList(t *testing.T) { 31 | expected := moreos.ReplacePathSeparator("$ENVOY_VERSION, $PWD/.envoy-version, $FUNC_E_HOME/version") 32 | require.Equal(t, expected, VersionUsageList()) 33 | } 34 | 35 | func TestWriteCurrentVersion_HomeDir(t *testing.T) { 36 | homeDir := t.TempDir() 37 | 38 | for _, tt := range []struct{ name, v string }{ 39 | {"writes initial home version", "1.1.1"}, 40 | {"overwrites home version", "2.2.2"}, 41 | } { 42 | tc := tt 43 | t.Run(tc.name, func(t *testing.T) { 44 | require.NoError(t, WriteCurrentVersion(version.PatchVersion(tc.v), homeDir)) 45 | v, src, err := getCurrentVersion(homeDir) 46 | require.NoError(t, err) 47 | require.Equal(t, tc.v, v) 48 | require.Equal(t, CurrentVersionHomeDirFile, src) 49 | require.NoFileExists(t, ".envoy-version") 50 | }) 51 | } 52 | } 53 | 54 | func TestWriteCurrentVersion_OverwritesWorkingDirVersion(t *testing.T) { 55 | homeDir := t.TempDir() 56 | 57 | homeVersionFile := filepath.Join(homeDir, "version") 58 | require.NoError(t, os.WriteFile(homeVersionFile, []byte("1.1.1"), 0o600)) 59 | 60 | revertWd := morerequire.RequireChdir(t, t.TempDir()) 61 | defer revertWd() 62 | require.NoError(t, os.WriteFile(".envoy-version", []byte("2.2.2"), 0o600)) 63 | 64 | require.NoError(t, WriteCurrentVersion(version.PatchVersion("3.3.3"), homeDir)) 65 | v, src, err := getCurrentVersion(homeDir) 66 | require.NoError(t, err) 67 | require.Equal(t, "3.3.3", v) 68 | require.Equal(t, CurrentVersionWorkingDirFile, src) 69 | 70 | // didn't overwrite the home version 71 | v, err = getHomeVersion(homeDir) 72 | require.NoError(t, err) 73 | require.Equal(t, "1.1.1", v) 74 | } 75 | 76 | // TestCurrentVersion is intentionally written in priority order instead of via a matrix. This particularly helps with 77 | // test setup complexity required to ensure tiered priority (ex layering overridden PWD with an ENV) 78 | func TestCurrentVersion(t *testing.T) { 79 | homeDir := t.TempDir() 80 | 81 | t.Run("defaults to nil", func(t *testing.T) { 82 | v, source, err := CurrentVersion(homeDir) 83 | require.Nil(t, v) 84 | require.Equal(t, CurrentVersionHomeDirFile, source) 85 | require.NoError(t, err) 86 | }) 87 | 88 | require.NoError(t, os.WriteFile(filepath.Join(homeDir, "version"), []byte("1.1.1"), 0o600)) 89 | t.Run("reads the home version", func(t *testing.T) { 90 | v, source, err := CurrentVersion(homeDir) 91 | require.Equal(t, version.PatchVersion("1.1.1"), v) 92 | require.Equal(t, CurrentVersionHomeDirFile, source) 93 | require.NoError(t, err) 94 | }) 95 | 96 | revertWd := morerequire.RequireChdir(t, t.TempDir()) 97 | defer revertWd() 98 | require.NoError(t, os.WriteFile(".envoy-version", []byte("2.2.2"), 0o600)) 99 | 100 | t.Run("prefers $PWD/.envoy-version over home version", func(t *testing.T) { 101 | v, source, err := CurrentVersion(homeDir) 102 | require.Equal(t, version.PatchVersion("2.2.2"), v) 103 | require.Equal(t, CurrentVersionWorkingDirFile, source) 104 | require.NoError(t, err) 105 | }) 106 | 107 | t.Setenv("ENVOY_VERSION", "3.3.3") 108 | 109 | t.Run("prefers $ENVOY_VERSION over $PWD/.envoy-version", func(t *testing.T) { 110 | v, source, err := CurrentVersion(homeDir) 111 | require.Equal(t, version.PatchVersion("3.3.3"), v) 112 | require.Equal(t, currentVersionVar, source) 113 | require.NoError(t, err) 114 | }) 115 | } 116 | 117 | // TestCurrentVersion_Validates is intentionally written in priority order instead of via a matrix 118 | func TestCurrentVersion_Validates(t *testing.T) { 119 | homeDir := t.TempDir() 120 | require.NoError(t, os.WriteFile(filepath.Join(homeDir, "version"), []byte("a.a.a"), 0o600)) 121 | 122 | t.Run("validates home version", func(t *testing.T) { 123 | _, _, err := CurrentVersion(homeDir) 124 | expectedErr := fmt.Sprintf(`invalid version in "$FUNC_E_HOME/version": "a.a.a" should look like %q or %q`, version.LastKnownEnvoy, version.LastKnownEnvoyMinor) 125 | require.EqualError(t, err, moreos.ReplacePathSeparator(expectedErr)) 126 | }) 127 | 128 | revertWd := morerequire.RequireChdir(t, t.TempDir()) 129 | defer revertWd() 130 | require.NoError(t, os.WriteFile(".envoy-version", []byte("b.b.b"), 0o600)) 131 | 132 | t.Run("validates $PWD/.envoy-version", func(t *testing.T) { 133 | _, _, err := CurrentVersion(homeDir) 134 | expectedErr := fmt.Sprintf(`invalid version in "$PWD/.envoy-version": "b.b.b" should look like %q or %q`, version.LastKnownEnvoy, version.LastKnownEnvoyMinor) 135 | require.EqualError(t, err, moreos.ReplacePathSeparator(expectedErr)) 136 | }) 137 | 138 | require.NoError(t, os.Remove(".envoy-version")) 139 | require.NoError(t, os.Mkdir(".envoy-version", 0o700)) 140 | 141 | t.Run("shows error reading $PWD/.envoy-version", func(t *testing.T) { 142 | _, _, err := CurrentVersion(homeDir) 143 | expectedErr := moreos.ReplacePathSeparator("couldn't read version from $PWD/.envoy-version") 144 | require.Contains(t, err.Error(), expectedErr) 145 | }) 146 | 147 | t.Setenv("ENVOY_VERSION", "c.c.c") 148 | 149 | t.Run("validates $ENVOY_VERSION", func(t *testing.T) { 150 | _, _, err := CurrentVersion(homeDir) 151 | require.EqualError(t, err, fmt.Sprintf(`invalid version in "$ENVOY_VERSION": "c.c.c" should look like %q or %q`, version.LastKnownEnvoy, version.LastKnownEnvoyMinor)) 152 | }) 153 | } 154 | -------------------------------------------------------------------------------- /internal/envoy/versions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | 24 | "github.com/tetratelabs/func-e/internal/version" 25 | ) 26 | 27 | // NewGetVersions creates a new Envoy versions fetcher. 28 | // TODO: validate the data before returning it! 29 | func NewGetVersions(envoyVersionsURL string, p version.Platform, v string) version.GetReleaseVersions { 30 | return func(ctx context.Context) (*version.ReleaseVersions, error) { 31 | // #nosec => This is by design, users can call out to wherever they like! 32 | resp, err := httpGet(ctx, envoyVersionsURL, p, v) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer resp.Body.Close() //nolint 37 | 38 | if resp.StatusCode != http.StatusOK { 39 | return nil, fmt.Errorf("received %v status code from %v", resp.StatusCode, envoyVersionsURL) 40 | } 41 | body, err := io.ReadAll(resp.Body) // fully read the response 42 | if err != nil { 43 | return nil, fmt.Errorf("error reading %s: %w", envoyVersionsURL, err) 44 | } 45 | 46 | result := version.ReleaseVersions{} 47 | if err := json.Unmarshal(body, &result); err != nil { 48 | return nil, fmt.Errorf("error unmarshalling Envoy versions: %w", err) 49 | } 50 | return &result, nil 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/envoy/versions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package envoy 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/tetratelabs/func-e/internal/globals" 24 | "github.com/tetratelabs/func-e/internal/test" 25 | "github.com/tetratelabs/func-e/internal/version" 26 | ) 27 | 28 | func TestNewGetVersions(t *testing.T) { 29 | versionsServer := test.RequireEnvoyVersionsTestServer(t, version.LastKnownEnvoy) 30 | gv := NewGetVersions(versionsServer.URL+"/envoy-versions.json", globals.DefaultPlatform, "dev") 31 | 32 | evs, err := gv(context.Background()) 33 | require.NoError(t, err) 34 | require.Contains(t, evs.Versions, version.LastKnownEnvoy) 35 | } 36 | -------------------------------------------------------------------------------- /internal/globals/globals.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package globals 16 | 17 | import ( 18 | "io" 19 | "runtime" 20 | 21 | "github.com/tetratelabs/func-e/internal/moreos" 22 | "github.com/tetratelabs/func-e/internal/version" 23 | ) 24 | 25 | // RunOpts support invocations of "func-e run" 26 | type RunOpts struct { 27 | // EnvoyPath is the exec.Cmd path to "envoy". Defaults to "$HomeDir/versions/$version/bin/envoy" 28 | EnvoyPath string 29 | // RunDir is the location any generated files are written. 30 | // This is not Envoy's working directory, which remains the same as the $PWD of func-e. 31 | // 32 | // Upon shutdown, this directory is archived as "../$(basename $RunDir).tar.gz" 33 | // Defaults to "$HomeDir/runs/$epochtime" 34 | RunDir string 35 | // DontArchiveRunDir is used in testing and prevents archiving the RunDir 36 | DontArchiveRunDir bool 37 | } 38 | 39 | // GlobalOpts represents options that affect more than one func-e commands. 40 | // 41 | // Fields representing non-hidden flags have values set according to the following rules: 42 | // 1. value that precedes flag parsing, used in tests 43 | // 2. to a value of the command line argument, e.g. `--home-dir` 44 | // 3. optional mapping to an environment variable, e.g. `FUNC_E_HOME` (not all flags are mapped to ENV) 45 | // 4. otherwise, to the default value, e.g. DefaultHomeDir 46 | type GlobalOpts struct { 47 | // RunOpts are inlined to allow tests to override parameters without changing ENV variables or flags 48 | RunOpts 49 | // Version is the version of the CLI, used in help statements and HTTP requests via "User-Agent". 50 | // Override this via "-X main.version=XXX" 51 | Version string 52 | // EnvoyVersionsURL is the path to the envoy-versions.json. Defaults to DefaultEnvoyVersionsURL 53 | EnvoyVersionsURL string 54 | // EnvoyVersion is the default version of Envoy to run. Defaults to the contents of "$HomeDir/versions/version". 55 | // When that file is missing, it is generated from ".latestVersion" from the EnvoyVersionsURL. Its 56 | // value can be in full version major.minor.patch format, e.g. 1.18.1 or without patch component, 57 | // major.minor, e.g. 1.18. 58 | EnvoyVersion version.PatchVersion 59 | // HomeDir is an absolute path which most importantly contains "versions" installed from EnvoyVersionsURL. Defaults to DefaultHomeDir 60 | HomeDir string 61 | // Quiet means don't Logf to Out 62 | Quiet bool 63 | // Out is where status messages are written. Defaults to os.Stdout 64 | Out io.Writer 65 | // The platform to target for the Envoy install. 66 | Platform version.Platform 67 | // GetEnvoyVersions returns Envoy release versions from EnvoyVersionsURL. 68 | GetEnvoyVersions version.GetReleaseVersions 69 | } 70 | 71 | // Logf is used for shared functions that log conditionally on GlobalOpts.Quiet 72 | func (o *GlobalOpts) Logf(format string, a ...interface{}) { 73 | if o.Quiet { // TODO: we may want to do scoped logging via a Context property, if this becomes common. 74 | return 75 | } 76 | moreos.Fprintf(o.Out, format, a...) 77 | } 78 | 79 | const ( 80 | // DefaultEnvoyVersionsURL is the default value for GlobalOpts.EnvoyVersionsURL 81 | DefaultEnvoyVersionsURL = "https://archive.tetratelabs.io/envoy/envoy-versions.json" 82 | // DefaultEnvoyVersionsSchemaURL is the JSON schema used to validate GlobalOpts.EnvoyVersionsURL 83 | DefaultEnvoyVersionsSchemaURL = "https://archive.tetratelabs.io/release-versions-schema.json" 84 | // DefaultPlatform is the current platform of the host machine 85 | DefaultPlatform = version.Platform(runtime.GOOS + "/" + runtime.GOARCH) 86 | ) 87 | 88 | // DefaultHomeDir is the default value for GlobalOpts.HomeDir 89 | var DefaultHomeDir = moreos.ReplacePathSeparator("${HOME}/.func-e") 90 | -------------------------------------------------------------------------------- /internal/moreos/moreos.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package moreos 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | "os" 22 | "runtime" 23 | "strings" 24 | "syscall" 25 | ) 26 | 27 | const ( 28 | // Exe is the runtime.GOOS-specific suffix for executables. Ex. "" unless windows which is ".exe" 29 | // See https://github.com/golang/go/issues/47567 for formalization 30 | Exe = exe 31 | // OSDarwin is a Platform.OS a.k.a. "macOS" 32 | OSDarwin = "darwin" 33 | // OSLinux is a Platform.OS 34 | OSLinux = "linux" 35 | // OSWindows is a Platform.OS 36 | OSWindows = "windows" 37 | ) 38 | 39 | // Errorf is like fmt.Errorf except it translates paths with ReplacePathSeparator when Windows. 40 | // This is needed because by default '\' escapes. 41 | func Errorf(format string, a ...interface{}) error { 42 | err := fmt.Errorf(format, a...) 43 | if runtime.GOOS != OSWindows { 44 | return err 45 | } 46 | return errorWithWindowsPathSeparator(err) 47 | } 48 | 49 | func errorWithWindowsPathSeparator(err error) error { 50 | wrappedErr := errors.Unwrap(err) 51 | msg := withWindowsPathSeparator(err.Error()) // must be done after to avoid escaping '\' 52 | 53 | if wrappedErr == nil { 54 | return errors.New(msg) 55 | } 56 | return &wrapError{msg, wrappedErr} 57 | } 58 | 59 | type wrapError struct { 60 | msg string 61 | err error 62 | } 63 | 64 | func (e *wrapError) Error() string { 65 | return e.msg 66 | } 67 | 68 | func (e *wrapError) Unwrap() error { 69 | return e.err 70 | } 71 | 72 | // ReplacePathSeparator returns the input unless it is Windows. 73 | // When Windows, all '/' characters replace with '\'. 74 | func ReplacePathSeparator(input string) string { 75 | if runtime.GOOS != OSWindows { 76 | return input 77 | } 78 | return withWindowsPathSeparator(input) 79 | } 80 | 81 | func withWindowsPathSeparator(input string) string { 82 | return strings.ReplaceAll(input, "/", "\\") 83 | } 84 | 85 | // Sprintf is like Fprintf is like fmt.Sprintf, except any '\n' in the format are converted according to runtime.GOOS. 86 | // This allows us to be consistent with Envoy, which handles \r\n on Windows. 87 | // See also https://github.com/golang/go/issues/28822 88 | func Sprintf(format string, a ...interface{}) string { 89 | // Don't do anything unless we are on windows and the format isn't already correct EOL. 90 | // EOL already being correct is a currently unexplained scenario on GitHub Actions windows-latest runner! 91 | if runtime.GOOS != OSWindows || strings.Contains(format, "\r\n") { 92 | return fmt.Sprintf(format, a...) 93 | } 94 | return fmt.Sprintf(format, a...) 95 | } 96 | 97 | // Fprintf is like fmt.Fprintf, but handles EOL according runtime.GOOS. See Sprintf for notes. 98 | func Fprintf(w io.Writer, format string, a ...interface{}) { 99 | if runtime.GOOS != OSWindows { 100 | _, _ = fmt.Fprintf(w, format, a...) 101 | return 102 | } 103 | 104 | _, _ = fmt.Fprint(w, Sprintf(format, a...)) 105 | } 106 | 107 | // ProcessGroupAttr sets attributes that ensure exec.Cmd doesn't propagate signals from func-e by default. 108 | // This is used to ensure shutdown hooks can apply 109 | func ProcessGroupAttr() *syscall.SysProcAttr { 110 | return processGroupAttr() // un-exported to prevent godoc drift 111 | } 112 | 113 | // Interrupt attempts to interrupt the process. It doesn't necessarily kill it. 114 | func Interrupt(p *os.Process) error { 115 | return interrupt(p) // un-exported to prevent godoc drift 116 | } 117 | 118 | // EnsureProcessDone makes sure the process has exited completely. 119 | func EnsureProcessDone(p *os.Process) error { 120 | return ensureProcessDone(p) // un-exported to prevent godoc drift 121 | } 122 | 123 | // IsExecutable returns true if the input can be run as an exec.Cmd 124 | func IsExecutable(f os.FileInfo) bool { 125 | return isExecutable(f) 126 | } 127 | -------------------------------------------------------------------------------- /internal/moreos/moreos_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package moreos 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "runtime" 25 | "testing" 26 | 27 | "github.com/shirou/gopsutil/v3/process" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestErrorf_ConvertsPathWhenWindows(t *testing.T) { 32 | err := Errorf("/foo/bar is bad") 33 | if runtime.GOOS == OSWindows { 34 | require.EqualError(t, err, `\foo\bar is bad`) 35 | } else { 36 | require.EqualError(t, err, `/foo/bar is bad`) 37 | } 38 | } 39 | 40 | // TestErrorWithWindowsPathSeparator makes sure errors don't accidentally escape the windows path separator. 41 | // this is extracted so that maintainers can make sure it works without using windows. 42 | func TestErrorWithWindowsPathSeparator(t *testing.T) { 43 | err := errors.New("/foo/bar is bad") 44 | require.EqualError(t, errorWithWindowsPathSeparator(err), `\foo\bar is bad`) 45 | 46 | wrapped := errors.New("bad day") 47 | err = fmt.Errorf("/foo/bar is unhappy: %w", wrapped) 48 | wErr := errorWithWindowsPathSeparator(err) 49 | require.EqualError(t, wErr, `\foo\bar is unhappy: bad day`) 50 | require.Same(t, wrapped, errors.Unwrap(wErr)) 51 | } 52 | 53 | func TestIsExecutable(t *testing.T) { 54 | tempDir := t.TempDir() 55 | 56 | bin := filepath.Join(tempDir, "envoy"+Exe) 57 | require.NoError(t, os.WriteFile(bin, []byte{}, 0o700)) 58 | 59 | f, err := os.Stat(bin) 60 | require.NoError(t, err) 61 | 62 | require.True(t, isExecutable(f)) 63 | } 64 | 65 | func TestIsExecutable_Not(t *testing.T) { 66 | tempDir := t.TempDir() 67 | 68 | bin := filepath.Join(tempDir, "foo.txt") 69 | require.NoError(t, os.WriteFile(bin, []byte{}, 0o600)) 70 | 71 | f, err := os.Stat(bin) 72 | require.NoError(t, err) 73 | 74 | require.False(t, isExecutable(f)) 75 | } 76 | 77 | func TestReplacePathSeparator(t *testing.T) { 78 | path := "/foo/bar" 79 | 80 | expected := path 81 | if runtime.GOOS == OSWindows { 82 | expected = "\\foo\\bar" 83 | } 84 | 85 | require.Equal(t, expected, ReplacePathSeparator(path)) 86 | } 87 | 88 | func TestSprintf(t *testing.T) { 89 | template := "%s\n\n%s\n" 90 | 91 | expected := "foo\n\nbar\n" 92 | if runtime.GOOS == OSWindows { 93 | expected = "foo\r\n\r\nbar\r\n" 94 | } 95 | 96 | require.Equal(t, expected, Sprintf(template, "foo", "bar")) 97 | 98 | // ensure idempotent 99 | require.Equal(t, expected, expected) 100 | } 101 | 102 | func TestFprintf(t *testing.T) { 103 | template := "%s\n\n%s\n" 104 | stdout := new(bytes.Buffer) 105 | Fprintf(stdout, template, "foo", "bar") 106 | 107 | expected := "foo\n\nbar\n" 108 | if runtime.GOOS == OSWindows { 109 | expected = "foo\r\n\r\nbar\r\n" 110 | } 111 | 112 | require.Equal(t, expected, stdout.String()) 113 | } 114 | 115 | // TestSprintf_IdiomaticPerOS is here to ensure that the EOL translation makes sense. For example, in UNIX, we expect 116 | // \n and windows \r\n. This uses a real command to prove the point. 117 | func TestSprintf_IdiomaticPerOS(t *testing.T) { 118 | stdout := new(bytes.Buffer) 119 | cmd := exec.Command("echo", "cats") 120 | if runtime.GOOS == OSWindows { 121 | cmd = exec.Command("cmd", "/c", "echo", "cats") 122 | } 123 | cmd.Stdout = stdout 124 | require.NoError(t, cmd.Run()) 125 | require.Equal(t, Sprintf("cats\n"), stdout.String()) 126 | } 127 | 128 | func TestProcessGroupAttr_Interrupt(t *testing.T) { 129 | // Fork a process that hangs 130 | cmd := exec.Command("cat" + Exe) 131 | cmd.SysProcAttr = ProcessGroupAttr() 132 | require.NoError(t, cmd.Start()) 133 | 134 | // Verify the process exists 135 | require.NoError(t, findProcess(cmd.Process)) 136 | 137 | // Interrupt it 138 | require.NoError(t, Interrupt(cmd.Process)) 139 | 140 | // Wait for the process to die; this could error due to the interrupt signal 141 | _ = cmd.Wait() 142 | require.Error(t, findProcess(cmd.Process)) 143 | 144 | // Ensure interrupting it again doesn't error 145 | require.NoError(t, Interrupt(cmd.Process)) 146 | } 147 | 148 | func Test_EnsureProcessDone(t *testing.T) { 149 | // Fork a process that hangs 150 | cmd := exec.Command("cat" + Exe) 151 | cmd.SysProcAttr = ProcessGroupAttr() 152 | require.NoError(t, cmd.Start()) 153 | 154 | // Kill it 155 | require.NoError(t, EnsureProcessDone(cmd.Process)) 156 | 157 | // Wait for the process to die; this could error due to the kill signal 158 | _ = cmd.Wait() 159 | require.Error(t, findProcess(cmd.Process)) 160 | 161 | // Ensure killing it again doesn't error 162 | require.NoError(t, EnsureProcessDone(cmd.Process)) 163 | } 164 | 165 | func findProcess(proc *os.Process) error { 166 | _, err := process.NewProcess(int32(proc.Pid)) // because os.FindProcess is no-op in Linux! 167 | return err 168 | } 169 | -------------------------------------------------------------------------------- /internal/moreos/proc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !windows 16 | 17 | package moreos 18 | 19 | import ( 20 | "os" 21 | "syscall" 22 | ) 23 | 24 | const exe = "" 25 | 26 | func interrupt(p *os.Process) error { 27 | if err := p.Signal(syscall.SIGINT); err != nil && err != os.ErrProcessDone { 28 | return err 29 | } 30 | return nil 31 | } 32 | 33 | func ensureProcessDone(p *os.Process) error { 34 | if err := p.Kill(); err != nil && err != os.ErrProcessDone { 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | func isExecutable(f os.FileInfo) bool { 41 | return f.Mode()&0o111 != 0 42 | } 43 | -------------------------------------------------------------------------------- /internal/moreos/proc_attr_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package moreos 16 | 17 | import ( 18 | "syscall" 19 | ) 20 | 21 | func processGroupAttr() *syscall.SysProcAttr { 22 | return &syscall.SysProcAttr{Setpgid: true} 23 | } 24 | -------------------------------------------------------------------------------- /internal/moreos/proc_attr_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package moreos 16 | 17 | import ( 18 | "syscall" 19 | ) 20 | 21 | func processGroupAttr() *syscall.SysProcAttr { 22 | // Pdeathsig aims to ensure the process group is cleaned up even if this process dies. When func-e 23 | // dies, the process (envoy) will get SIGKILL. 24 | return &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL} 25 | } 26 | -------------------------------------------------------------------------------- /internal/moreos/proc_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package moreos 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os" 21 | "strings" 22 | "syscall" 23 | ) 24 | 25 | const ( 26 | exe = ".exe" 27 | // from WinError.h, but not defined for some reason in types_windows.go 28 | errorInvalidParameter = 87 29 | ) 30 | 31 | func processGroupAttr() *syscall.SysProcAttr { 32 | return &syscall.SysProcAttr{ 33 | CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, // Stop Ctrl-Break propagation to allow shutdown-hooks 34 | } 35 | } 36 | 37 | // interrupt contains signal_windows_test.go sendCtrlBreak() as there's no main source with the same. 38 | func interrupt(p *os.Process) error { 39 | pid := p.Pid 40 | d, err := syscall.LoadDLL("kernel32.dll") 41 | if err != nil { 42 | return errorInterrupting(pid, err) 43 | } 44 | proc, err := d.FindProc("GenerateConsoleCtrlEvent") 45 | if err != nil { 46 | return errorInterrupting(pid, err) 47 | } 48 | r, _, err := proc.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid)) 49 | if r == 0 { // because err != nil on success "The operation completed successfully" 50 | return errorInterrupting(pid, err) 51 | } 52 | return nil 53 | } 54 | 55 | func errorInterrupting(pid int, err error) error { 56 | return fmt.Errorf("couldn't Interrupt pid(%d): %w", pid, err) 57 | } 58 | 59 | // ensureProcessDone attempts to work around flakey logic in os.Process Wait on Windows. This code block should be 60 | // revisited if https://golang.org/issue/25965 is solved. 61 | func ensureProcessDone(p *os.Process) error { 62 | // Process.handle is not exported. Lookup the process again, using logic similar to exec_windows/findProcess() 63 | const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_TERMINATE | 64 | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE 65 | h, e := syscall.OpenProcess(da, true, uint32(p.Pid)) 66 | if e != nil { 67 | if errno, ok := e.(syscall.Errno); ok && uintptr(errno) == errorInvalidParameter { 68 | return nil // don't error if the process isn't around anymore 69 | } 70 | return os.NewSyscallError("OpenProcess", e) 71 | } 72 | defer syscall.CloseHandle(h) //nolint:errcheck 73 | 74 | // Try to wait for the process to close naturally first, using logic from exec_windows/findProcess() 75 | // Difference here, is we are waiting 100ms not infinite. If there's a timeout, we kill the proc. 76 | s, e := syscall.WaitForSingleObject(h, 100) 77 | switch s { 78 | case syscall.WAIT_OBJECT_0: 79 | return nil // process is no longer around 80 | case syscall.WAIT_TIMEOUT: 81 | return syscall.TerminateProcess(h, uint32(0)) // kill, but don't effect the exit code 82 | case syscall.WAIT_FAILED: 83 | return os.NewSyscallError("WaitForSingleObject", e) 84 | default: 85 | return errors.New("os: unexpected result from WaitForSingleObject") 86 | } 87 | } 88 | 89 | func isExecutable(f os.FileInfo) bool { // In windows, we cannot read execute bit 90 | return strings.HasSuffix(f.Name(), ".exe") 91 | } 92 | -------------------------------------------------------------------------------- /internal/moreos/testdata/fake_func-e.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // only import moreos, as that's what we are testing 4 | import ( 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/tetratelabs/func-e/internal/moreos" 15 | ) 16 | 17 | // main simulates ../../.../main.go, but only focuses on sub process control style used by envoy.Run. 18 | // This allows us to write unit tests and identify failures more directly than e2e tests. 19 | // 20 | // Notably, this uses a variable ENVOY_PATH instead of envoy.GetHomeVersion, in order to reduce logic. 21 | // 22 | // In the future, some of this process control structure might move to moreos in order to reduce copy/paste between here 23 | // and internal/envoy/run.go (envoy.Run). 24 | func main() { 25 | if len(os.Args) < 2 { 26 | moreos.Fprintf(os.Stderr, "not enough args\n") 27 | os.Exit(1) 28 | } 29 | 30 | if os.Args[1] != "run" { 31 | moreos.Fprintf(os.Stderr, "%s not supported\n", os.Args[1]) 32 | os.Exit(1) 33 | } 34 | 35 | // This is similar to main.go, except we don't import the validation error 36 | if err := run(context.Background(), os.Args[2:]); err != nil { 37 | moreos.Fprintf(os.Stderr, "error: %s\n", err) 38 | os.Exit(1) 39 | } 40 | os.Exit(0) 41 | } 42 | 43 | // simulates envoy.Run with slight adjustments 44 | func run(ctx context.Context, args []string) error { 45 | // Like envoy.GetHomeVersion, $FUNC_E_HOME/versions/$(cat $FUNC_E_HOME/version)/bin/envoy$GOEXE. 46 | cmd := exec.Command(os.Getenv("ENVOY_PATH"), args...) 47 | cmd.SysProcAttr = moreos.ProcessGroupAttr() 48 | cmd.Stdout = os.Stdout 49 | cmd.Stderr = os.Stderr 50 | 51 | waitCtx, waitCancel := context.WithCancel(ctx) 52 | defer waitCancel() 53 | 54 | sigCtx, stop := signal.NotifyContext(waitCtx, os.Interrupt, syscall.SIGTERM) 55 | defer stop() 56 | 57 | moreos.Fprintf(cmd.Stdout, "starting: %s\n", strings.Join(cmd.Args, " ")) 58 | if err := cmd.Start(); err != nil { 59 | return fmt.Errorf("unable to start Envoy process: %w", err) 60 | } 61 | 62 | // Wait in a goroutine. We may need to kill the process if a signal occurs first. 63 | go func() { 64 | defer waitCancel() 65 | _ = cmd.Wait() // Envoy logs like "caught SIGINT" or "caught ENVOY_SIGTERM", so we don't repeat logging here. 66 | }() 67 | 68 | // Block until we receive SIGINT or are canceled because Envoy has died. 69 | <-sigCtx.Done() 70 | 71 | // The process could have exited due to incorrect arguments or otherwise. 72 | // If it is still running, run shutdown hooks and propagate the interrupt. 73 | if cmd.ProcessState == nil { 74 | handleShutdown(cmd) 75 | } 76 | 77 | // Block until it exits to ensure file descriptors are closed prior to archival. 78 | // Allow up to 5 seconds for a clean stop, killing if it can't for any reason. 79 | select { 80 | case <-waitCtx.Done(): // cmd.Wait goroutine finished 81 | case <-time.After(5 * time.Second): 82 | _ = moreos.EnsureProcessDone(cmd.Process) 83 | } 84 | 85 | // Unlike real func-e, we don't run shutdown hooks, so have no run directory to archive. 86 | if cmd.ProcessState.ExitCode() > 0 { 87 | return fmt.Errorf("envoy exited with status: %d", cmd.ProcessState.ExitCode()) 88 | } 89 | return nil 90 | } 91 | 92 | // handleShutdown simulates the same named function in envoy.Run, except doesn't run any shutdown hooks. 93 | // This is a copy/paste of envoy.Runtime.interruptEnvoy() with some formatting differences. 94 | func handleShutdown(cmd *exec.Cmd) { 95 | p := cmd.Process 96 | moreos.Fprintf(cmd.Stdout, "sending interrupt to envoy (pid=%d)\n", p.Pid) 97 | if err := moreos.Interrupt(p); err != nil { 98 | moreos.Fprintf(cmd.Stdout, "warning: %s\n", err) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/tar/testdata/empty.tar: -------------------------------------------------------------------------------- 1 | ./000755 000765 000024 00000000000 14050452547 011622 5ustar00adrianstaff000000 000000 -------------------------------------------------------------------------------- /internal/tar/testdata/empty.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/internal/tar/testdata/empty.tar.gz -------------------------------------------------------------------------------- /internal/tar/testdata/empty.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/internal/tar/testdata/empty.tar.xz -------------------------------------------------------------------------------- /internal/tar/testdata/foo/bar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | -------------------------------------------------------------------------------- /internal/tar/testdata/foo/bar/baz.txt: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /internal/tar/testdata/foo/bar/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/internal/tar/testdata/foo/bar/empty.txt -------------------------------------------------------------------------------- /internal/tar/testdata/test.tar: -------------------------------------------------------------------------------- 1 | foo/000755 000765 000024 00000000000 14050430652 012241 5ustar00adrianstaff000000 000000 foo/bar.sh000755 000765 000024 00000000000 14050423371 013332 0ustar00adrianstaff000000 000000 foo/bar/000755 000765 000024 00000000000 14050430661 013005 5ustar00adrianstaff000000 000000 foo/bar/baz.txt000644 000765 000024 00000000000 14050423371 014310 0ustar00adrianstaff000000 000000 -------------------------------------------------------------------------------- /internal/tar/testdata/test.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/internal/tar/testdata/test.tar.gz -------------------------------------------------------------------------------- /internal/tar/testdata/test.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/internal/tar/testdata/test.tar.xz -------------------------------------------------------------------------------- /internal/test/envoy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package test 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | _ "embed" // Embedding the fakeEnvoySrc is easier than file I/O and ensures it doesn't skew coverage 21 | "fmt" 22 | "io" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | // Runner allows us to not introduce dependency cycles on envoy.Runtime 31 | type Runner interface { 32 | Run(ctx context.Context, args []string) error 33 | } 34 | 35 | // RequireRun executes Run on the given Runtime and calls shutdown after it started. 36 | func RequireRun(t *testing.T, timeout time.Duration, r Runner, stderr io.Reader, args ...string) error { 37 | var ctx context.Context 38 | if timeout == 0 { 39 | ctx = context.Background() 40 | } else { 41 | var cancel context.CancelFunc 42 | ctx, cancel = context.WithTimeout(context.Background(), timeout) 43 | defer cancel() 44 | } 45 | 46 | // Run in a goroutine, and signal when that completes 47 | ran := make(chan bool) 48 | var err error 49 | go func() { 50 | if e := r.Run(ctx, args); e != nil && err == nil { 51 | err = e // first error 52 | } 53 | ran <- true 54 | }() 55 | 56 | // Block until we reach an expected line or timeout 57 | reader := bufio.NewReader(stderr) 58 | waitFor := "initializing epoch 0" 59 | if !assert.Eventually(t, func() bool { 60 | b, e := reader.Peek(512) 61 | return e != nil && strings.Contains(string(b), waitFor) 62 | }, 5*time.Second, 100*time.Millisecond) { 63 | if err == nil { // first error 64 | err = fmt.Errorf(`timeout waiting for stderr to contain "%s": runner: %s`, waitFor, r) 65 | } 66 | } 67 | 68 | <-ran // block until the runner finished 69 | return err 70 | } 71 | -------------------------------------------------------------------------------- /internal/test/fakebinary/fake_binary.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fakebinary as using morerequire introduces build cycles 16 | package fakebinary 17 | 18 | import ( 19 | _ "embed" // Embedding the fakeEnvoySrc is easier than file I/O and ensures it doesn't skew coverage 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "runtime" 24 | "sync" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | // exe is like moreos.Exe, except if we used that it would make a build cycle. 31 | var exe = func() string { 32 | if runtime.GOOS == "windows" { 33 | return ".exe" 34 | } 35 | return "" 36 | }() 37 | 38 | var ( 39 | // fakeEnvoySrc is a test source file used to simulate Envoy console output and signal processing. 40 | // This has no other source dependencies. 41 | //go:embed testdata/fake_envoy.go 42 | fakeEnvoySrc []byte 43 | // fakeEnvoyBin is the compiled code of fakeEnvoySrc which will be runtime.GOOS dependent. 44 | fakeEnvoyBin []byte 45 | builtFakeEnvoy sync.Once 46 | ) 47 | 48 | // RequireFakeEnvoy writes fakeEnvoyBin to the given path. This is embedded here because it is reused in many places. 49 | func RequireFakeEnvoy(t *testing.T, path string) { 50 | builtFakeEnvoy.Do(func() { 51 | fakeEnvoyBin = RequireBuildFakeBinary(t, t.TempDir(), "envoy", fakeEnvoySrc) 52 | }) 53 | require.NoError(t, os.WriteFile(path, fakeEnvoyBin, 0o700)) //nolint:gosec 54 | } 55 | 56 | // RequireBuildFakeBinary builds a fake binary and returns its contents. 57 | func RequireBuildFakeBinary(t *testing.T, workDir, name string, mainSrc []byte) []byte { 58 | goBin := requireGoBin(t) 59 | 60 | bin := name + exe 61 | goArgs := []string{"build", "-o", bin, "main.go"} 62 | require.NoError(t, os.WriteFile(filepath.Join(workDir, "main.go"), mainSrc, 0o600)) 63 | 64 | // Don't allow any third party dependencies for now. 65 | require.NoError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), 66 | []byte("module github.com/tetratelabs/func-e\n\ngo 1.17\n"), 0o600)) 67 | 68 | cmd := exec.Command(goBin, goArgs...) //nolint:gosec 69 | cmd.Dir = workDir 70 | out, err := cmd.CombinedOutput() 71 | require.NoError(t, err, "couldn't compile %s: %s", bin, string(out)) 72 | bytes, err := os.ReadFile(filepath.Join(workDir, bin)) //nolint:gosec 73 | require.NoError(t, err) 74 | return bytes 75 | } 76 | 77 | func requireGoBin(t *testing.T) string { 78 | binName := "go" + exe 79 | goBin := filepath.Join(os.Getenv("GOROOT"), "bin", binName) 80 | if _, err := os.Stat(goBin); err == nil { 81 | return goBin 82 | } 83 | // Now, search the path 84 | goBin, err := exec.LookPath(binName) 85 | require.NoError(t, err, "couldn't find %s in the PATH", goBin) 86 | return goBin 87 | } 88 | -------------------------------------------------------------------------------- /internal/test/fakebinary/testdata/fake_envoy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // It is essential that no func-e imports are used here. For example, internal/moreos is not used. 4 | import ( 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "os/signal" 10 | "runtime" 11 | "syscall" 12 | ) 13 | 14 | const errorConfig = "At least one of --config-path or --config-yaml or Options::configProto() should be non-empty" 15 | 16 | // lf ensures line feeds are realistic 17 | var lf = func() string { 18 | if runtime.GOOS == "windows" { 19 | return "\r\n" 20 | } 21 | return "\n" 22 | }() 23 | 24 | // main was originally ported from a shell script. Compiling allows a more realistic test. 25 | func main() { 26 | // Trap signals so we can respond like Envoy does 27 | c := make(chan os.Signal, 1) 28 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 29 | 30 | // Echo the same first line Envoy would. This also lets code scraping output know signals are trapped 31 | os.Stderr.Write([]byte("initializing epoch 0" + lf)) //nolint 32 | 33 | // Validate a config is passed just like Envoy 34 | haveConfig := false 35 | adminAddressPath := "" 36 | for i := 1; i < len(os.Args); i++ { 37 | switch os.Args[i] { 38 | case "-c", "--config-path", "--config-yaml": 39 | haveConfig = true 40 | case "--admin-address-path": 41 | i++ 42 | adminAddressPath = os.Args[i] 43 | } 44 | } 45 | if !haveConfig { 46 | exit(1, "exiting", errorConfig) // oddly, it is this order 47 | } 48 | 49 | // Start a fake admin listener that write the same sort of response Envoy's /ready would, but on all endpoints. 50 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 | // Envoy console messages all write to stderr. Simulate access_log_path: '/dev/stdout' 52 | os.Stdout.Write([]byte(fmt.Sprintf("GET %s HTTP/1.1%s", r.RequestURI, lf))) 53 | 54 | w.Header().Add("Content-Type", "text/plain; charset=UTF-8") 55 | w.WriteHeader(200) 56 | w.Write([]byte("LIVE" + lf)) //nolint 57 | })) 58 | defer ts.Close() 59 | adminAddress := ts.Listener.Addr().String() // ex. 127.0.0.1:55438 60 | 61 | // We don't echo the admin address intentionally as it makes tests complicated as they 62 | // would have to use regex to address the random port value. 63 | if adminAddressPath != "" { 64 | os.WriteFile(adminAddressPath, []byte(adminAddress), 0o600) 65 | } 66 | 67 | // Echo the same line Envoy would on successful startup 68 | os.Stderr.Write([]byte("starting main dispatch loop" + lf)) 69 | 70 | // Block until we receive a signal 71 | msg := "unexpected" 72 | select { 73 | case s := <-c: // Below are how Envoy 1.17 handle signals 74 | switch s { 75 | case os.Interrupt: // Ex. "kill -2 $pid", Ctrl+C or Ctrl+Break 76 | msg = "caught SIGINT" 77 | case syscall.SIGTERM: // Ex. "kill $pid" 78 | msg = "caught ENVOY_SIGTERM" 79 | } 80 | } 81 | 82 | exit(0, msg, "exiting") 83 | } 84 | 85 | func exit(ec int, messages ...string) { 86 | for _, m := range messages { 87 | os.Stderr.Write([]byte(m + lf)) //nolint 88 | } 89 | os.Exit(ec) 90 | } 91 | -------------------------------------------------------------------------------- /internal/test/morerequire/morerequire.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package morerequire includes more require functions than "github.com/stretchr/testify/require" 16 | // Do not add dependencies on any main code as it will cause cycles. 17 | package morerequire 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | "time" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | // RequireSetMtime sets the mtime of the dir given a string formatted date. Ex "2006-01-02" 28 | func RequireSetMtime(t *testing.T, dir, date string) { 29 | // Make sure we do parsing using time.Local to match the time.ModTime's location used for sorting. 30 | td, err := time.ParseInLocation("2006-01-02", date, time.Local) 31 | require.NoError(t, err) 32 | require.NoError(t, os.Chtimes(dir, td, td)) 33 | } 34 | 35 | // RequireChdir changes the working directory reverts it on the returned function 36 | func RequireChdir(t *testing.T, dir string) func() { 37 | wd, err := os.Getwd() 38 | require.NoError(t, err) 39 | require.NoError(t, os.Chdir(dir)) 40 | 41 | return func() { 42 | require.NoError(t, os.Chdir(wd)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/test/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package test 16 | 17 | import ( 18 | "crypto/sha256" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | "net/http/httptest" 24 | "os" 25 | "path" 26 | "path/filepath" 27 | "runtime" 28 | "strings" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/require" 32 | 33 | "github.com/tetratelabs/func-e/internal/moreos" 34 | "github.com/tetratelabs/func-e/internal/tar" 35 | "github.com/tetratelabs/func-e/internal/test/fakebinary" 36 | "github.com/tetratelabs/func-e/internal/version" 37 | ) 38 | 39 | const ( 40 | // FakeReleaseDate helps us make sure main code doesn't accidentally write the system time instead of the expected. 41 | FakeReleaseDate = version.ReleaseDate("2020-12-31") 42 | // Even though currently binaries are compressed with "xz" more than "gz", using "gz" in tests allows us to re-use 43 | // tar.TarGz instead of complicating internal utilities or adding dependencies only for tests. 44 | archiveFormat = ".tar.gz" 45 | versionsPath = "/versions/" 46 | ) 47 | 48 | // RequireEnvoyVersionsTestServer serves "/envoy-versions.json", containing download links a fake Envoy archive. 49 | func RequireEnvoyVersionsTestServer(t *testing.T, v version.PatchVersion) *httptest.Server { 50 | s := &server{t: t} 51 | h := httptest.NewServer(s) 52 | s.versions = version.ReleaseVersions{ 53 | Versions: map[version.PatchVersion]version.Release{ // hard-code date so that tests don't drift 54 | v: {ReleaseDate: FakeReleaseDate, Tarballs: map[version.Platform]version.TarballURL{ 55 | version.Platform(moreos.OSLinux + "/" + runtime.GOARCH): TarballURL(h.URL, moreos.OSLinux, runtime.GOARCH, v), 56 | version.Platform(moreos.OSDarwin + "/" + runtime.GOARCH): TarballURL(h.URL, moreos.OSDarwin, runtime.GOARCH, v), 57 | version.Platform(moreos.OSWindows + "/" + runtime.GOARCH): TarballURL(h.URL, moreos.OSWindows, runtime.GOARCH, v), 58 | }}}, 59 | SHA256Sums: map[version.Tarball]version.SHA256Sum{}, 60 | } 61 | fakeEnvoyTarGz, sha256Sum := RequireFakeEnvoyTarGz(s.t, v) 62 | s.fakeEnvoyTarGz = fakeEnvoyTarGz 63 | for _, u := range s.versions.Versions[v].Tarballs { 64 | s.versions.SHA256Sums[version.Tarball(path.Base(string(u)))] = sha256Sum 65 | } 66 | return h 67 | } 68 | 69 | // TarballURL gives the expected download URL for the given runtime.GOOS and Envoy version. 70 | func TarballURL(baseURL, goos, goarch string, v version.PatchVersion) version.TarballURL { 71 | var arch = "x86_64" 72 | if goarch != "arm64" { 73 | arch = goarch 74 | } 75 | return version.TarballURL(fmt.Sprintf("%s%s%s/envoy-%s-%s-%s%s", baseURL, versionsPath, v, v, goos, arch, archiveFormat)) 76 | } 77 | 78 | // server represents an HTTP server serving func-e versions. 79 | type server struct { 80 | t *testing.T 81 | versions version.ReleaseVersions 82 | fakeEnvoyTarGz []byte 83 | } 84 | 85 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 86 | switch { 87 | case r.RequestURI == "/envoy-versions.json": 88 | w.WriteHeader(http.StatusOK) 89 | _, err := w.Write(s.funcEVersions()) 90 | require.NoError(s.t, err) 91 | case strings.HasPrefix(r.RequestURI, versionsPath): 92 | subpath := r.RequestURI[len(versionsPath):] 93 | require.True(s.t, strings.HasSuffix(subpath, archiveFormat), "unexpected uri %q: expected archive suffix %q", subpath, archiveFormat) 94 | 95 | v := strings.Split(subpath, "/")[0] 96 | require.NotNil(s.t, version.NewPatchVersion(v), "unsupported version in uri %q", subpath) 97 | 98 | w.WriteHeader(http.StatusOK) 99 | _, err := w.Write(s.fakeEnvoyTarGz) 100 | require.NoError(s.t, err) 101 | default: 102 | w.WriteHeader(http.StatusNotFound) 103 | } 104 | } 105 | 106 | func (s *server) funcEVersions() []byte { 107 | data, err := json.Marshal(s.versions) 108 | require.NoError(s.t, err) 109 | return data 110 | } 111 | 112 | // RequireFakeEnvoyTarGz makes a fake envoy.tar.gz 113 | // 114 | //nolint:gosec 115 | func RequireFakeEnvoyTarGz(t *testing.T, v version.PatchVersion) ([]byte, version.SHA256Sum) { 116 | tempDir := t.TempDir() 117 | 118 | // construct the platform directory based on the input version 119 | installDir := filepath.Join(tempDir, v.String()) 120 | require.NoError(t, os.MkdirAll(filepath.Join(installDir, "bin"), 0o700)) //nolint:gosec 121 | fakebinary.RequireFakeEnvoy(t, filepath.Join(installDir, "bin", "envoy"+moreos.Exe)) 122 | 123 | // tar.gz the platform dir 124 | tempGz := filepath.Join(tempDir, "envoy.tar.gz") 125 | err := tar.TarGz(tempGz, installDir) 126 | require.NoError(t, err) 127 | 128 | // Read the tar.gz into a byte array. This allows the mock server to set content length correctly 129 | f, err := os.Open(tempGz) 130 | require.NoError(t, err) 131 | defer f.Close() //nolint 132 | b, err := io.ReadAll(f) 133 | require.NoError(t, err) 134 | return b, version.SHA256Sum(fmt.Sprintf("%x", sha256.Sum256(b))) 135 | } 136 | -------------------------------------------------------------------------------- /internal/version/.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://github.com/mvdan/sh#shfmt 2 | [last_known_envoy.txt] 3 | insert_final_newline = false 4 | trim_trailing_whitespace = true 5 | -------------------------------------------------------------------------------- /internal/version/last_known_envoy.txt: -------------------------------------------------------------------------------- 1 | 1.33.0 -------------------------------------------------------------------------------- /lint/last_known_envoy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package lint 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/tetratelabs/func-e/internal/envoy" 25 | "github.com/tetratelabs/func-e/internal/globals" 26 | "github.com/tetratelabs/func-e/internal/version" 27 | ) 28 | 29 | const lastKnownEnvoyFile = "../internal/version/last_known_envoy.txt" 30 | 31 | // TestLastKnownEnvoyAvailableOnAllPlatforms ensures that an inconsistent Envoy release doesn't end up being suggested, 32 | // or used in unit tests. This passes only when all platforms are available. This is most frequently inconsistent due to 33 | // Homebrew (macOS) being a version behind latest Linux. 34 | // 35 | // This issues a remote call to the versions server, so shouldn't be a normal unit test (as they must pass offline). 36 | // This is invoked via `make lint`. 37 | func TestLastKnownEnvoyAvailableOnAllPlatforms(t *testing.T) { 38 | getEnvoyVersions := envoy.NewGetVersions(globals.DefaultEnvoyVersionsURL, globals.DefaultPlatform, "dev") 39 | evs, err := getEnvoyVersions(context.Background()) 40 | require.NoError(t, err) 41 | 42 | var patchVersions []version.PatchVersion 43 | for v, r := range evs.Versions { 44 | if supportsAllPlatforms(r.Tarballs) { 45 | patchVersions = append(patchVersions, v) 46 | } 47 | } 48 | 49 | lastKnownEnvoy := version.FindLatestVersion(patchVersions) 50 | actual, err := os.ReadFile(lastKnownEnvoyFile) 51 | require.NoError(t, err) 52 | require.Equal(t, lastKnownEnvoy.String(), string(actual)) 53 | } 54 | 55 | // allPlatforms are the platforms that Envoy is available on, which may differ than func-e. 56 | // func-e's platforms are defined in the Makefile and are slightly wider due to the --platform flag. 57 | var allPlatforms = []version.Platform{ 58 | "linux/amd64", 59 | "linux/arm64", 60 | "darwin/amd64", 61 | "darwin/arm64", 62 | } 63 | 64 | func supportsAllPlatforms(r map[version.Platform]version.TarballURL) bool { 65 | for _, p := range allPlatforms { 66 | if _, ok := r[p]; !ok { 67 | return false 68 | } 69 | } 70 | return true 71 | } 72 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "os" 22 | "os/signal" 23 | "syscall" 24 | 25 | "github.com/urfave/cli/v2" 26 | 27 | cmdutil "github.com/tetratelabs/func-e/internal/cmd" 28 | "github.com/tetratelabs/func-e/internal/globals" 29 | "github.com/tetratelabs/func-e/internal/moreos" 30 | ) 31 | 32 | func main() { 33 | os.Exit(run(os.Stdout, os.Stderr, os.Args)) 34 | } 35 | 36 | // version is the string representation of globals.GlobalOpts 37 | // We can't use debug.ReadBuildInfo because it doesn't get the last known version properly 38 | // See https://github.com/golang/go/issues/37475 39 | var version = "dev" 40 | 41 | // run handles all error logging and coding so that no other place needs to. 42 | func run(stdout, stderr io.Writer, args []string) int { 43 | app := cmdutil.NewApp(&globals.GlobalOpts{Version: version, Out: stdout}) 44 | app.Writer = stdout 45 | app.ErrWriter = stderr 46 | app.Action = func(c *cli.Context) error { 47 | command := c.Args().First() 48 | if command == "" { // Show help by default 49 | return cli.ShowSubcommandHelp(c) 50 | } 51 | return cmdutil.NewValidationError(fmt.Sprintf("unknown command %q", command)) 52 | } 53 | app.OnUsageError = func(c *cli.Context, err error, isSub bool) error { 54 | return cmdutil.NewValidationError(err.Error()) 55 | } 56 | sigCtx, sigCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 57 | defer sigCancel() 58 | if err := app.RunContext(sigCtx, args); err != nil { 59 | if _, ok := err.(*cmdutil.ValidationError); ok { 60 | moreos.Fprintf(stderr, "%s\n", err) 61 | logUsageError(app.Name, stderr) 62 | } else { 63 | moreos.Fprintf(stderr, "error: %s\n", err) 64 | } 65 | return 1 66 | } 67 | return 0 68 | } 69 | 70 | func logUsageError(name string, stderr io.Writer) { 71 | moreos.Fprintf(stderr, "show usage with: %s help\n", name) 72 | } 73 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Tetrate 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "net/http" 20 | "net/http/httptest" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestRunErrors(t *testing.T) { 27 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 28 | w.WriteHeader(200) 29 | })) 30 | t.Cleanup(server.Close) 31 | 32 | tests := []struct { 33 | name string 34 | args []string 35 | expectedStatus int 36 | expectedStdout string 37 | expectedStderr string 38 | }{ 39 | { 40 | name: "built-in --version output", 41 | args: []string{"func-e", "--version"}, 42 | expectedStdout: `func-e version dev 43 | `, 44 | }, 45 | { 46 | name: "incorrect global flag name", 47 | args: []string{"func-e", "--d"}, 48 | expectedStatus: 1, 49 | expectedStderr: `flag provided but not defined: -d 50 | show usage with: func-e help 51 | `, 52 | }, 53 | { 54 | name: "incorrect global flag value", 55 | args: []string{"func-e", "--envoy-versions-url", ".", "list"}, 56 | expectedStatus: 1, 57 | expectedStderr: `"." is not a valid Envoy versions URL 58 | show usage with: func-e help 59 | `, 60 | }, 61 | { 62 | name: "unknown command", 63 | args: []string{"func-e", "fly"}, 64 | expectedStatus: 1, 65 | expectedStderr: `unknown command "fly" 66 | show usage with: func-e help 67 | `, 68 | }, 69 | { 70 | name: "execution error", 71 | args: []string{"func-e", "--envoy-versions-url", server.URL, "versions", "-a"}, 72 | expectedStatus: 1, 73 | expectedStderr: `error: error unmarshalling Envoy versions: unexpected end of JSON input 74 | `, 75 | }, 76 | } 77 | 78 | for _, test := range tests { 79 | test := test // pin! see https://github.com/kyoh86/scopelint for why 80 | 81 | t.Run(test.name, func(t *testing.T) { 82 | stdout := new(bytes.Buffer) 83 | stderr := new(bytes.Buffer) 84 | 85 | status := run(stdout, stderr, test.args) 86 | require.Equal(t, test.expectedStatus, status) 87 | require.Equal(t, test.expectedStdout, stdout.String()) 88 | require.Equal(t, test.expectedStderr, stderr.String()) 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "site" 3 | publish = "public" 4 | 5 | [build.environment] 6 | HUGO_VERSION = "0.109.0" 7 | 8 | [context.production] 9 | command = "git submodule update --init && hugo --gc --minify" 10 | 11 | [context.deploy-preview] 12 | command = "git submodule update --init && hugo --gc --minify -b $DEPLOY_PRIME_URL" 13 | 14 | [context.branch-deploy] 15 | command = "git submodule update --init && hugo --gc --minify -b $DEPLOY_PRIME_URL" 16 | -------------------------------------------------------------------------------- /packaging/icon@48w.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/packaging/icon@48w.ico -------------------------------------------------------------------------------- /packaging/msi/func-e.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/packaging/msi/func-e.p12 -------------------------------------------------------------------------------- /packaging/msi/func-e.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 48 | 50 | 51 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 64 | NOT NEWERVERSIONDETECTED 65 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /packaging/msi/msi_product_code.ps1: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Tetrate 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # msi_product_code.ps1 gets the ProductCode (wix Product.Id) from the given MSI file. 16 | # This must be invoked with Windows. 17 | # 18 | # Ex powershell -File ./packaging/msi/msi_product_code.ps1 -msi /path/to/func-e.msi 19 | # 20 | # See https://docs.microsoft.com/en-us/windows/win32/msi/product-codes 21 | param ([string]$msi) 22 | 23 | $windowsInstaller = New-Object -com WindowsInstaller.Installer 24 | 25 | # All the below chain from this OpenDatabase 26 | # See https://docs.microsoft.com/en-us/windows/win32/msi/installer-opendatabase 27 | $database = $windowsInstaller.GetType().InvokeMember( 28 | "OpenDatabase", "InvokeMethod", $Null, $windowsInstaller, @($msi, 0) 29 | ) 30 | 31 | # We need the ProductCode, which is what wxs Product.Id ends up as: 32 | # See https://docs.microsoft.com/en-us/windows/win32/msi/productcode 33 | $q = "SELECT Value FROM Property WHERE Property = 'ProductCode'" 34 | $View = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $database, ($q)) 35 | 36 | try { 37 | # https://docs.microsoft.com/en-us/windows/win32/msi/view-execute 38 | $View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null) | Out-Null 39 | 40 | $record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null) 41 | $productCode = $record.GetType().InvokeMember("StringData", "GetProperty", $Null, $record, 1) 42 | 43 | Write-Output $productCode 44 | } finally { 45 | if ($View) { 46 | $View.GetType().InvokeMember("Close", "InvokeMethod", $Null, $View, $Null) | Out-Null 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packaging/msi/verify_msi.cmd: -------------------------------------------------------------------------------- 1 | :: Copyright 2021 Tetrate 2 | :: 3 | :: Licensed under the Apache License, Version 2.0 (the "License"); 4 | :: you may not use this file except in compliance with the License. 5 | :: You may obtain a copy of the License at 6 | :: 7 | :: http://www.apache.org/licenses/LICENSE-2.0 8 | :: 9 | :: Unless required by applicable law or agreed to in writing, software 10 | :: distributed under the License is distributed on an "AS IS" BASIS, 11 | :: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | :: See the License for the specific language governing permissions and 13 | :: limitations under the License. 14 | 15 | :: verify_msi is written in cmd because msiexec doesn't agree with git-bash 16 | :: See https://github.com/git-for-windows/git/issues/2526 17 | @echo off 18 | if not defined MSI_FILE set MSI_FILE=dist\func-e_dev_windows_amd64.msi 19 | echo installing %MSI_FILE% 20 | msiexec /i %MSI_FILE% /qn || exit /b 1 21 | 22 | :: Use chocolatey tool to refresh the current PATH without exiting the shell 23 | call RefreshEnv.cmd 24 | 25 | echo ensuring func-e was installed 26 | func-e -version || exit /b 1 27 | 28 | echo uninstalling func-e 29 | msiexec /x %MSI_FILE% /qn || exit /b 1 30 | 31 | echo ensuring func-e was uninstalled 32 | func-e -version && exit /b 1 33 | :: success! 34 | exit /b 0 35 | -------------------------------------------------------------------------------- /packaging/msi/winget_manifest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ue 2 | # Copyright 2021 Tetrate 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # This script echos the winget manifest for a func-e Windows Installer (msi). 17 | # 18 | # Ex. 19 | # msi_file=/path/to/func-e.msi 20 | # manifest_path=./manifests/t/Tetrate/func-e/${version}/Tetrate.func-e.yaml 21 | # mkdir -p $(dirname "${manifest_path}") 22 | # packaging/msi/winget_manifest.sh ${version} ${msi_file} > ${manifest_path} 23 | # winget validate --manifest ${manifest_path} 24 | 25 | version=${1:-0.0.1} 26 | msi_file=${2:-dist/func-e_dev_windows_amd64.msi} 27 | 28 | case $(uname -s) in 29 | CYGWIN* | MSYS* | MINGW*) 30 | installer_sha256=$(certutil -hashfile "${msi_file}" SHA256 | sed -n 2p) 31 | product_code=$(powershell -File ./packaging/msi/msi_product_code.ps1 -msi "${msi_file}") 32 | ;; 33 | *) # notably, this gets rid of the Windows carriage return (\r), which otherwise would mess up the heredoc. 34 | msiinfo -h export >/dev/null 35 | # shasum -a 256, not sha256sum as https://github.com/actions/virtual-environments/issues/90 36 | installer_sha256=$(shasum -a 256 "${msi_file}" | awk '{print toupper($1)}' 2>&-) 37 | product_code=$(msiinfo export "${msi_file}" Property | sed -n '/ProductCode/s/\r$//p' | cut -f2) 38 | ;; 39 | esac 40 | 41 | cat < 22 | vendor: Tetrate 23 | description: func-e makes running Envoy® easy 24 | homepage: https://func-e.io 25 | license: Apache-2.0 26 | provides: 27 | - func-e 28 | contents: 29 | - src: build/func-e_linux_amd64/func-e 30 | dst: /usr/bin/func-e 31 | - src: packaging/nfpm/func-e.8 32 | dst: /usr/local/share/man/man8/func-e.8 33 | -------------------------------------------------------------------------------- /packaging/nfpm/verify_deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ue 2 | 3 | # Copyright 2021 Tetrate 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | case $(uname -m) in 18 | amd64* | x86_64* ) 19 | deb_arch=amd64 20 | ;; 21 | arm64* | aarch64* ) 22 | deb_arch=arm64 23 | ;; 24 | * ) 25 | >&2 echo "Unsupported hardware: $(uname -m)" 26 | exit 1; 27 | esac 28 | 29 | deb_file=${1:-$(ls dist/func-e_*_linux_${deb_arch}.deb)} 30 | 31 | echo installing "${deb_file}" 32 | sudo dpkg -i "${deb_file}" 33 | 34 | echo ensuring func-e was installed 35 | test -f /usr/bin/func-e 36 | func-e -version 37 | 38 | echo ensuring func-e man page was installed 39 | test -f /usr/local/share/man/man8/func-e.8 40 | 41 | echo uninstalling func-e 42 | sudo apt-get remove -yqq func-e 43 | 44 | echo ensuring func-e was uninstalled 45 | test -f /usr/bin/func-e && exit 1 46 | exit 0 47 | -------------------------------------------------------------------------------- /packaging/nfpm/verify_rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ue 2 | 3 | # Copyright 2021 Tetrate 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | rpm_file=${1:-$(ls dist/func-e_*_linux_$(uname -m).rpm)} 18 | 19 | echo "installing ${rpm_file}" 20 | sudo rpm -i "${rpm_file}" 21 | 22 | echo ensuring func-e was installed 23 | test -f /usr/bin/func-e 24 | func-e -version 25 | 26 | echo ensuring func-e man page was installed 27 | test -f /usr/local/share/man/man8/func-e.8 28 | 29 | echo uninstalling func-e 30 | sudo rpm -e func-e 31 | 32 | echo ensuring func-e was uninstalled 33 | test -f /usr/bin/func-e && exit 1 34 | exit 0 35 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | resources/ 2 | public/ 3 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | # func-e.io 2 | 3 | This directory holds the func-e site's source code. To visit the site click [here](https://func-e.io/) 4 | 5 | The website is built using [Hugo](https://gohugo.io/), as static website generator, and [Hello Friend](https://github.com/panr/hugo-theme-hello-friend) theme. 6 | 7 | ## Deployment process 8 | This site deploys via Netlify on change to the `master` branch. 9 | -------------------------------------------------------------------------------- /site/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://func-e.io/" 2 | languageCode = "en-us" 3 | defaultContentLanguage = "en" 4 | title = "func-e" 5 | theme = "hello-friend" 6 | 7 | [params] 8 | defaultTheme = "light" 9 | showReadingTime = false 10 | 11 | [languages] 12 | [languages.en] 13 | [languages.en.params.logo] 14 | logoText = "func-e" 15 | [languages.en.menu] 16 | [[languages.en.menu.main]] 17 | url = "/learn/" 18 | identifier = "learn" 19 | name = "Start Learning" 20 | weight = 10 21 | [[languages.en.menu.main]] 22 | url = "https://github.com/tetratelabs/func-e/blob/master/USAGE.md" 23 | name = "Usage" 24 | weight = 20 25 | [[languages.en.menu.main]] 26 | url = "https://github.com/tetratelabs/func-e/releases" 27 | name = "Releases" 28 | weight = 30 29 | [[languages.en.menu.main]] 30 | url = "https://github.com/tetratelabs/func-e" 31 | name = "GitHub" 32 | weight = 40 33 | -------------------------------------------------------------------------------- /site/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "func-e makes running Envoy® easy" 3 | layout = "single" 4 | +++ 5 | 6 | func-e (pronounced funky) allows you to quickly see available versions of Envoy and try them out. This makes it easy to validate 7 | configuration you would use in production. Each time you end a run, a snapshot of runtime state is taken on 8 | your behalf. This makes knowledge sharing and troubleshooting easier, especially when upgrading. Try it out! 9 | 10 | ```sh 11 | curl https://func-e.io/install.sh | bash -s -- -b /usr/local/bin 12 | func-e run -c /path/to/envoy.yaml 13 | ``` 14 | 15 | If you don't have a configuration file, you can start the admin port like this: 16 | ```sh 17 | func-e run --config-yaml "admin: {address: {socket_address: {address: '127.0.0.1', port_value: 9901}}}" 18 | ``` 19 | -------------------------------------------------------------------------------- /site/content/learn.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Start Learning" 3 | +++ 4 | 5 | # Learn Envoy Fundamentals 6 | 7 | Tetrate presents [**Envoy fundamentals**](https://academy.tetrate.io/courses/envoy-fundamentals), the easiest way to 8 | learn Envoy®. Envoy is an open-source edge and service proxy that is a key part of modern, cloud-native applications. 9 | This free course provides a comprehensive, eight-part tour of Envoy with concept text, labs, and quizzes. 10 | -------------------------------------------------------------------------------- /site/layouts/partials/extended_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /site/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /site/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/site/static/favicon.ico -------------------------------------------------------------------------------- /site/static/icons/icon@16w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/site/static/icons/icon@16w.png -------------------------------------------------------------------------------- /site/static/icons/icon@180w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/site/static/icons/icon@180w.png -------------------------------------------------------------------------------- /site/static/icons/icon@192w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/site/static/icons/icon@192w.png -------------------------------------------------------------------------------- /site/static/icons/icon@32w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/site/static/icons/icon@32w.png -------------------------------------------------------------------------------- /site/static/icons/icon@512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tetratelabs/func-e/b46a029a6395bc759049c02a753a9f49eabe0c28/site/static/icons/icon@512w.png -------------------------------------------------------------------------------- /site/static/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "func-e", 3 | "short_name": "func-e", 4 | "description": "func-e (pronounced funky) makes running Envoy® easy", 5 | "start_url": "/", 6 | "background_color": "#fff", 7 | "theme_color": "#222", 8 | "icons": [ 9 | { 10 | "src": "icons/icon@16w.png", 11 | "sizes": "16x16", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "icons/icon@32w.png", 16 | "sizes": "32x32", 17 | "type": "image/png" 18 | }, 19 | { 20 | "src": "icons/icon@180w.png", 21 | "sizes": "180x180", 22 | "type": "image/png" 23 | }, 24 | { 25 | "src": "icons/icon@192w.png", 26 | "sizes": "192x192", 27 | "type": "image/png" 28 | }, 29 | { 30 | "src": "icons/icon@512w.png", 31 | "sizes": "512x512", 32 | "type": "image/png" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /site/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /install.sh 3 | --------------------------------------------------------------------------------