diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000000000000000000000000000000000..10631f0665062750b2a1f2c4e538df4d6bc03f23 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,8 @@ +coverage: + range: 70..90 # First number represents red, and second represents green. + status: + patch: false + project: + default: + # Allow going dows 1% before being a failure. + threshold: 1% \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000000000000000000000000000000..609f0d426adf0d3f37e1968b9c167c9289d64ade --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @slok + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..c751154c8089a145d3b442b7ed248c22a0f34a95 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/docker/dev" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/docker/prod" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000000000000000000000000000000000..33ae1be44ccba60af448c3885ae252ea0dfaa9a8 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,96 @@ +name: CI + +on: [push, pull_request] + +jobs: + check: + name: Check + runs-on: ubuntu-latest + # Execute the checks inside the container instead the VM. + container: golangci/golangci-lint:v1.37.1-alpine + steps: + - uses: actions/checkout@v2 + - run: ./scripts/check/check.sh + + unit-test: + name: Unit test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.16 + - run: make ci-test + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} + file: ./.test_coverage.txt + fail_ci_if_error: false + + rolling-release-image: + # Only on main branch. + if: startsWith(github.ref, 'refs/heads/main') + env: + TAG_IMAGE_LATEST: "true" + PROD_IMAGE_NAME: ${GITHUB_REPOSITORY} + VERSION: ${GITHUB_SHA} + needs: [check, unit-test] + name: Release image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build image + run: make build-image + - name: Docker login + run: docker login ${DOCKER_HOST} -u ${DOCKER_USER} -p ${DOCKER_TOKEN} + env: + DOCKER_HOST: "" + DOCKER_USER: slok + DOCKER_TOKEN: ${{secrets.DOCKER_HUB_TOKEN}} + - name: Publish image + run: make publish-image + + tagged-release-image: + # Only on tags. + if: startsWith(github.ref, 'refs/tags/') + env: + PROD_IMAGE_NAME: ${GITHUB_REPOSITORY} + needs: [check, unit-test] + name: Tagged release image + runs-on: ubuntu-latest + steps: + - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. + - uses: actions/checkout@v2 + - name: Build image + run: make build-image + - name: Docker login + run: docker login ${DOCKER_HOST} -u ${DOCKER_USER} -p ${DOCKER_TOKEN} + env: + DOCKER_HOST: "" + DOCKER_USER: slok + DOCKER_TOKEN: ${{secrets.DOCKER_HUB_TOKEN}} + - name: Publish image + run: make publish-image + + tagged-release-binaries: + # Only on tags. + if: startsWith(github.ref, 'refs/tags/') + needs: [check, unit-test] + name: Tagged release binaries + runs-on: ubuntu-latest + steps: + - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. + - uses: actions/checkout@v2 + - name: Build binaries + run: | + mkdir -p ./bin + chmod -R 0777 ./bin + make build-all + - name: Upload binaries + uses: xresloader/upload-to-github-release@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + file: "bin/*" + tags: true + draft: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9b746dd29679b4e1d09315eff65e263c0fb417bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Vendor directory +vendor/ + +# Test coverage. +.test_coverage.txt + +# Binaries +bin/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1e51c291d3c13c840023d6767eb6995863116b20 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,5 @@ +--- + +run: + build-tags: + - integration diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..2d5331af9de02a82c372d69bad0b3b882243a200 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## [Unreleased] + +[unreleased]: https://github.com/slok/sloth/compare/v0.1.0...HEAD +[v0.1.0]: https://github.com/slok/sloth/releases/tag/v0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..10f6571ffee0d9c96eec8853cdfdbd4b3f2c0b57 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2021] [Xabier Larrakoetxea] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..a1a8560174f9a400a9d0fda61008bb97b40db688 --- /dev/null +++ b/Makefile @@ -0,0 +1,86 @@ + +SHELL := $(shell which bash) +OSTYPE := $(shell uname) +DOCKER := $(shell command -v docker) +GID := $(shell id -g) +UID := $(shell id -u) +VERSION ?= $(shell git describe --tags --always) + +UNIT_TEST_CMD := ./scripts/check/unit-test.sh +INTEGRATION_TEST_CMD := ./scripts/check/integration-test.sh +CHECK_CMD := ./scripts/check/check.sh + +DEV_IMAGE_NAME := slok/sloth-dev +PROD_IMAGE_NAME ?= slok/sloth + +DOCKER_RUN_CMD := docker run --env ostype=$(OSTYPE) -v ${PWD}:/src --rm ${DEV_IMAGE_NAME} +BUILD_BINARY_CMD := VERSION=${VERSION} ./scripts/build/build.sh +BUILD_BINARY_ALL_CMD := VERSION=${VERSION} ./scripts/build/build-all.sh +BUILD_DEV_IMAGE_CMD := IMAGE=${DEV_IMAGE_NAME} DOCKER_FILE_PATH=./docker/dev/Dockerfile VERSION=latest ./scripts/build/build-image.sh +BUILD_PROD_IMAGE_CMD := IMAGE=${PROD_IMAGE_NAME} DOCKER_FILE_PATH=./docker/prod/Dockerfile VERSION=${VERSION} ./scripts/build/build-image.sh +PUBLISH_PROD_IMAGE_CMD := IMAGE=${PROD_IMAGE_NAME} VERSION=${VERSION} ./scripts/build/publish-image.sh + + +help: ## Show this help + @echo "Help" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[93m %s\n", $$1, $$2}' + +.PHONY: default +default: help + +.PHONY: build-image +build-image: ## Builds the production docker image. + @$(BUILD_PROD_IMAGE_CMD) + +.PHONY: publish-image +publish-image: ##Publishes the production docker image. + @$(PUBLISH_PROD_IMAGE_CMD) + +.PHONY: build-dev-image +build-dev-image: ## Builds the development docker image. + @$(BUILD_DEV_IMAGE_CMD) + +build: build-dev-image ## Builds the production binary. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(BUILD_BINARY_CMD)' + +build-all: build-dev-image ## Builds all archs production binaries. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(BUILD_BINARY_ALL_CMD)' + +.PHONY: test +test: build-dev-image ## Runs unit test. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(UNIT_TEST_CMD)' + +.PHONY: check +check: build-dev-image ## Runs checks. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(CHECK_CMD)' + +.PHONY: integration +integration: build-dev-image ## Runs integration test. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(INTEGRATION_TEST_CMD)' + +.PHONY: go-gen +go-gen: build-dev-image ## Generates go based code. + @$(DOCKER_RUN_CMD) /bin/sh -c './scripts/gogen.sh' + +.PHONY: gen +gen: go-gen ## Generates all. + +.PHONY: deps +deps: ## Fixes the dependencies + @$(DOCKER_RUN_CMD) /bin/sh -c './scripts/deps.sh' + +.PHONY: ci-build +ci-build: ## Builds the production binary in CI environment (without docker). + @$(BUILD_BINARY_CMD) + +.PHONY: ci-unit-test +ci-test: ## Runs unit test in CI environment (without docker). + @$(UNIT_TEST_CMD) + +.PHONY: ci-check +ci-check: ## Runs checks in CI environment (without docker). + @$(CHECK_CMD) + +.PHONY: ci-integration-test +ci-integration: ## Runs integraton test in CI environment (without docker). + @$(INTEGRATION_TEST_CMD) diff --git a/cmd/sloth/commands/init.go b/cmd/sloth/commands/init.go new file mode 100644 index 0000000000000000000000000000000000000000..53c1ac5f63494cf92200d7f01295bddd8844409d --- /dev/null +++ b/cmd/sloth/commands/init.go @@ -0,0 +1,21 @@ +package commands + +import ( + "context" + "fmt" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type generateCommand struct{} + +// NewGenerateCommand returns the generate command. +func NewGenerateCommand(app *kingpin.Application) Command { + app.Command("generate", "Generates SLOs.") + return generateCommand{} +} + +func (g generateCommand) Name() string { return "generate" } +func (g generateCommand) Run(ctx context.Context, config RootConfig) error { + return fmt.Errorf("not implemented") +} diff --git a/cmd/sloth/commands/root.go b/cmd/sloth/commands/root.go new file mode 100644 index 0000000000000000000000000000000000000000..9be32e58f2fa3cfd8f5081dd0966df47ceb387e1 --- /dev/null +++ b/cmd/sloth/commands/root.go @@ -0,0 +1,53 @@ +package commands + +import ( + "context" + "io" + + "gopkg.in/alecthomas/kingpin.v2" + + "github.com/slok/sloth/internal/log" +) + +const ( + // LoggerTypeDefault is the logger default type. + LoggerTypeDefault = "default" + // LoggerTypeJSON is the logger json type. + LoggerTypeJSON = "json" +) + +// Command represents an application command, all commands that want to be executed +// should implement and setup on main. +type Command interface { + Name() string + Run(ctx context.Context, config RootConfig) error +} + +// RootConfig represents the root command configuration and global configuration +// for all the commands. +type RootConfig struct { + // Global flags. + Debug bool + NoLog bool + NoColor bool + LoggerType string + + // Global instances. + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + Logger log.Logger +} + +// NewRootConfig initializes the main root configuration +func NewRootConfig(app *kingpin.Application) *RootConfig { + c := &RootConfig{} + + // Register. + app.Flag("debug", "Enable debug mode.").BoolVar(&c.Debug) + app.Flag("no-log", "Disable logger.").BoolVar(&c.NoLog) + app.Flag("no-color", "Disable logger color.").BoolVar(&c.NoColor) + app.Flag("logger", "Selects the logger type.").Default(LoggerTypeDefault).EnumVar(&c.LoggerType, LoggerTypeDefault, LoggerTypeJSON) + + return c +} diff --git a/cmd/sloth/main.go b/cmd/sloth/main.go new file mode 100644 index 0000000000000000000000000000000000000000..614320fe769f6ff4321347bc62f97c36dae5bfb8 --- /dev/null +++ b/cmd/sloth/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/sirupsen/logrus" + "gopkg.in/alecthomas/kingpin.v2" + + "github.com/slok/sloth/cmd/sloth/commands" + "github.com/slok/sloth/internal/log" + loglogrus "github.com/slok/sloth/internal/log/logrus" +) + +// Version is the application version. +var Version = "dev" + +// Run runs the main application. +func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + app := kingpin.New("sloth", "Easy SLO generator.") + app.Version(Version) + app.DefaultEnvars() + config := commands.NewRootConfig(app) + + // Setup commands (registers flags). + generateCmd := commands.NewGenerateCommand(app) + + cmds := map[string]commands.Command{ + generateCmd.Name(): generateCmd, + } + + // Parse commandline. + cmdName, err := app.Parse(args[1:]) + if err != nil { + return fmt.Errorf("invalid command configuration: %w", err) + } + + // Set up global dependencies. + config.Stdin = stdin + config.Stdout = stdout + config.Stderr = stderr + config.Logger = getLogger(*config, stderr) + + // Execute command. + err = cmds[cmdName].Run(ctx, *config) + if err != nil { + return fmt.Errorf("%q command failed: %w", cmdName, err) + } + + return nil +} + +// getLogger returns the application logger. +func getLogger(config commands.RootConfig, stderr io.Writer) log.Logger { + if config.NoLog { + return log.Noop + } + + // If not logger disabled use logrus logger. + logrusLog := logrus.New() + logrusLog.Out = stderr // By default logger goes to stderr (so it can split stdout prints). + logrusLogEntry := logrus.NewEntry(logrusLog) + + if config.Debug { + logrusLogEntry.Logger.SetLevel(logrus.DebugLevel) + } + + // Log format. + switch config.LoggerType { + case commands.LoggerTypeDefault: + logrusLogEntry.Logger.SetFormatter(&logrus.TextFormatter{ + ForceColors: !config.NoColor, + DisableColors: config.NoColor, + }) + case commands.LoggerTypeJSON: + logrusLogEntry.Logger.SetFormatter(&logrus.JSONFormatter{}) + } + + logger := loglogrus.NewLogrus(logrusLogEntry).WithValues(log.Kv{ + "version": Version, + }) + + logger.Debugf("Debug level is enabled") // Will log only when debug enabled. + + return logger +} + +func main() { + ctx := context.Background() + err := Run(ctx, os.Args, os.Stdin, os.Stdout, os.Stderr) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %s", err) + os.Exit(1) + } +} diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..82471b1f017c56e754054cdf88b81f648b742914 --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,39 @@ +FROM golang:1.16 + +ARG GOLANGCI_LINT_VERSION="1.37.1" +ARG MOCKERY_VERSION="2.5.1" +ARG ostype=Linux + +RUN apt-get update && apt-get install -y \ + git \ + bash \ + zip + + +RUN wget https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz && \ + tar zxvf golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz --strip 1 -C /usr/local/bin/ && \ + rm golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz && \ + \ + wget https://github.com/vektra/mockery/releases/download/v${MOCKERY_VERSION}/mockery_${MOCKERY_VERSION}_Linux_x86_64.tar.gz && \ + tar zxvf mockery_${MOCKERY_VERSION}_Linux_x86_64.tar.gz -C /tmp && \ + mv /tmp/mockery /usr/local/bin/ && \ + rm mockery_${MOCKERY_VERSION}_Linux_x86_64.tar.gz + +# Create user. +ARG uid=1000 +ARG gid=1000 + +RUN bash -c 'if [ ${ostype} == Linux ]; then addgroup -gid $gid app; else addgroup app; fi && \ + adduser --disabled-password -uid $uid --ingroup app --gecos "" app && \ + chown app:app -R /go' + +# Fill go mod cache. +RUN mkdir /tmp/cache +COPY go.mod /tmp/cache +COPY go.sum /tmp/cache +RUN chown app:app -R /tmp/cache +USER app +RUN cd /tmp/cache && \ + go mod download + +WORKDIR /src diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1d819b62078ed60baaa8226a24fdce77b1e79bbd --- /dev/null +++ b/docker/prod/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.16-alpine as build-stage + +RUN apk --no-cache add \ + g++ \ + git \ + make \ + curl \ + bash + +ARG VERSION +ENV VERSION=${VERSION} + +# Compile. +WORKDIR /src +COPY . . +RUN ./scripts/build/build.sh + + +FROM alpine:latest + +RUN apk --no-cache add \ + ca-certificates \ + bash + +COPY --from=build-stage /src/bin/sloth /usr/local/bin/sloth + +ENTRYPOINT ["/usr/local/bin/sloth"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..2d423c65dc5154ca003d8d906642da11e856d876 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/slok/sloth + +go 1.16 + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect + github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/testify v1.7.0 // indirect + gopkg.in/alecthomas/kingpin.v2 v2.2.6 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..bc98665a934da93f59c925e622acb0025c064599 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000000000000000000000000000000000000..1c6539570cd29375c7b6b8cc087f213c09238970 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,66 @@ +package log + +import "context" + +// Kv is a helper type for structured logging fields usage. +type Kv = map[string]interface{} + +// Logger is the interface that the loggers used by the library will use. +type Logger interface { + Infof(format string, args ...interface{}) + Warningf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Debugf(format string, args ...interface{}) + WithValues(values map[string]interface{}) Logger + WithCtxValues(ctx context.Context) Logger + SetValuesOnCtx(parent context.Context, values map[string]interface{}) context.Context +} + +// Noop logger doesn't log anything. +const Noop = noop(0) + +type noop int + +func (n noop) Infof(format string, args ...interface{}) {} +func (n noop) Warningf(format string, args ...interface{}) {} +func (n noop) Errorf(format string, args ...interface{}) {} +func (n noop) Debugf(format string, args ...interface{}) {} +func (n noop) WithValues(map[string]interface{}) Logger { return n } +func (n noop) WithCtxValues(context.Context) Logger { return n } +func (n noop) SetValuesOnCtx(parent context.Context, values Kv) context.Context { return parent } + +type contextKey string + +// contextLogValuesKey used as unique key to store log values in the context. +const contextLogValuesKey = contextKey("internal-log") + +// CtxWithValues returns a copy of parent in which the key values passed have been +// stored ready to be used using log.Logger. +func CtxWithValues(parent context.Context, kv Kv) context.Context { + // Maybe we have values already set. + oldValues, ok := parent.Value(contextLogValuesKey).(Kv) + if !ok { + oldValues = Kv{} + } + + // Copy old and received values into the new kv. + newValues := Kv{} + for k, v := range oldValues { + newValues[k] = v + } + for k, v := range kv { + newValues[k] = v + } + + return context.WithValue(parent, contextLogValuesKey, newValues) +} + +// ValuesFromCtx gets the log Key values from a context. +func ValuesFromCtx(ctx context.Context) Kv { + values, ok := ctx.Value(contextLogValuesKey).(Kv) + if !ok { + return Kv{} + } + + return values +} diff --git a/internal/log/logrus/logrus.go b/internal/log/logrus/logrus.go new file mode 100644 index 0000000000000000000000000000000000000000..efa25bdf0a2acc1ea462e50adf243dd49812462f --- /dev/null +++ b/internal/log/logrus/logrus.go @@ -0,0 +1,31 @@ +package logrus + +import ( + "context" + + "github.com/sirupsen/logrus" + + "github.com/slok/sloth/internal/log" +) + +type logger struct { + *logrus.Entry +} + +// NewLogrus returns a new log.Logger for a logrus implementation. +func NewLogrus(l *logrus.Entry) log.Logger { + return logger{Entry: l} +} + +func (l logger) WithValues(kv log.Kv) log.Logger { + newLogger := l.Entry.WithFields(kv) + return NewLogrus(newLogger) +} + +func (l logger) WithCtxValues(ctx context.Context) log.Logger { + return l.WithValues(log.ValuesFromCtx(ctx)) +} + +func (l logger) SetValuesOnCtx(parent context.Context, values log.Kv) context.Context { + return log.CtxWithValues(parent, values) +} diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh new file mode 100755 index 0000000000000000000000000000000000000000..df5554613e7fc8268803cc4c0741b2663fc288e8 --- /dev/null +++ b/scripts/build/build-all.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + +# Build all. +ostypes=("Linux" "Darwin" "Windows" "ARM") +for ostype in "${ostypes[@]}" +do + ostype="${ostype}" ./scripts/build/build.sh +done + +# Create checksums. +checksums_dir="./bin" +cd ${checksums_dir} && sha256sum * > ./checksums.txt diff --git a/scripts/build/build-image.sh b/scripts/build/build-image.sh new file mode 100755 index 0000000000000000000000000000000000000000..90e9d3565418651dd44b8ce925ff935198a3efcd --- /dev/null +++ b/scripts/build/build-image.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh + +set -e + +if [ -z ${VERSION} ]; then + echo "IMAGE_VERSION env var needs to be set" + exit 1 +fi + +if [ -z ${IMAGE} ]; then + echo "IMAGE env var needs to be set" + exit 1 +fi + +if [ -z ${DOCKER_FILE_PATH} ]; then + echo "DOCKER_FILE_PATH env var needs to be set" + exit 1 +fi + +echo "Building image ${IMAGE}:${VERSION}..." +docker build \ + --build-arg VERSION=${VERSION} \ + -t ${IMAGE}:${VERSION} \ + -f ${DOCKER_FILE_PATH} . + +if [ ! -z ${TAG_IMAGE_LATEST} ]; then + echo "Tagged image ${IMAGE}:${VERSION} with ${IMAGE}:latest" + docker tag ${IMAGE}:${VERSION} ${IMAGE}:latest +fi \ No newline at end of file diff --git a/scripts/build/build.sh b/scripts/build/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..47f7f77963b9df6489d2ee559d1fb9a28e1844db --- /dev/null +++ b/scripts/build/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + +src=./cmd/sloth +out=./bin/sloth + +ostype=${ostype:-"native"} + +function build() { + ext="${1:-}" + goos="${2:-}" + goarch="${3:-}" + goarm="${4:-}" + + [[ ! -z "${goos}" ]] && export GOOS="${goos}" + [[ ! -z "${goarch}" ]] && export GOARCH="${goarch}" + [[ ! -z "${goarm}" ]] && export GOARM="${goarm}" + + final_out=${out}${ext} + ldf_cmp="-s -w -extldflags '-static'" + f_ver="-X main.Version=${VERSION:-dev}" + + echo "Building binary at ${final_out} (GOOS=${GOOS:-}, GOARCH=${GOARCH:-}, GOARM=${GOARM:-}, VERSION=${VERSION:-})" + CGO_ENABLED=0 go build -o ${final_out} --ldflags "${ldf_cmp} ${f_ver}" ${src} +} + + +if [ $ostype == 'Linux' ]; then + build "-linux-amd64" "linux" "amd64" +elif [ $ostype == 'Darwin' ]; then + build "-darwin-amd64" "darwin" "amd64" + build "-darwin-arm64" "darwin" "arm64" +elif [ $ostype == 'Windows' ]; then + build "-windows-amd64.exe" "windows" "amd64" +elif [ $ostype == 'ARM' ]; then + build "-linux-arm64" "linux" "arm64" + build "-linux-arm-v7" "linux" "arm" "7" +else + # Native. + build +fi diff --git a/scripts/build/publish-image.sh b/scripts/build/publish-image.sh new file mode 100755 index 0000000000000000000000000000000000000000..76fbbdc8dc327bd993abad9a3d619cdd8316cbcb --- /dev/null +++ b/scripts/build/publish-image.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +set -e + +if [ -z ${VERSION} ]; then + echo "IMAGE_VERSION env var needs to be set" + exit 1 +fi + +if [ -z ${IMAGE} ]; then + echo "IMAGE env var needs to be set" + exit 1 +fi + +echo "Pushing image ${IMAGE}:${VERSION}..." +docker push ${IMAGE}:${VERSION} + +if [ ! -z ${TAG_IMAGE_LATEST} ]; then + echo "Pushing image ${IMAGE}:latest..." + docker push ${IMAGE}:latest +fi \ No newline at end of file diff --git a/scripts/check/check.sh b/scripts/check/check.sh new file mode 100755 index 0000000000000000000000000000000000000000..532753e82c780c7f02ed5ac651a2fc0cb6cf75ed --- /dev/null +++ b/scripts/check/check.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +golangci-lint run -E goimports --timeout 3m \ No newline at end of file diff --git a/scripts/check/integration-test.sh b/scripts/check/integration-test.sh new file mode 100755 index 0000000000000000000000000000000000000000..f0d9c941fe7dba480fb58ca103840b0485529beb --- /dev/null +++ b/scripts/check/integration-test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go test -race -tags='integration' -v ./tests/integration/... \ No newline at end of file diff --git a/scripts/check/unit-test.sh b/scripts/check/unit-test.sh new file mode 100755 index 0000000000000000000000000000000000000000..b3b3a088d9dadb8218a7505e3c6b695c9d69a678 --- /dev/null +++ b/scripts/check/unit-test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go test -race -coverprofile=.test_coverage.txt ./... +go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $3}' \ No newline at end of file diff --git a/scripts/deps.sh b/scripts/deps.sh new file mode 100755 index 0000000000000000000000000000000000000000..f11203913f6115e84617adfb80e131faba965ef3 --- /dev/null +++ b/scripts/deps.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go mod tidy \ No newline at end of file diff --git a/scripts/gogen.sh b/scripts/gogen.sh new file mode 100755 index 0000000000000000000000000000000000000000..803ac8a6723c49f0f27bc6b58b70ba60be9d4d80 --- /dev/null +++ b/scripts/gogen.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go generate ./... \ No newline at end of file