diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..e857e1cd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,194 @@ +name: build + +on: [workflow_dispatch, push] + +env: + PKG_NAME: "http-echo" + +jobs: + get-go-version: + name: "Determine Go toolchain version" + runs-on: ubuntu-latest + outputs: + go-version: ${{ steps.get-go-version.outputs.go-version }} + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + # This is the minimum required version. Go 1.21+ should pull in new + # toolchains as indicated in go.mod. + go-version: '1.21' + - name: Determine Go version + id: get-go-version + # With go1.21+, we can use `go version` to print out the exact toolchain that will be used + run: | + echo "Building with Go $(go version | awk '{print $3}' | sed -e 's/^go//')" + echo "go-version=$(go version | awk '{print $3}' | sed -e 's/^go//')" >> $GITHUB_OUTPUT + + set-product-version: + runs-on: ubuntu-latest + outputs: + product-version: ${{ steps.set-product-version.outputs.product-version }} + product-base-version: ${{ steps.set-product-version.outputs.base-product-version }} + product-prerelease-version: ${{ steps.set-product-version.outputs.prerelease-product-version }} + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set Product version + id: set-product-version + uses: hashicorp/actions-set-product-version@v1 + + generate-metadata-file: + needs: set-product-version + runs-on: ubuntu-latest + outputs: + filepath: ${{ steps.generate-metadata-file.outputs.filepath }} + steps: + - name: "Checkout directory" + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Generate metadata file + id: generate-metadata-file + uses: hashicorp/actions-generate-metadata@v1 + with: + version: ${{ needs.set-product-version.outputs.product-version }} + product: ${{ env.PKG_NAME }} + repositoryOwner: "hashicorp" + repository: "http-echo" + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: metadata.json + path: ${{ steps.generate-metadata-file.outputs.filepath }} + + build-other: + needs: + - get-go-version + - set-product-version + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + goos: [windows] + goarch: ["386", "amd64"] + + name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build + + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - uses: hashicorp/actions-go-build@v0.1.7 + env: + BASE_VERSION: ${{ needs.set-product-version.outputs.product-base-version }} + PRERELEASE_VERSION: ${{ needs.set-product-version.outputs.product-prerelease-version}} + METADATA_VERSION: ${{ env.METADATA }} + with: + product_name: ${{ env.PKG_NAME }} + product_version: ${{ needs.set-product-version.outputs.product-version }} + go_version: ${{ needs.get-go-version.outputs.go-version }} + os: ${{ matrix.goos }} + arch: ${{ matrix.goarch }} + reproducible: report + instructions: | + make build + + build-linux: + needs: + - get-go-version + - set-product-version + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: ["arm", "arm64", "386", "amd64"] + + fail-fast: true + + name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build + + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - uses: hashicorp/actions-go-build@v0.1.7 + env: + BASE_VERSION: ${{ needs.set-product-version.outputs.product-base-version }} + PRERELEASE_VERSION: ${{ needs.set-product-version.outputs.product-prerelease-version}} + METADATA_VERSION: ${{ env.METADATA }} + with: + product_name: ${{ env.PKG_NAME }} + product_version: ${{ needs.set-product-version.outputs.product-version }} + go_version: ${{ needs.get-go-version.outputs.go-version }} + os: ${{ matrix.goos }} + arch: ${{ matrix.goarch }} + reproducible: report + instructions: | + make build + + build-darwin: + needs: + - get-go-version + - set-product-version + runs-on: macos-latest + strategy: + matrix: + goos: [darwin] + goarch: ["amd64", "arm64"] + fail-fast: true + + name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build + + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - uses: hashicorp/actions-go-build@v0.1.7 + env: + BASE_VERSION: ${{ needs.set-product-version.outputs.product-base-version }} + PRERELEASE_VERSION: ${{ needs.set-product-version.outputs.product-prerelease-version}} + METADATA_VERSION: ${{ env.METADATA }} + with: + product_name: ${{ env.PKG_NAME }} + product_version: ${{ needs.set-product-version.outputs.product-version }} + go_version: ${{ needs.get-go-version.outputs.go-version }} + os: ${{ matrix.goos }} + arch: ${{ matrix.goarch }} + reproducible: report + instructions: | + make build + + build-docker-default: + name: Docker ${{ matrix.arch }} default release build + needs: + - set-product-version + - build-linux + runs-on: ubuntu-latest + strategy: + matrix: + arch: ["arm64", "amd64"] + env: + repo: ${{ github.event.repository.name }} + version: ${{ needs.set-product-version.outputs.product-version }} + + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Docker Build (Action) + uses: hashicorp/actions-docker-build@v1 + with: + smoke_test: | + TEST_VERSION="$(docker run "${IMAGE_NAME}" -version | awk '/http-echo/{print $2}' | sed s/v//)" + if [ "${TEST_VERSION}" != "${version}" ]; then + echo "Test FAILED" + echo "Test Version: ${TEST_VERSION}" + echo "Version: ${version}" + exit 1 + fi + echo "Test PASSED" + version: ${{ env.version }} + target: default + arch: ${{ matrix.arch }} + tags: | + docker.io/hashicorp/${{env.repo}}:${{env.version}} + dev_tags: | + docker.io/hashicorppreview/${{ env.repo }}:${{ env.version }}-dev + docker.io/hashicorppreview/${{ env.repo }}:${{ env.version }}-${{ github.sha }} + diff --git a/.gitignore b/.gitignore index 774e6890..4cb1337b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ### Custom ### /bin /pkg +dist/ ### Golang ### # Compiled Object files, Static and Dynamic libs (Shared Objects) diff --git a/.release/ci.hcl b/.release/ci.hcl new file mode 100644 index 00000000..e69de29b diff --git a/.release/release.metadata.hcl b/.release/release.metadata.hcl new file mode 100644 index 00000000..e69de29b diff --git a/.release/security-scan.hcl b/.release/security-scan.hcl new file mode 100644 index 00000000..e69de29b diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..efde1de9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +FROM gcr.io/distroless/static-debian12:nonroot as default + +# TARGETOS and TARGETARCH are set automatically when --platform is provided. +ARG TARGETOS +ARG TARGETARCH +ARG PRODUCT_VERSION +ARG BIN_NAME + +LABEL name="http-echo" \ + maintainer="HashiCorp Consul Team " \ + vendor="HashiCorp" \ + version=$PRODUCT_VERSION \ + release=$PRODUCT_VERSION \ + summary="A test webserver that echos a response. You know, for kids." + +COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME / + +EXPOSE 5678/tcp + +ENV ECHO_TEXT="hello-world" + +ENTRYPOINT ["/http-echo"] diff --git a/Makefile b/Makefile index ee7a6d8c..27bbfbf8 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ CURRENT_DIR := $(patsubst %/,%,$(dir $(realpath $(MKFILE_PATH)))) # Ensure GOPATH GOPATH ?= $(HOME)/go +GOBIN ?= $(GOPATH)/bin # List all our actual files, excluding vendor GOFILES ?= $(shell go list $(TEST) | grep -v /vendor/) @@ -15,137 +16,87 @@ GOTAGS ?= GOMAXPROCS ?= 4 # Get the project metadata -GOVERSION := 1.19 +GOVERSION := $(shell go version | awk '{print $3}' | sed -e 's/^go//') PROJECT := $(CURRENT_DIR:$(GOPATH)/src/%=%) OWNER ?= hashicorp NAME ?= http-echo -GIT_COMMIT ?= $(shell git rev-parse --short HEAD) -VERSION := $(shell awk -F\" '/Version/ { print $$2; exit }' "${CURRENT_DIR}/version/version.go") - -# Current system information -GOOS ?= $(shell go env GOOS) -GOARCH ?= $(shell go env GOARCH) +REVISION ?= $(shell git rev-parse --short HEAD) +VERSION := $(shell cat "${CURRENT_DIR}/version/VERSION") +TIMESTAMP := $(shell date) + +# Get local ARCH; on Intel Mac, 'uname -m' returns x86_64 which we turn into amd64. +# Not using 'go env GOOS/GOARCH' here so 'make docker' will work without local Go install. +ARCH ?= $(shell A=$$(uname -m); [ $$A = x86_64 ] && A=amd64; echo $$A) +OS ?= $(shell uname | tr [[:upper:]] [[:lower:]]) +PLATFORM = $(OS)/$(ARCH) +DIST = dist/$(PLATFORM) +BIN = $(DIST)/$(NAME) # Default os-arch combination to build XC_OS ?= darwin linux windows XC_ARCH ?= amd64 arm64 XC_EXCLUDE ?= -# GPG Signing key (blank by default, means no GPG signing) -GPG_KEY ?= - # List of ldflags LD_FLAGS ?= \ -s \ -w \ - -X 'github.com/hashicorp/http-echo/version.Name=${NAME}' \ - -X 'github.com/hashicorp/http-echo/version.GitCommit=${GIT_COMMIT}' - -# List of Docker targets to build -DOCKER_TARGETS ?= scratch alpine + -X 'github.com/hashicorp/http-echo/version.Version=${VERSION}' \ + -X 'github.com/hashicorp/http-echo/version.GitCommit=${REVISION}' \ + -X 'github.com/hashicorp/http-echo/version.Timestamp=${TIMESTAMP}' # List of tests to run TEST ?= ./... -# Create a cross-compile target for every os-arch pairing. This will generate -# a make target for each os/arch like "make linux/amd64" as well as generate a -# meta target (build) for compiling everything. -define make-xc-target - $1/$2: - ifneq (,$(findstring ${1}/${2},$(XC_EXCLUDE))) - @printf "%s%20s %s\n" "-->" "${1}/${2}:" "${PROJECT} (excluded)" - else - @printf "%s%20s %s\n" "-->" "${1}/${2}:" "${PROJECT}" - @docker run \ - --interactive \ - --rm \ - --dns="8.8.8.8" \ - --volume="${CURRENT_DIR}:/go/src/${PROJECT}" \ - --workdir="/go/src/${PROJECT}" \ - "golang:${GOVERSION}" \ - env \ - CGO_ENABLED="0" \ - GOOS="${1}" \ - GOARCH="${2}" \ - go build \ - -a \ - -o="pkg/${1}_${2}/${NAME}${3}" \ - -ldflags "${LD_FLAGS}" \ - -tags "${GOTAGS}" - endif - .PHONY: $1/$2 - - $1:: $1/$2 - .PHONY: $1 - - build:: $1/$2 - .PHONY: build -endef -$(foreach goarch,$(XC_ARCH),$(foreach goos,$(XC_OS),$(eval $(call make-xc-target,$(goos),$(goarch),$(if $(findstring windows,$(goos)),.exe,))))) +version: + @echo $(VERSION) +.PHONY: version + + +dist: + mkdir -p $(DIST) + +# build is used for the CRT build.yml workflow. +# Environment variables are populated by hashicorp/actions-go-build, not the makefile. +# https://github.com/hashicorp/actions-go-build +build: + CGO_ENABLED=0 go build \ + -a \ + -o="${BIN_PATH}" \ + -ldflags " \ + -X 'github.com/hashicorp/http-echo/version.Version=${PRODUCT_VERSION}' \ + -X 'github.com/hashicorp/http-echo/version.GitCommit=${PRODUCT_REVISION}' \ + -X 'github.com/hashicorp/http-echo/version.Timestamp=${PRODUCT_REVISION_TIME}' \ + " \ + -tags "${GOTAGS}" \ + -trimpath \ + -buildvcs=false +.PHONY: build + +bin: dist + @echo "==> Building ${BIN}" + @GOARCH=$(ARCH) GOOS=$(OS) CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags="$(LD_FLAGS)" -o $(BIN) +.PHONY: bin # dev builds and installs the project locally. -dev: - @echo "==> Installing ${NAME} for ${GOOS}/${GOARCH}" - @rm -f "${GOPATH}/pkg/${GOOS}_${GOARCH}/${PROJECT}/version.a" # ldflags change and go doesn't detect - @env \ - CGO_ENABLED="0" \ - go install \ - -ldflags "${LD_FLAGS}" \ - -tags "${GOTAGS}" +dev: bin + cp $(BIN) $(GOBIN)/$(BIN_NAME) .PHONY: dev -# dist builds the binaries and then signs and packages them for distribution -dist: -ifndef GPG_KEY - @echo "==> ERROR: No GPG key specified! Without a GPG key, this release cannot" - @echo " be signed. Set the environment variable GPG_KEY to the ID of" - @echo " the GPG key to continue." - @exit 127 -else - @$(MAKE) -f "${MKFILE_PATH}" _cleanup - @$(MAKE) -f "${MKFILE_PATH}" -j4 build - @$(MAKE) -f "${MKFILE_PATH}" _compress _checksum _sign -endif -.PHONY: dist - -# Create a docker compile and push target for each container. This will create -# docker-build/scratch, docker-push/scratch, etc. It will also create two meta -# targets: docker-build and docker-push, which will build and push all -# configured Docker containers. Each container must have a folder in docker/ -# named after itself with a Dockerfile (docker/alpine/Dockerfile). -define make-docker-target - docker-build/$1: - @echo "==> Building ${1} Docker container for ${PROJECT}" - @docker buildx create --use && docker buildx build \ - --rm \ - --force-rm \ - --no-cache \ - --compress \ - --file="docker/${1}/Dockerfile" \ - --platform linux/amd64,linux/arm64 \ - --build-arg="LD_FLAGS=${LD_FLAGS}" \ - --build-arg="GOTAGS=${GOTAGS}" \ - $(if $(filter $1,scratch),--tag="${OWNER}/${NAME}",) \ - --tag="${OWNER}/${NAME}:${1}" \ - --tag="${OWNER}/${NAME}:${VERSION}-${1}" \ - "${CURRENT_DIR}" \ - --push - .PHONY: docker-build/$1 - - docker-build:: docker-build/$1 - .PHONY: docker-build - - docker-push/$1: - @echo "==> Pushing ${1} to Docker registry" - $(if $(filter $1,scratch),@docker push "${OWNER}/${NAME}",) - @docker push "${OWNER}/${NAME}:${1}" - @docker push "${OWNER}/${NAME}:${VERSION}-${1}" - .PHONY: docker-push/$1 - - docker-push:: docker-push/$1 - .PHONY: docker-push -endef -$(foreach target,$(DOCKER_TARGETS),$(eval $(call make-docker-target,$(target)))) +# Docker Stuff. +export DOCKER_BUILDKIT=1 +BUILD_ARGS = BIN_NAME=http-echo PRODUCT_VERSION=$(VERSION) +TAG = $(NAME):local +BA_FLAGS = $(addprefix --build-arg=,$(BUILD_ARGS)) +FLAGS = --platform $(PLATFORM) --tag $(TAG) $(BA_FLAGS) + +# Set OS to linux for all docker/* targets. +docker: OS = linux + +docker: bin + docker build ${FLAGS} . + @echo 'Image built; run "docker run --rm ${TAG}" to try it out.' +.PHONY: docker # test runs the test suite. test: @@ -159,61 +110,9 @@ test-race: @go test -timeout=60s -race -tags="${GOTAGS}" ${GOFILES} ${TESTARGS} .PHONY: test-race -# _cleanup removes any previous binaries -_cleanup: +# clean removes any previous binaries +clean: @rm -rf "${CURRENT_DIR}/pkg/" @rm -rf "${CURRENT_DIR}/bin/" +.PHONY: clean -# _compress compresses all the binaries in pkg/* as tarball and zip. -_compress: - @mkdir -p "${CURRENT_DIR}/pkg/dist" - @for platform in $$(find ./pkg -mindepth 1 -maxdepth 1 -type d); do \ - osarch=$$(basename "$$platform"); \ - if [ "$$osarch" = "dist" ]; then \ - continue; \ - fi; \ - \ - ext=""; \ - if test -z "$${osarch##*windows*}"; then \ - ext=".exe"; \ - fi; \ - cd "$$platform"; \ - tar -czf "${CURRENT_DIR}/pkg/dist/${NAME}_${VERSION}_$${osarch}.tgz" "${NAME}$${ext}"; \ - zip -q "${CURRENT_DIR}/pkg/dist/${NAME}_${VERSION}_$${osarch}.zip" "${NAME}$${ext}"; \ - cd - &>/dev/null; \ - done -.PHONY: _compress - -# _checksum produces the checksums for the binaries in pkg/dist -_checksum: - @cd "${CURRENT_DIR}/pkg/dist" && \ - shasum --algorithm 256 * > ${CURRENT_DIR}/pkg/dist/${NAME}_${VERSION}_SHA256SUMS && \ - cd - &>/dev/null -.PHONY: _checksum - -# _sign signs the binaries using the given GPG_KEY. This should not be called -# as a separate function. -_sign: - @echo "==> Signing ${PROJECT} at v${VERSION}" - @gpg \ - --default-key "${GPG_KEY}" \ - --detach-sig "${CURRENT_DIR}/pkg/dist/${NAME}_${VERSION}_SHA256SUMS" - @git commit \ - --allow-empty \ - --gpg-sign="${GPG_KEY}" \ - --message "Release v${VERSION}" \ - --quiet \ - --signoff - @git tag \ - --annotate \ - --create-reflog \ - --local-user "${GPG_KEY}" \ - --message "Version ${VERSION}" \ - --sign \ - "v${VERSION}" main - @echo "--> Do not forget to run:" - @echo "" - @echo " git push && git push --tags" - @echo "" - @echo "And then upload the binaries in dist/!" -.PHONY: _sign diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile deleted file mode 100644 index 9e6ff8a0..00000000 --- a/docker/alpine/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -FROM alpine:3.17.1 - -# TARGETOS and TARGETARCH are set automatically when --platform is provided. -ARG TARGETOS -ARG TARGETARCH - -ADD "https://curl.haxx.se/ca/cacert.pem" "/etc/ssl/certs/ca-certificates.crt" -ADD "./pkg/${TARGETOS}_${TARGETARCH}/http-echo" "/" -RUN apk add curl - -# Create a non-root user to run the software. -RUN addgroup http-echo -RUN adduser -S -G http-echo 1005 - -USER 1005 -ENTRYPOINT ["/http-echo"] diff --git a/docker/scratch/Dockerfile b/docker/scratch/Dockerfile deleted file mode 100644 index 0b2f2268..00000000 --- a/docker/scratch/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -FROM ubuntu:latest -RUN useradd -u 1005 scratchuser - -FROM scratch - -# TARGETOS and TARGETARCH are set automatically when --platform is provided. -ARG TARGETOS -ARG TARGETARCH - -ADD "https://curl.haxx.se/ca/cacert.pem" "/etc/ssl/certs/ca-certificates.crt" -ADD "./pkg/${TARGETOS}_${TARGETARCH}/http-echo" "/" -COPY --from=0 /etc/passwd /etc/passwd - -USER scratchuser - -ENTRYPOINT ["/http-echo"] diff --git a/go.mod b/go.mod index 0459540d..219ccdb9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/hashicorp/http-echo -go 1.19 +go 1.21.0 + +toolchain go1.21.1 diff --git a/main.go b/main.go index 42e6df25..281c4f21 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { // Asking for the version? if *versionFlag { - fmt.Fprintln(stderrW, version.HumanVersion) + fmt.Fprintln(stdoutW, version.HumanVersion) os.Exit(0) } diff --git a/version/VERSION b/version/VERSION new file mode 100644 index 00000000..3eefcb9d --- /dev/null +++ b/version/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/version/version.go b/version/version.go index ff7688e2..650dcbdd 100644 --- a/version/version.go +++ b/version/version.go @@ -5,11 +5,12 @@ package version import "fmt" -const Version = "0.2.4" +const Name = "http-echo" var ( - Name string GitCommit string + Version string + Timestamp string - HumanVersion = fmt.Sprintf("%s v%s (%s)", Name, Version, GitCommit) + HumanVersion = fmt.Sprintf("%s v%s (%s)\nBuilt: %s", Name, Version, GitCommit, Timestamp) )