├── lgtm.yml ├── cmd ├── openapi │ ├── types.cfg.yaml │ ├── server.cfg.yaml │ ├── example-types.gen.go │ ├── schema.yaml │ └── example-server.gen.go └── main.go ├── Dockerfile-release ├── internal ├── handlers │ ├── readiness.go │ ├── health_check.go │ ├── readiness_test.go │ └── health_check_test.go ├── mocks │ └── counterfeit.go └── database │ ├── models.go │ ├── sql_repository.go │ └── databasefakes │ └── fake_repository.go ├── SECURITY.md ├── .gitignore ├── metrics └── basic.go ├── api ├── models.go ├── public │ ├── greet.go │ ├── greet_test.go │ ├── server.go │ ├── employees.go │ └── employees_test.go └── admin │ └── internal.go ├── Dockerfile ├── .travis.yml ├── deployment ├── base │ ├── service.yaml │ ├── monitoring.yaml │ ├── kustomization.yaml │ ├── initContainer.yaml │ ├── ingress.yaml │ ├── alerting.yaml │ └── deployment.yaml └── environments │ ├── poz-dev │ └── kustomization.yaml │ ├── res │ └── kustomization.yaml │ ├── sjc │ └── kustomization.yaml │ └── sjc-dev │ └── kustomization.yaml ├── .github ├── workflows │ ├── lint.yaml │ ├── trivy-analysis.yml │ └── codeql-analysis.yml └── dependabot.yml ├── docker-compose.yaml ├── .air.toml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── .golangci.yml ├── Makefile ├── go.mod ├── ci └── get_gotestsum.sh └── go.sum /lgtm.yml: -------------------------------------------------------------------------------- 1 | extraction: 2 | go: 3 | index: 4 | build_command: 5 | - make build 6 | -------------------------------------------------------------------------------- /cmd/openapi/types.cfg.yaml: -------------------------------------------------------------------------------- 1 | output: 2 | ./cmd/openapi/example-types.gen.go 3 | generate: 4 | - types 5 | package: openapi -------------------------------------------------------------------------------- /cmd/openapi/server.cfg.yaml: -------------------------------------------------------------------------------- 1 | output: 2 | ./cmd/openapi/example-server.gen.go 3 | generate: 4 | - server 5 | - spec 6 | package: openapi -------------------------------------------------------------------------------- /Dockerfile-release: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY go-service-example /app/ 3 | WORKDIR /app 4 | 5 | USER 65534:65534 6 | 7 | EXPOSE 3000:3000 8 | EXPOSE 4000:4000 9 | EXPOSE 5000:5000 10 | 11 | ENTRYPOINT ["/app/go-service-example"] -------------------------------------------------------------------------------- /internal/handlers/readiness.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func Readiness(ctx echo.Context) error { 10 | return ctx.NoContent(http.StatusOK) 11 | } 12 | -------------------------------------------------------------------------------- /internal/handlers/health_check.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func HealthCheck(ctx echo.Context) error { 10 | return ctx.String(http.StatusOK, "OK") 11 | } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | < 0.1 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Currently not supported. Please create PR if needed. 12 | -------------------------------------------------------------------------------- /internal/mocks/counterfeit.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package mocks 4 | 5 | import ( 6 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 7 | ) 8 | 9 | // This file imports packages that are used when running go generate, or used 10 | // during the development process but not otherwise depended on by built code. 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | tmp/ 15 | bin/ 16 | dist/ 17 | 18 | # idea ide 19 | .idea 20 | -------------------------------------------------------------------------------- /metrics/basic.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var GreetCount = prometheus.NewCounter( 6 | prometheus.CounterOpts{ 7 | Name: "greets_total", 8 | Help: "Number of generated greetings", 9 | }) 10 | 11 | func RegisterMetrics(registerer prometheus.Registerer) { 12 | registerer.MustRegister(GreetCount) 13 | } 14 | -------------------------------------------------------------------------------- /api/models.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type CreateEmployeeRequest struct { 4 | Name string `json:"name" validate:"required,gt=3,lt=50,alphanumunicode|printascii"` 5 | City string `json:"city" validate:"required,gt=4,lt=30,alphanumunicode|printascii"` 6 | } 7 | 8 | type EmployeeResponse struct { 9 | ID int `json:"id"` 10 | Name string `json:"name"` 11 | City string `json:"city"` 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | RUN apk add git build-base 3 | RUN mkdir /build 4 | ADD . /build/ 5 | WORKDIR /build 6 | RUN make build-alpine 7 | 8 | FROM scratch 9 | COPY --from=builder /build/bin/go-service-example /app/ 10 | WORKDIR /app 11 | 12 | USER 65534:65534 13 | 14 | EXPOSE 3000:3000 15 | EXPOSE 4000:4000 16 | EXPOSE 5000:5000 17 | 18 | ENTRYPOINT ["/app/go-service-example"] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.17" # golangci-lint does not play well with go 1.18, see https://github.com/golangci/golangci-lint/issues/2649 4 | install: 5 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2 6 | - ./ci/get_gotestsum.sh -b $(go env GOPATH)/bin v1.6.4 7 | script: 8 | - make lint-ci 9 | - make test -------------------------------------------------------------------------------- /deployment/base/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: go-service-example 5 | spec: 6 | ports: 7 | - port: 4000 8 | name: admin 9 | protocol: TCP 10 | targetPort: 4000 11 | - port: 80 12 | name: main 13 | protocol: TCP 14 | targetPort: 3000 15 | - port: 5000 16 | name: debug 17 | protocol: TCP 18 | targetPort: 5000 19 | type: ClusterIP -------------------------------------------------------------------------------- /deployment/base/monitoring.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: go-service-example 5 | labels: 6 | app: go-service-example 7 | spec: 8 | jobLabel: app 9 | selector: 10 | matchLabels: 11 | app: go-service-example 12 | namespaceSelector: 13 | matchNames: 14 | - dev 15 | endpoints: 16 | - port: admin 17 | interval: 30s 18 | targetLabels: 19 | - team -------------------------------------------------------------------------------- /api/public/greet.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/Wikia/go-commons/logging" 7 | "github.com/Wikia/go-service-example/metrics" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | type Message struct { 12 | Text string 13 | } 14 | 15 | func (s APIServer) Greet(ctx echo.Context) error { 16 | logger := logging.FromEchoContext(ctx) 17 | logger.Info("Greeting user") 18 | 19 | defer metrics.GreetCount.Inc() 20 | 21 | m := Message{"Hello World!"} 22 | 23 | return ctx.JSON(http.StatusOK, m) 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Go Code 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-go@v2 17 | with: 18 | stable: 'false' 19 | go-version: '1.16.5' 20 | 21 | - name: Lint 22 | run: | 23 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.41.1 24 | 25 | make lint-ci -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for gomod 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | # Enable version updates for Docker 10 | - package-ecosystem: "docker" 11 | # Look for a `Dockerfile` in the `root` directory 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | 16 | # Enable version updates for github-actions 17 | # - package-ecosystem: "github-actions" 18 | # Workflow files stored in the 19 | # default location of `.github/workflows` 20 | # directory: "/" 21 | # schedule: 22 | # interval: "weekly" 23 | -------------------------------------------------------------------------------- /internal/handlers/readiness_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/Wikia/go-service-example/internal/handlers" 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func TestReadinessHandler(t *testing.T) { 16 | t.Parallel() 17 | e := echo.New() 18 | req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader("")) 19 | rec := httptest.NewRecorder() 20 | 21 | if c := e.NewContext(req, rec); assert.NoError(t, handlers.Readiness(c)) { 22 | assert.Equal(t, http.StatusOK, rec.Code) 23 | assert.Equal(t, "", rec.Body.String()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/handlers/health_check_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/Wikia/go-service-example/internal/handlers" 10 | 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/labstack/echo/v4" 14 | ) 15 | 16 | func TestHealthCheckHandler(t *testing.T) { 17 | t.Parallel() 18 | e := echo.New() 19 | req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader("")) 20 | rec := httptest.NewRecorder() 21 | 22 | if c := e.NewContext(req, rec); assert.NoError(t, handlers.HealthCheck(c)) { 23 | assert.Equal(t, http.StatusOK, rec.Code) 24 | assert.Equal(t, "OK", rec.Body.String()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | runner: 4 | environment: 5 | - EXAMPLE_ENVIRONMENT=localhost 6 | - EXAMPLE_LOGGING_TYPE=localhost 7 | - EXAMPLE_DB_SOURCES=root@tcp(mysql:3306)/example?charset=utf8mb4&parseTime=True&loc=Local 8 | image: cosmtrek/air 9 | ports: 10 | - "3000:3000" 11 | - "4000:4000" 12 | - "5000:5000" 13 | working_dir: /example 14 | volumes: 15 | - .:/example 16 | - golang_cache:/go/pkg/ 17 | depends_on: 18 | - mysql 19 | mysql: 20 | image: mysql 21 | environment: 22 | - MYSQL_DATABASE=example 23 | - MYSQL_ALLOW_EMPTY_PASSWORD=1 24 | ports: 25 | - "3306:3306" 26 | volumes: 27 | golang_cache: -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | tmp_dir = "tmp" 3 | 4 | [build] 5 | bin = "./tmp/server" 6 | cmd = "go build -race -o ./tmp/server cmd/main.go" 7 | delay = 1000 8 | exclude_dir = ["assets", "tmp", "vendor", "dist"] 9 | exclude_file = [] 10 | exclude_regex = [] 11 | exclude_unchanged = false 12 | follow_symlink = false 13 | full_bin = "./tmp/server" 14 | include_dir = [] 15 | include_ext = ["go", "tpl", "tmpl", "html"] 16 | kill_delay = "0s" 17 | log = "build-errors.log" 18 | send_interrupt = false 19 | stop_on_error = true 20 | 21 | [color] 22 | app = "" 23 | build = "yellow" 24 | main = "magenta" 25 | runner = "green" 26 | watcher = "cyan" 27 | 28 | [log] 29 | time = false 30 | 31 | [misc] 32 | clean_on_exit = true 33 | -------------------------------------------------------------------------------- /deployment/environments/poz-dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: dev 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | resources: 7 | - ../../base 8 | replicas: 9 | - name: go-service-example 10 | count: 1 11 | configMapGenerator: 12 | - name: go-service-example 13 | behavior: merge 14 | literals: 15 | - vault_address=active.vault.service.poz.consul:8200 16 | - example_datacenter=poz-dev 17 | - example_environment=dev 18 | patches: 19 | - target: 20 | kind: PrometheusRule 21 | patch: |- 22 | - op: replace 23 | path: /metadata/labels/prometheus 24 | value: dev 25 | - target: 26 | kind: ServiceMonitor 27 | patch: |- 28 | - op: replace 29 | path: /spec/namespaceSelector/matchNames 30 | value: dev -------------------------------------------------------------------------------- /deployment/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | - ingress.yaml 7 | - monitoring.yaml 8 | - alerting.yaml 9 | images: 10 | - name: artifactory.wikia-inc.com/services/go-service-example 11 | newTag: CHANGEME 12 | patches: 13 | - path: initContainer.yaml 14 | target: 15 | kind: Deployment 16 | name: go-service-example 17 | commonLabels: 18 | team: YOUR_TEAM_NAME_HERE 19 | app: go-service-example 20 | generatorOptions: 21 | disableNameSuffixHash: true 22 | configMapGenerator: 23 | - name: go-service-example 24 | literals: 25 | - jaeger_reporter_log_spans=false 26 | - jaeger_agent_host=localhost 27 | - jaeger_agent_port=6831 28 | - jaeger_sampler_type=probabilistic 29 | - jaeger_sampler_param=1.0 30 | - jaeger_service_name=go-jobrunner -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - goos: 6 | - darwin 7 | - linux 8 | - windows 9 | goarch: 10 | - amd64 11 | main: ./cmd/main.go 12 | archives: 13 | - format_overrides: 14 | - goos: windows 15 | format: zip 16 | release: 17 | github: 18 | prerelease: auto 19 | dockers: 20 | - goos: linux 21 | goarch: amd64 22 | dockerfile: Dockerfile-release 23 | image_templates: 24 | - "artifactory.wikia-inc.com/services/{{ .ProjectName }}:{{ .Major }}.{{ .Minor }}.{{ .Patch }}" 25 | build_flag_templates: 26 | - "--pull" 27 | - "--label=org.opencontainers.image.created={{.Date}}" 28 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 29 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 30 | - "--label=org.opencontainers.image.version={{.Version}}" 31 | - "--label=org.opencontainers.image.source={{.GitURL}}" 32 | -------------------------------------------------------------------------------- /internal/database/models.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 4 | 5 | import ( 6 | "context" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type EmployeeDBModel struct { 12 | ID int `gorm:"column:id;primaryKey"` 13 | Name string `gorm:"column:name"` 14 | City string `gorm:"column:city"` 15 | } 16 | 17 | //counterfeiter:generate . Repository 18 | type Repository interface { 19 | GetAllEmployees(ctx context.Context) ([]EmployeeDBModel, error) 20 | AddEmployee(ctx context.Context, newEmployee *EmployeeDBModel) error 21 | GetEmployee(ctx context.Context, employeeID int64) (*EmployeeDBModel, error) 22 | DeleteEmployee(ctx context.Context, employeeID int64) error 23 | } 24 | 25 | func InitData(db *gorm.DB) (err error) { 26 | err = db.AutoMigrate(&EmployeeDBModel{}) 27 | if err != nil { 28 | return 29 | } 30 | 31 | db.Create(&EmployeeDBModel{Name: "Przemek", City: "Olsztyn"}) 32 | db.Create(&EmployeeDBModel{Name: "Łukasz", City: "Poznań"}) 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /api/public/greet_test.go: -------------------------------------------------------------------------------- 1 | package public_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/Wikia/go-commons/logging" 9 | "github.com/Wikia/go-commons/validator" 10 | "github.com/Wikia/go-service-example/api/public" 11 | "github.com/Wikia/go-service-example/internal/database/databasefakes" 12 | "github.com/labstack/echo/v4" 13 | "github.com/stretchr/testify/assert" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | func TestGreet(t *testing.T) { 18 | t.Parallel() 19 | mockRepo := &databasefakes.FakeRepository{} 20 | server := public.NewAPIServer(mockRepo) 21 | 22 | e := echo.New() 23 | e.Validator = &validator.EchoValidator{} 24 | 25 | req := httptest.NewRequest(http.MethodPut, "/example/greet", nil) 26 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 27 | rec := httptest.NewRecorder() 28 | c := e.NewContext(req, rec) 29 | logging.AddToContext(c, zap.L()) 30 | 31 | if assert.NoError(t, server.Greet(c)) { 32 | assert.Equal(t, http.StatusOK, rec.Code) 33 | assert.JSONEq(t, `{"Text": "Hello World!"}`, rec.Body.String()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /deployment/base/initContainer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: go-service-example 5 | spec: 6 | template: 7 | spec: 8 | serviceAccountName: pandora-k8s-pod-dev 9 | initContainers: 10 | - image: artifactory.wikia-inc.com/ops/init-vault:0.13 11 | args: #select your secrets here. You can also try the --debug flag for verbose logging 12 | - SECRET=secret/app/dev/go-service-example/rabbit_credentials.password 13 | name: secrets 14 | volumeMounts: 15 | - name: secrets-dir # secrets are stored here 16 | mountPath: /var/lib/secrets 17 | env: 18 | - name: VAULT_ADDR # This works fine by default in prod but must be overridden in dev 19 | valueFrom: 20 | configMapKeyRef: 21 | key: vault_address 22 | name: go-service-example 23 | - name: ENV 24 | valueFrom: 25 | configMapKeyRef: 26 | key: jobrunner_environment 27 | name: go-service-example -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 FANDOM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/openapi/example-types.gen.go: -------------------------------------------------------------------------------- 1 | // Package openapi provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/deepmap/oapi-codegen version v1.8.1 DO NOT EDIT. 4 | package openapi 5 | 6 | // Employee defines model for Employee. 7 | type Employee struct { 8 | // Embedded struct due to allOf(#/components/schemas/NewEmployee) 9 | NewEmployee `yaml:",inline"` 10 | // Embedded fields due to inline allOf schema 11 | Id int64 `json:"id"` 12 | } 13 | 14 | // EmployeeList defines model for EmployeeList. 15 | type EmployeeList []Employee 16 | 17 | // Error defines model for Error. 18 | type Error struct { 19 | Code int32 `json:"code"` 20 | Message string `json:"message"` 21 | } 22 | 23 | // NewEmployee defines model for NewEmployee. 24 | type NewEmployee struct { 25 | City string `json:"city"` 26 | Name string `json:"name"` 27 | } 28 | 29 | // CreateEmployeeJSONBody defines parameters for CreateEmployee. 30 | type CreateEmployeeJSONBody NewEmployee 31 | 32 | // CreateEmployeeJSONRequestBody defines body for CreateEmployee for application/json ContentType. 33 | type CreateEmployeeJSONRequestBody CreateEmployeeJSONBody 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-service-example 2 | 3 | [![Build Status](https://travis-ci.com/Wikia/go-service-example.svg?branch=main)](https://travis-ci.com/Wikia/go-service-example) 4 | [![CodeQL Status](https://github.com/Wikia/go-service-example/workflows/CodeQL/badge.svg)](https://github.com/Wikia/go-service-example/actions) 5 | [![Linter Status](https://github.com/Wikia/go-service-example/workflows/Lint%20Go%20Code/badge.svg)](https://github.com/Wikia/go-service-example/actions) 6 | [![Trivy Status](https://github.com/Wikia/go-service-example/workflows/trivy/badge.svg)](https://github.com/Wikia/go-service-example/actions) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/Wikia/go-service-example)](https://goreportcard.com/report/github.com/Wikia/go-service-example) 8 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/Wikia/go-service-example.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Wikia/go-service-example/alerts/) 9 | [![Language grade: Go](https://img.shields.io/lgtm/grade/go/g/Wikia/go-service-example.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Wikia/go-service-example/context:go) 10 | 11 | This is a template for simple golang HTTP service with most common bits and pieces 12 | -------------------------------------------------------------------------------- /api/admin/internal.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | 8 | "github.com/Wikia/go-commons/logging" 9 | "github.com/pkg/errors" 10 | 11 | internalHandlers "github.com/Wikia/go-service-example/internal/handlers" 12 | "github.com/getkin/kin-openapi/openapi3" 13 | "github.com/labstack/echo/v4/middleware" 14 | "github.com/labstack/gommon/log" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // NewInternalAPI constructs an echo server with all application routes defined. 20 | func NewInternalAPI(logger *zap.Logger, swagger *openapi3.T) *echo.Echo { 21 | r := echo.New() 22 | 23 | r.Use( 24 | middleware.RemoveTrailingSlash(), 25 | logging.LoggerInContext(logger), 26 | logging.EchoLogger(logger), 27 | middleware.RecoverWithConfig(middleware.RecoverConfig{LogLevel: log.ERROR}), 28 | ) 29 | 30 | health := r.Group("/health") 31 | { 32 | health.GET("/alive", internalHandlers.HealthCheck) 33 | health.GET("/ready", internalHandlers.Readiness) 34 | } 35 | 36 | r.GET("/metrics", func(ctx echo.Context) error { 37 | promhttp.Handler().ServeHTTP(ctx.Response(), ctx.Request()) 38 | 39 | return nil 40 | }) 41 | 42 | r.GET("/swagger", func(ctx echo.Context) error { 43 | data, err := swagger.MarshalJSON() 44 | if err != nil { 45 | return errors.Wrap(err, "error marshaling swagger spec") 46 | } 47 | 48 | return ctx.JSONBlob(http.StatusOK, data) 49 | }) 50 | 51 | return r 52 | } 53 | -------------------------------------------------------------------------------- /deployment/environments/res/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: prod 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | resources: 7 | - ../../base 8 | replicas: 9 | - name: go-service-example 10 | count: 1 11 | configMapGenerator: 12 | - name: go-service-example 13 | behavior: merge 14 | literals: 15 | - vault_address=active.vault.service.res.consul:8200 16 | - example_datacenter=res 17 | - example_environment=prod 18 | patches: 19 | - target: 20 | kind: PrometheusRule 21 | patch: |- 22 | - op: replace 23 | path: /metadata/labels/prometheus 24 | value: prod 25 | - target: 26 | kind: ServiceMonitor 27 | patch: |- 28 | - op: replace 29 | path: /spec/namespaceSelector/matchNames 30 | value: prod 31 | - target: 32 | kind: Ingress 33 | patch: |- 34 | - op: replace 35 | path: /spec/rules/0/host 36 | value: go-service-example.prod.res.k8s.wikia.net 37 | - op: replace 38 | path: /spec/rules/1/host 39 | value: prod.res.k8s.wikia.net 40 | - op: replace 41 | path: /spec/rules/2/host 42 | value: go-service-example-admin.res.k8s.wikia.net 43 | - op: replace 44 | path: /spec/rules/3/host 45 | value: go-service-example-debug.res.k8s.wikia.net 46 | - op: replace 47 | path: /spec/rules/4/host 48 | value: services.wikia.com 49 | - op: replace 50 | path: /spec/rules/5/host 51 | value: services.fandom.com -------------------------------------------------------------------------------- /deployment/environments/sjc/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: prod 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | resources: 7 | - ../../base 8 | replicas: 9 | - name: go-service-example 10 | count: 1 11 | configMapGenerator: 12 | - name: go-service-example 13 | behavior: merge 14 | literals: 15 | - vault_address=active.vault.service.sjc.consul:8200 16 | - example_datacenter=sjc 17 | - example_environment=prod 18 | patches: 19 | - target: 20 | kind: PrometheusRule 21 | patch: |- 22 | - op: replace 23 | path: /metadata/labels/prometheus 24 | value: prod 25 | - target: 26 | kind: ServiceMonitor 27 | patch: |- 28 | - op: replace 29 | path: /spec/namespaceSelector/matchNames 30 | value: prod 31 | - target: 32 | kind: Ingress 33 | patch: |- 34 | - op: replace 35 | path: /spec/rules/0/host 36 | value: go-service-example.prod.sjc.k8s.wikia.net 37 | - op: replace 38 | path: /spec/rules/1/host 39 | value: prod.sjc.k8s.wikia.net 40 | - op: replace 41 | path: /spec/rules/2/host 42 | value: go-service-example-admin.sjc.k8s.wikia.net 43 | - op: replace 44 | path: /spec/rules/3/host 45 | value: go-service-example-debug.sjc.k8s.wikia.net 46 | - op: replace 47 | path: /spec/rules/4/host 48 | value: services.wikia.com 49 | - op: replace 50 | path: /spec/rules/5/host 51 | value: services.fandom.com -------------------------------------------------------------------------------- /deployment/environments/sjc-dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: dev 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | resources: 7 | - ../../base 8 | replicas: 9 | - name: go-service-example 10 | count: 1 11 | configMapGenerator: 12 | - name: go-service-example 13 | behavior: merge 14 | literals: 15 | - vault_address=active.vault.service.sjc.consul:8200 16 | - example_datacenter=sjc-dev 17 | - example_environment=dev 18 | patches: 19 | - target: 20 | kind: PrometheusRule 21 | patch: |- 22 | - op: replace 23 | path: /metadata/labels/prometheus 24 | value: dev 25 | - target: 26 | kind: ServiceMonitor 27 | patch: |- 28 | - op: replace 29 | path: /spec/namespaceSelector/matchNames 30 | value: dev 31 | - target: 32 | kind: Ingress 33 | patch: |- 34 | - op: replace 35 | path: /spec/rules/0/host 36 | value: go-service-example.dev.sjc.k8s.wikia.net 37 | - op: replace 38 | path: /spec/rules/1/host 39 | value: dev.sjc.k8s.wikia.net 40 | - op: replace 41 | path: /spec/rules/2/host 42 | value: go-service-example-admin.sjc-dev.k8s.wikia.net 43 | - op: replace 44 | path: /spec/rules/3/host 45 | value: go-service-example-debug.sjc-dev.k8s.wikia.net 46 | - op: replace 47 | path: /spec/rules/4/host 48 | value: services.wikia-dev.us 49 | - op: replace 50 | path: /spec/rules/5/host 51 | value: services.fandom-dev.us -------------------------------------------------------------------------------- /internal/database/sql_repository.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type SQLRepository struct { 11 | db *gorm.DB 12 | } 13 | 14 | func NewSQLRepository(db *gorm.DB) Repository { 15 | return &SQLRepository{db: db} 16 | } 17 | 18 | func (r SQLRepository) GetAllEmployees(ctx context.Context) (people []EmployeeDBModel, err error) { 19 | span, spanCtx := opentracing.StartSpanFromContext(ctx, "models.AllEmployees") 20 | defer span.Finish() 21 | 22 | err = r.db.WithContext(spanCtx).Find(&people).Error 23 | 24 | return 25 | } 26 | 27 | func (r SQLRepository) AddEmployee(ctx context.Context, newEmployee *EmployeeDBModel) (err error) { 28 | span, spanCtx := opentracing.StartSpanFromContext(ctx, "models.AddEmployee") 29 | defer span.Finish() 30 | 31 | err = r.db.WithContext(spanCtx).Create(newEmployee).Error 32 | 33 | return 34 | } 35 | 36 | func (r SQLRepository) GetEmployee(ctx context.Context, employeeID int64) (*EmployeeDBModel, error) { 37 | span, spanCtx := opentracing.StartSpanFromContext(ctx, "models.GetEmployee") 38 | defer span.Finish() 39 | 40 | employee := EmployeeDBModel{} 41 | err := r.db.WithContext(spanCtx).First(&employee, employeeID).Error 42 | 43 | return &employee, err 44 | } 45 | 46 | func (r SQLRepository) DeleteEmployee(ctx context.Context, employeeID int64) (err error) { 47 | span, spanCtx := opentracing.StartSpanFromContext(ctx, "models.DeleteEmployee") 48 | defer span.Finish() 49 | 50 | err = r.db.WithContext(spanCtx).Delete(&EmployeeDBModel{}, employeeID).Error 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /deployment/base/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: go-service-example 5 | spec: 6 | rules: 7 | - host: go-service-example.dev.poz-dev.k8s.wikia.net 8 | http: 9 | paths: 10 | - path: / 11 | backend: 12 | service: 13 | name: go-service-example 14 | port: 15 | name: main 16 | - host: dev.poz-dev.k8s.wikia.net 17 | http: 18 | paths: 19 | - path: /go-service-example 20 | backend: 21 | service: 22 | name: go-service-example 23 | port: 24 | name: main 25 | - host: go-service-example-admin.poz-dev.k8s.wikia.net 26 | http: 27 | paths: 28 | - path: / 29 | backend: 30 | service: 31 | name: go-service-example 32 | port: 33 | name: admin 34 | - host: go-service-example-debug.poz-dev.k8s.wikia.net 35 | http: 36 | paths: 37 | - path: / 38 | backend: 39 | service: 40 | name: go-service-example 41 | port: 42 | name: debug 43 | # for exposing service to the world 44 | - host: services-k8s.wikia-dev.pl 45 | http: 46 | paths: 47 | - path: /go-service-example 48 | backend: 49 | service: 50 | name: go-service-example 51 | port: 52 | name: main 53 | - host: services.fandom-dev.pl 54 | http: 55 | paths: 56 | - path: /go-service-example 57 | backend: 58 | service: 59 | name: go-service-example 60 | port: 61 | name: main -------------------------------------------------------------------------------- /api/public/server.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "github.com/Wikia/go-commons/logging" 5 | "github.com/Wikia/go-commons/validator" 6 | "github.com/Wikia/go-service-example/cmd/openapi" 7 | "github.com/Wikia/go-service-example/internal/database" 8 | openapimiddleware "github.com/deepmap/oapi-codegen/pkg/middleware" 9 | "github.com/getkin/kin-openapi/openapi3" 10 | "github.com/labstack/echo-contrib/jaegertracing" 11 | "github.com/labstack/echo-contrib/prometheus" 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | "github.com/labstack/gommon/log" 15 | "github.com/opentracing/opentracing-go" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | type APIServer struct { 20 | employeeRepo database.Repository 21 | } 22 | 23 | func NewAPIServer(repository database.Repository) *APIServer { 24 | return &APIServer{repository} 25 | } 26 | 27 | // NewPublicAPI constructs a public echo server with all application routes defined. 28 | func NewPublicAPI(logger *zap.Logger, tracer opentracing.Tracer, appName string, repository database.Repository, swagger *openapi3.T) *echo.Echo { 29 | wrapper := NewAPIServer(repository) 30 | r := echo.New() 31 | traceConfig := jaegertracing.DefaultTraceConfig 32 | traceConfig.ComponentName = appName 33 | traceConfig.Tracer = tracer 34 | 35 | traceMiddleware := jaegertracing.TraceWithConfig(traceConfig) 36 | promMetrics := prometheus.NewPrometheus("http", func(c echo.Context) bool { return false }) 37 | 38 | r.Use( 39 | middleware.RemoveTrailingSlash(), 40 | traceMiddleware, 41 | logging.LoggerInContext(logger), 42 | logging.EchoLogger(logger), 43 | ) 44 | 45 | if swagger != nil { 46 | r.Use(openapimiddleware.OapiRequestValidator(swagger)) 47 | } 48 | r.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{LogLevel: log.ERROR})) 49 | 50 | promMetrics.Use(r) 51 | // request/form validation 52 | r.Validator = &validator.EchoValidator{} 53 | 54 | openapi.RegisterHandlers(r, wrapper) 55 | 56 | return r 57 | } 58 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # default concurrency is a available CPU number 4 | concurrency: 4 5 | 6 | # timeout for analysis, e.g. 30s, 5m, default is 1m 7 | timeout: 5m 8 | 9 | # include test files or not, default is true 10 | tests: true 11 | 12 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 13 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 14 | # automatic updating of go.mod described above. Instead, it fails when any changes 15 | # to go.mod are needed. This setting is most useful to check that go.mod does 16 | # not need updates, such as in a continuous integration and testing system. 17 | # If invoked with -mod=vendor, the go command assumes that the vendor 18 | # directory holds the correct copies of dependencies and ignores 19 | # the dependency descriptions in go.mod. 20 | modules-download-mode: readonly 21 | 22 | # Allow multiple parallel golangci-lint instances running. 23 | # If false (default) - golangci-lint acquires file lock on start. 24 | allow-parallel-runners: true 25 | 26 | 27 | # output configuration options 28 | output: 29 | # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions 30 | # default is "colored-line-number" 31 | format: colored-line-number 32 | 33 | # print lines of code with issue, default is true 34 | print-issued-lines: true 35 | 36 | # print linter name in the end of issue text, default is true 37 | print-linter-name: true 38 | 39 | # make issues output unique by line, default is true 40 | uniq-by-line: true 41 | 42 | # add a prefix to the output file references; default is no prefix 43 | path-prefix: "" 44 | 45 | # sorts results by: filepath, line and column 46 | sort-results: true 47 | 48 | whitespace: 49 | multi-if: true # Enforces newlines (or comments) after every multi-line if statement 50 | multi-func: true # Enforces newlines (or comments) after every multi-line function signature 51 | 52 | linters: 53 | enable: 54 | - revive 55 | - goimports 56 | - stylecheck 57 | - errorlint 58 | - exhaustive 59 | - gocognit 60 | - goconst 61 | - gocritic 62 | - gofmt 63 | - gomnd 64 | - gosec 65 | - ifshort 66 | - noctx 67 | - nlreturn 68 | - paralleltest 69 | - prealloc 70 | - promlinter 71 | - testpackage 72 | - tparallel 73 | - wastedassign 74 | - whitespace 75 | -------------------------------------------------------------------------------- /deployment/base/alerting.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: PrometheusRule 3 | metadata: 4 | labels: 5 | prometheus: dev 6 | role: alert-rules 7 | name: go-service-example 8 | spec: 9 | groups: 10 | - name: go-service-example.rules 11 | rules: 12 | - alert: go-service-example-down 13 | expr: 100 * (count(up{service="go-service-example"} == 0) BY (service) / count(up{service="go-service-example"}) BY (service)) > 10 14 | for: 2m 15 | labels: 16 | severity: warning 17 | team: YOUR_TEAM_NAME 18 | annotations: 19 | description: '{{ $value | humanize }}% of {{ $labels.service }} targets are down.' 20 | - alert: go-service-example-5xx-ratio 21 | expr: 100 * sum(rate(http_handler_statuses_total{service="go-service-example",status_bucket="5xx"} [1m])) by (pod, service) / sum(rate(http_handler_statuses_total{service="go-service-example"} [1m])) by (pod, service) > 10 22 | for: 2m 23 | labels: 24 | severity: warning 25 | team: YOUR_TEAM_NAME 26 | annotations: 27 | description: '{{ $labels.service }}: {{ $value | humanize }}% of requests to pod {{ $labels.pod }} fail with 5xx response' 28 | summary: '{{ $labels.service }} returns a lot of 5xx responses' 29 | - alert: go-service-example-response-time 30 | expr: avg(rate(http_handler_duration_seconds_sum{service="go-service-example"} [1m]) / rate(http_handler_duration_seconds_count{service="go-service-example"} [1m])) by (handler_name, service, pod) > 200/1000 31 | for: 2m 32 | labels: 33 | severity: warning 34 | team: YOUR_TEAM_NAME 35 | annotations: 36 | description: response time ({{ $value | humanizeDuration }}) for pod {{ $labels.pod }} exceeds defined threshold (200ms)' 37 | summary: 'Response time for {{ $labels.service }} to high ({{ $value | humanizeDuration }})' 38 | - alert: go-service-example-restarts 39 | expr: floor(sum(increase(kube_pod_container_status_restarts_total{container="go-service-example"} [10m])) by (container, pod)) >= 2 40 | for: 1m 41 | labels: 42 | severity: warning 43 | team: YOUR_TEAM_NAME 44 | annotations: 45 | description: 'There were {{ $value }} restarts of {{ $labels.container }} in the last 10m, which is above configured 2 (pod: {{ $labels.pod }})' 46 | summary: '{{ $labels.container }} restarts too often' -------------------------------------------------------------------------------- /.github/workflows/trivy-analysis.yml: -------------------------------------------------------------------------------- 1 | name: trivy 2 | on: 3 | push: 4 | # Currently limited to master because of the following: 5 | # Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access. 6 | # To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. 7 | # See https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events. 8 | branches: [ main ] 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [ main ] 12 | jobs: 13 | trivy: 14 | name: Trivy 15 | runs-on: ubuntu-18.04 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v2.3.4 19 | 20 | # Setup docker buildx caching and building 21 | # based on: https://github.com/dtinth/github-actions-docker-layer-caching-poc/pull/1/files 22 | # also see: 23 | # * https://github.com/actions/cache/issues/31 24 | # * https://dev.to/dtinth/caching-docker-builds-in-github-actions-which-approach-is-the-fastest-a-research-18ei 25 | # * https://evilmartians.com/chronicles/build-images-on-github-actions-with-docker-layer-caching 26 | # * https://docs.docker.com/buildx/working-with-buildx/ 27 | - uses: docker/setup-buildx-action@v1 28 | - uses: actions/cache@v2 29 | with: 30 | path: /tmp/.buildx-cache 31 | key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }} 32 | restore-keys: | 33 | ${{ runner.os }}-buildx- 34 | - name: docker build (target app) from cache 35 | uses: docker/build-push-action@v2 36 | with: 37 | push: false 38 | tags: fandom.com/services/go-service-example:${{ github.sha }} 39 | # target: app 40 | cache-from: type=local,src=/tmp/.buildx-cache 41 | cache-to: type=local,dest=/tmp/.buildx-cache 42 | load: true # make the image available for local docker run commands 43 | 44 | - name: Run Trivy vulnerability scanner 45 | uses: aquasecurity/trivy-action@master 46 | with: 47 | image-ref: 'fandom.com/services/go-service-example:${{ github.sha }}' 48 | format: 'template' 49 | template: '@/contrib/sarif.tpl' 50 | output: 'trivy-results.sarif' 51 | severity: 'CRITICAL,HIGH' 52 | ignore-unfixed: true 53 | 54 | - name: Upload Trivy scan results to GitHub Security tab 55 | uses: github/codeql-action/upload-sarif@v1 56 | with: 57 | sarif_file: 'trivy-results.sarif' -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | # Currently limited to master because of the following: 11 | # Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access. 12 | # To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. 13 | # See https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events. 14 | branches: [main] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [main] 18 | schedule: 19 | - cron: '0 2 * * 1' 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | # Override automatic language detection by changing the below list 30 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 31 | language: ['go'] 32 | # Learn more... 33 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v2.3.4 38 | with: 39 | # We must fetch at least the immediate parents so that if this is 40 | # a pull request then we can checkout the head. 41 | fetch-depth: 2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make build 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v1 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build build-alpine clean test help default lint run-local bump-version 2 | 3 | BIN_NAME = go-service-example 4 | GITHUB_REPO = github.com/wikia/go-service-example 5 | BIN_DIR := $(GOPATH)/bin 6 | GOLANGCI_LINT := /usr/local/bin/golangci-lint 7 | GORELEASER := /usr/local/bin/goreleaser 8 | CURRENT_DIR := $(shell pwd) 9 | 10 | VERSION := $(shell git describe --tags --exact-match 2>/dev/null || echo "unknown") 11 | GIT_COMMIT = $(shell git rev-parse --short HEAD) 12 | GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) 13 | BUILD_DATE = $(shell date '+%Y-%m-%d-%H:%M:%S') 14 | IMAGE_NAME := "artifactory.wikia-inc.com/services/${BIN_NAME}" 15 | 16 | default: test 17 | 18 | $(GOLANGCI_LINT): 19 | brew install golangci-lint 20 | 21 | $(GORELEASER): 22 | brew install goreleaser/tap/goreleaser 23 | 24 | help: 25 | @echo 'Management commands for ${BIN_NAME}:' 26 | @echo 27 | @echo 'Usage:' 28 | @echo ' make build Compile the project.' 29 | @echo ' make build-docker Build all releases and docker images but will not publish them.' 30 | @echo ' make release Build all releases and docker image and pushes them out' 31 | @echo ' make get-deps Runs `go mod install`, mostly used for ci.' 32 | @echo ' make build-alpine Compile optimized for alpine linux.' 33 | @echo ' make test Run tests on a compiled project.' 34 | @echo ' make clean Clean the directory tree.' 35 | @echo ' make lint Run the linter on the source code' 36 | @echo ' make go-generate Generate golang code' 37 | @echo ' make openapi-generate Will generate server/client code using OpenAPI schema' 38 | @echo ' make run-local Run the server locally with live-reload (using local air binary of docker if not found' 39 | @echo 40 | 41 | lint: $(GOLANGCI_LINT) lint-cmd 42 | 43 | lint-ci: lint-cmd 44 | 45 | lint-cmd: 46 | @golangci-lint run 47 | @go vet ./... 48 | 49 | build: 50 | @echo "building ${BIN_NAME}@${VERSION}" 51 | @go build -ldflags "-X main.commit=${GIT_COMMIT}${GIT_DIRTY} -X main.date=${BUILD_DATE} -X main.version=${VERSION}" -o bin/${BIN_NAME} cmd/main.go 52 | 53 | get-deps: 54 | @go mod install 55 | 56 | build-alpine: 57 | @echo "building ${BIN_NAME}@${VERSION}" 58 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w -linkmode external -extldflags "-static" -X main.commit=${GIT_COMMIT}${GIT_DIRTY} -X main.date=${BUILD_DATE} -X main.version=${VERSION}' -o bin/${BIN_NAME} cmd/main.go 59 | 60 | build-docker: $(GORELEASER) 61 | @goreleaser --snapshot --skip-publish --rm-dist 62 | 63 | release: $(GORELEASER) 64 | @goreleaser --rm-dist 65 | 66 | clean: 67 | @test ! -e bin/${BIN_NAME} || rm bin/${BIN_NAME} 68 | @docker compose down 69 | 70 | test: 71 | @gotestsum --format pkgname-and-test-fails --jsonfile /tmp/test.log -- -race -cover -count=1 -coverprofile=/tmp/coverage.out ./... 72 | 73 | openapi-generate: 74 | @oapi-codegen -config ./cmd/openapi/server.cfg.yaml ./cmd/openapi/schema.yaml 75 | @oapi-codegen -config ./cmd/openapi/types.cfg.yaml ./cmd/openapi/schema.yaml 76 | 77 | run-local: 78 | @echo "Running server using docker air image" 79 | @docker compose up --remove-orphans 80 | 81 | go-generate: 82 | @go generate ./... -------------------------------------------------------------------------------- /api/public/employees.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/Wikia/go-commons/logging" 9 | "github.com/Wikia/go-service-example/api" 10 | "github.com/Wikia/go-service-example/internal/database" 11 | 12 | "github.com/labstack/echo/v4" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func employeeDBModelFromCreateRequest(e api.CreateEmployeeRequest) *database.EmployeeDBModel { 17 | return &database.EmployeeDBModel{ 18 | Name: e.Name, 19 | City: e.City, 20 | } 21 | } 22 | 23 | func employeeResponseFromDBModel(e database.EmployeeDBModel) *api.EmployeeResponse { 24 | return &api.EmployeeResponse{ 25 | ID: e.ID, 26 | Name: e.Name, 27 | City: e.City, 28 | } 29 | } 30 | 31 | func (s APIServer) GetAllEmployees(ctx echo.Context) error { 32 | logger := logging.FromEchoContext(ctx) 33 | logger.Info("Fetching list of all employees") 34 | 35 | people, err := s.employeeRepo.GetAllEmployees(ctx.Request().Context()) 36 | if err != nil { 37 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 38 | } 39 | 40 | response := make([]*api.EmployeeResponse, len(people)) 41 | for pos, e := range people { 42 | response[pos] = employeeResponseFromDBModel(e) 43 | } 44 | 45 | return ctx.JSON(http.StatusOK, response) 46 | } 47 | 48 | func (s APIServer) CreateEmployee(ctx echo.Context) error { 49 | logger := logging.FromEchoContext(ctx).Sugar() 50 | e := api.CreateEmployeeRequest{} 51 | 52 | if err := ctx.Bind(&e); err != nil { 53 | return err 54 | } 55 | 56 | if err := ctx.Validate(&e); err != nil { 57 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 58 | } 59 | 60 | // CWE-117, https://lgtm.com/rules/1514630437007/ 61 | escapedName := strings.ReplaceAll(e.Name, "\n", "") 62 | escapedName = strings.ReplaceAll(escapedName, "\r", "") 63 | logger.With("employee", escapedName).Info("creating new employee") 64 | 65 | dbEmployee := employeeDBModelFromCreateRequest(e) 66 | 67 | if err := s.employeeRepo.AddEmployee(ctx.Request().Context(), dbEmployee); err != nil { 68 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 69 | } 70 | 71 | return ctx.NoContent(http.StatusCreated) 72 | } 73 | 74 | func (s APIServer) FindEmployeeByID(ctx echo.Context, employeeID int64) error { 75 | logger := logging.FromEchoContext(ctx).Sugar() 76 | logger.With("id", employeeID).Info("looking up employee") 77 | e, err := s.employeeRepo.GetEmployee(ctx.Request().Context(), employeeID) 78 | 79 | if errors.Is(err, gorm.ErrRecordNotFound) { 80 | return echo.NewHTTPError(http.StatusNotFound, "object with given id not found") 81 | } else if err != nil { 82 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 83 | } 84 | 85 | return ctx.JSON(http.StatusOK, employeeResponseFromDBModel(*e)) 86 | } 87 | 88 | func (s APIServer) DeleteEmployee(ctx echo.Context, employeeID int64) error { 89 | logger := logging.FromEchoContext(ctx).Sugar() 90 | logger.With("id", employeeID).Info("deleting employee") 91 | err := s.employeeRepo.DeleteEmployee(ctx.Request().Context(), employeeID) 92 | 93 | if errors.Is(err, gorm.ErrRecordNotFound) { 94 | return echo.NewHTTPError(http.StatusNotFound, "object with given id not found") 95 | } else if err != nil { 96 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 97 | } 98 | 99 | return ctx.NoContent(http.StatusAccepted) 100 | } 101 | -------------------------------------------------------------------------------- /deployment/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | traefik.frontend.rule.type: PathPrefixStrip 6 | name: go-service-example 7 | spec: 8 | replicas: 0 9 | template: 10 | spec: 11 | containers: 12 | - env: 13 | - name: HOST 14 | valueFrom: 15 | fieldRef: 16 | fieldPath: spec.nodeName 17 | - name: DATACENTER 18 | valueFrom: 19 | configMapKeyRef: 20 | key: datacenter 21 | name: go-service-example 22 | - name: JAEGER_AGENT_HOST 23 | valueFrom: 24 | configMapKeyRef: 25 | key: jaeger_agent_host 26 | name: go-service-example 27 | - name: JAEGER_SERVICE_NAME 28 | valueFrom: 29 | configMapKeyRef: 30 | key: jaeger_service_name 31 | name: go-service-example 32 | - name: JAEGER_REPORTER_LOG_SPANS 33 | valueFrom: 34 | configMapKeyRef: 35 | key: jaeger_reporter_log_spans 36 | name: go-service-example 37 | - name: JAEGER_AGENT_PORT 38 | valueFrom: 39 | configMapKeyRef: 40 | key: jaeger_agent_port 41 | name: go-service-example 42 | - name: JAEGER_SAMPLER_PARAM 43 | valueFrom: 44 | configMapKeyRef: 45 | key: jaeger_sampler_param 46 | name: go-service-example 47 | - name: JAEGER_SAMPLER_TYPE 48 | valueFrom: 49 | configMapKeyRef: 50 | key: jaeger_sampler_type 51 | name: go-service-example 52 | image: artifactory.wikia-inc.com/services/go-service-example:v0.0.0 53 | name: go-service-example 54 | livenessProbe: 55 | httpGet: 56 | path: /health/alive 57 | port: 4000 58 | initialDelaySeconds: 2 59 | periodSeconds: 10 60 | timeoutSeconds: 3 61 | readinessProbe: 62 | httpGet: 63 | path: /health/ready 64 | port: 4000 65 | initialDelaySeconds: 10 66 | periodSeconds: 10 67 | timeoutSeconds: 3 68 | resources: 69 | limits: 70 | memory: 250Mi 71 | requests: 72 | cpu: 100m 73 | memory: 50Mi 74 | volumeMounts: # MOUNT THE SECRETS INTO YOUR CONTAINER 75 | - name: secrets-dir 76 | readOnly: true 77 | mountPath: /secrets 78 | - command: 79 | - /go/bin/agent-linux 80 | - --reporter.grpc.host-port=jaeger-collector:14250 81 | image: jaegertracing/jaeger-agent:1.17.1 82 | name: jaeger-agent 83 | ports: 84 | - containerPort: 6831 85 | protocol: UDP 86 | resources: 87 | limits: 88 | memory: 100Mi 89 | requests: 90 | cpu: 100m 91 | memory: 100Mi 92 | securityContext: 93 | runAsNonRoot: true 94 | runAsUser: 65534 95 | volumes: # all volumes must be defined here 96 | - name: secrets-dir # secrets will be stored here 97 | emptyDir: 98 | medium: Memory -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Wikia/go-service-example 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/Wikia/go-commons v0.2.1 7 | github.com/ardanlabs/conf v1.5.0 8 | github.com/deepmap/oapi-codegen v1.11.0 9 | github.com/getkin/kin-openapi v0.94.0 10 | github.com/labstack/echo-contrib v0.12.0 11 | github.com/labstack/echo/v4 v4.7.2 12 | github.com/labstack/gommon v0.3.1 13 | github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0 14 | github.com/opentracing/opentracing-go v1.2.0 15 | github.com/pkg/errors v0.9.1 16 | github.com/prometheus/client_golang v1.12.2 17 | github.com/stretchr/testify v1.7.1 18 | go.uber.org/zap v1.21.0 19 | gorm.io/gorm v1.23.5 20 | gorm.io/plugin/opentracing v0.0.0-20210506132430-24a9caea7709 21 | ) 22 | 23 | require ( 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/ghodss/yaml v1.0.0 // indirect 28 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 29 | github.com/go-openapi/swag v0.21.1 // indirect 30 | github.com/go-playground/locales v0.14.0 // indirect 31 | github.com/go-playground/universal-translator v0.18.0 // indirect 32 | github.com/go-playground/validator/v10 v10.11.0 // indirect 33 | github.com/go-sql-driver/mysql v1.6.0 // indirect 34 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 35 | github.com/golang/protobuf v1.5.2 // indirect 36 | github.com/google/uuid v1.3.0 // indirect 37 | github.com/gorilla/mux v1.8.0 // indirect 38 | github.com/jinzhu/inflection v1.0.0 // indirect 39 | github.com/jinzhu/now v1.1.4 // indirect 40 | github.com/josharian/intern v1.0.0 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/leodido/go-urn v1.2.1 // indirect 43 | github.com/mailru/easyjson v0.7.7 // indirect 44 | github.com/mattn/go-colorable v0.1.12 // indirect 45 | github.com/mattn/go-isatty v0.0.14 // indirect 46 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/prometheus/client_model v0.2.0 // indirect 51 | github.com/prometheus/common v0.32.1 // indirect 52 | github.com/prometheus/procfs v0.7.3 // indirect 53 | github.com/sirupsen/logrus v1.8.1 // indirect 54 | github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect 55 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 56 | github.com/valyala/bytebufferpool v1.0.0 // indirect 57 | github.com/valyala/fasttemplate v1.2.1 // indirect 58 | go.uber.org/atomic v1.9.0 // indirect 59 | go.uber.org/multierr v1.7.0 // indirect 60 | golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect 61 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 62 | golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect 63 | golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect 64 | golang.org/x/text v0.3.7 // indirect 65 | golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect 66 | golang.org/x/tools v0.1.10 // indirect 67 | golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect 68 | google.golang.org/protobuf v1.28.0 // indirect 69 | gopkg.in/yaml.v2 v2.4.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 71 | gorm.io/driver/mysql v1.3.2 // indirect 72 | gorm.io/plugin/dbresolver v1.1.0 // indirect 73 | moul.io/zapgorm2 v1.1.0 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /cmd/openapi/schema.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Example Service 5 | description: A sample API that illustrates simple CRUD operations on Employees 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Platform Team 9 | email: platform-l@fandom.com 10 | url: https://www.fandom.com 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: https://www.fandom.com 16 | paths: 17 | /example/hello: 18 | get: 19 | description: | 20 | Returns smiple greeting as an response. 21 | operationId: Greet 22 | responses: 23 | '200': 24 | description: greet response 25 | content: 26 | text/plain: 27 | schema: 28 | type: string 29 | /example/employee/all: 30 | get: 31 | description: | 32 | Returns all employees stored in the system. 33 | operationId: GetAllEmployees 34 | responses: 35 | '200': 36 | description: employee list response 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/EmployeeList' 41 | /example/employee: 42 | put: 43 | description: Creates new employee 44 | operationId: CreateEmployee 45 | requestBody: 46 | description: Employee to add 47 | required: true 48 | content: 49 | application/json: 50 | schema: 51 | $ref: '#/components/schemas/NewEmployee' 52 | responses: 53 | '202': 54 | description: employee response 55 | default: 56 | description: unexpected error 57 | content: 58 | application/json: 59 | schema: 60 | $ref: '#/components/schemas/Error' 61 | /example/employee/{id}: 62 | get: 63 | description: Returns an employee based on a single ID 64 | operationId: FindEmployeeByID 65 | parameters: 66 | - name: id 67 | in: path 68 | description: ID of employee to fetch 69 | required: true 70 | schema: 71 | type: integer 72 | format: int64 73 | responses: 74 | '200': 75 | description: employee response 76 | content: 77 | application/json: 78 | schema: 79 | $ref: '#/components/schemas/Employee' 80 | default: 81 | description: unexpected error 82 | content: 83 | application/json: 84 | schema: 85 | $ref: '#/components/schemas/Error' 86 | delete: 87 | description: deletes a single employee based on the ID supplied 88 | operationId: DeleteEmployee 89 | parameters: 90 | - name: id 91 | in: path 92 | description: ID of employee to delete 93 | required: true 94 | schema: 95 | type: integer 96 | format: int64 97 | responses: 98 | '204': 99 | description: employee deleted 100 | default: 101 | description: unexpected error 102 | content: 103 | application/json: 104 | schema: 105 | $ref: '#/components/schemas/Error' 106 | components: 107 | schemas: 108 | EmployeeList: 109 | type: array 110 | items: 111 | $ref: '#/components/schemas/Employee' 112 | 113 | Employee: 114 | allOf: 115 | - $ref: '#/components/schemas/NewEmployee' 116 | - type: object 117 | required: 118 | - id 119 | properties: 120 | id: 121 | type: integer 122 | format: int64 123 | 124 | NewEmployee: 125 | type: object 126 | required: 127 | - name 128 | - city 129 | properties: 130 | name: 131 | type: string 132 | city: 133 | type: string 134 | 135 | Error: 136 | type: object 137 | required: 138 | - code 139 | - message 140 | properties: 141 | code: 142 | type: integer 143 | format: int32 144 | message: 145 | type: string -------------------------------------------------------------------------------- /api/public/employees_test.go: -------------------------------------------------------------------------------- 1 | package public_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/Wikia/go-commons/logging" 11 | "github.com/Wikia/go-commons/validator" 12 | "github.com/Wikia/go-service-example/internal/database" 13 | "github.com/Wikia/go-service-example/internal/database/databasefakes" 14 | "github.com/pkg/errors" 15 | 16 | "gorm.io/gorm" 17 | 18 | "go.uber.org/zap" 19 | 20 | "github.com/labstack/echo/v4" 21 | 22 | "github.com/stretchr/testify/assert" 23 | 24 | "github.com/Wikia/go-service-example/api/public" 25 | ) 26 | 27 | var stubEmployees = []database.EmployeeDBModel{ 28 | { 29 | ID: 0, Name: "John Wick", City: "Atlanta", 30 | }, 31 | { 32 | ID: 1, Name: "Wade Winston Wilson", City: "New York", 33 | }, 34 | } 35 | 36 | func TestGetAllEmployees(t *testing.T) { 37 | t.Parallel() 38 | mockRepo := &databasefakes.FakeRepository{} 39 | server := public.NewAPIServer(mockRepo) 40 | 41 | mockRepo.GetAllEmployeesReturns(stubEmployees, nil) 42 | 43 | e := echo.New() 44 | req := httptest.NewRequest(http.MethodGet, "/example/employee/all", nil) 45 | rec := httptest.NewRecorder() 46 | c := e.NewContext(req, rec) 47 | logging.AddToContext(c, zap.L()) 48 | 49 | if assert.NoError(t, server.GetAllEmployees(c)) { 50 | assert.Equal(t, http.StatusOK, rec.Code) 51 | assert.Equal(t, 1, mockRepo.GetAllEmployeesCallCount()) 52 | assert.JSONEq(t, `[{"id":0,"name":"John Wick","city":"Atlanta"},{"id":1,"name":"Wade Winston Wilson","city":"New York"}]`, rec.Body.String()) 53 | } 54 | } 55 | 56 | func TestGetAllEmployeesFail(t *testing.T) { 57 | t.Parallel() 58 | mockRepo := &databasefakes.FakeRepository{} 59 | server := public.NewAPIServer(mockRepo) 60 | 61 | mockRepo.GetAllEmployeesReturns(nil, errors.New("some error")) 62 | 63 | e := echo.New() 64 | req := httptest.NewRequest(http.MethodGet, "/example/employee/all", nil) 65 | rec := httptest.NewRecorder() 66 | c := e.NewContext(req, rec) 67 | logging.AddToContext(c, zap.L()) 68 | 69 | err := server.GetAllEmployees(c) 70 | var httpError *echo.HTTPError 71 | if assert.ErrorAs(t, err, &httpError) { 72 | assert.Equal(t, http.StatusInternalServerError, httpError.Code) 73 | assert.Equal(t, 1, mockRepo.GetAllEmployeesCallCount()) 74 | } 75 | } 76 | 77 | func TestDeleteEmployee(t *testing.T) { 78 | t.Parallel() 79 | mockRepo := &databasefakes.FakeRepository{} 80 | server := public.NewAPIServer(mockRepo) 81 | 82 | e := echo.New() 83 | req := httptest.NewRequest(http.MethodDelete, "/example/employee/1", nil) 84 | rec := httptest.NewRecorder() 85 | c := e.NewContext(req, rec) 86 | logging.AddToContext(c, zap.L()) 87 | 88 | if assert.NoError(t, server.DeleteEmployee(c, 1)) { 89 | assert.Equal(t, http.StatusAccepted, rec.Code) 90 | assert.Equal(t, 1, mockRepo.DeleteEmployeeCallCount()) 91 | _, id := mockRepo.DeleteEmployeeArgsForCall(0) 92 | assert.EqualValues(t, 1, id) 93 | } 94 | } 95 | 96 | func TestDeleteEmployeeMissing(t *testing.T) { 97 | t.Parallel() 98 | mockRepo := &databasefakes.FakeRepository{} 99 | server := public.NewAPIServer(mockRepo) 100 | mockRepo.DeleteEmployeeReturns(gorm.ErrRecordNotFound) 101 | 102 | e := echo.New() 103 | req := httptest.NewRequest(http.MethodDelete, "/example/employee/5", nil) 104 | rec := httptest.NewRecorder() 105 | c := e.NewContext(req, rec) 106 | logging.AddToContext(c, zap.L()) 107 | 108 | err := server.DeleteEmployee(c, 5) 109 | var httpError *echo.HTTPError 110 | if assert.ErrorAs(t, err, &httpError) { 111 | assert.Equal(t, http.StatusNotFound, httpError.Code) 112 | assert.Equal(t, 1, mockRepo.DeleteEmployeeCallCount()) 113 | _, id := mockRepo.DeleteEmployeeArgsForCall(0) 114 | assert.EqualValues(t, 5, id) 115 | } 116 | } 117 | 118 | func TestFindEmployeeByID(t *testing.T) { 119 | t.Parallel() 120 | mockRepo := &databasefakes.FakeRepository{} 121 | server := public.NewAPIServer(mockRepo) 122 | 123 | mockRepo.GetEmployeeReturns(&stubEmployees[0], nil) 124 | 125 | e := echo.New() 126 | req := httptest.NewRequest(http.MethodGet, "/example/employee/1", nil) 127 | rec := httptest.NewRecorder() 128 | c := e.NewContext(req, rec) 129 | logging.AddToContext(c, zap.L()) 130 | 131 | if assert.NoError(t, server.FindEmployeeByID(c, 1)) { 132 | assert.Equal(t, http.StatusOK, rec.Code) 133 | assert.Equal(t, 1, mockRepo.GetEmployeeCallCount()) 134 | _, id := mockRepo.GetEmployeeArgsForCall(0) 135 | assert.EqualValues(t, 1, id) 136 | assert.JSONEq(t, `{"id":0,"name":"John Wick","city":"Atlanta"}`, rec.Body.String()) 137 | } 138 | } 139 | 140 | func TestFindEmployeeByIDMissing(t *testing.T) { 141 | t.Parallel() 142 | mockRepo := &databasefakes.FakeRepository{} 143 | server := public.NewAPIServer(mockRepo) 144 | 145 | mockRepo.GetEmployeeReturns(nil, gorm.ErrRecordNotFound) 146 | 147 | e := echo.New() 148 | req := httptest.NewRequest(http.MethodGet, "/example/employee/2", nil) 149 | rec := httptest.NewRecorder() 150 | c := e.NewContext(req, rec) 151 | logging.AddToContext(c, zap.L()) 152 | 153 | err := server.FindEmployeeByID(c, 2) 154 | var httpError *echo.HTTPError 155 | if assert.ErrorAs(t, err, &httpError) { 156 | assert.Equal(t, http.StatusNotFound, httpError.Code) 157 | assert.Equal(t, 1, mockRepo.GetEmployeeCallCount()) 158 | _, id := mockRepo.GetEmployeeArgsForCall(0) 159 | assert.EqualValues(t, 2, id) 160 | assert.Empty(t, rec.Body.String()) 161 | } 162 | } 163 | 164 | func TestCreateEmployee(t *testing.T) { 165 | t.Parallel() 166 | mockRepo := &databasefakes.FakeRepository{} 167 | server := public.NewAPIServer(mockRepo) 168 | 169 | e := echo.New() 170 | e.Validator = &validator.EchoValidator{} 171 | payload, err := json.Marshal(stubEmployees[0]) 172 | assert.NoError(t, err) 173 | 174 | req := httptest.NewRequest(http.MethodPut, "/example/employee", bytes.NewBuffer(payload)) 175 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 176 | rec := httptest.NewRecorder() 177 | c := e.NewContext(req, rec) 178 | logging.AddToContext(c, zap.L()) 179 | 180 | if assert.NoError(t, server.CreateEmployee(c)) { 181 | assert.Equal(t, http.StatusCreated, rec.Code) 182 | assert.Equal(t, 1, mockRepo.AddEmployeeCallCount()) 183 | _, ret := mockRepo.AddEmployeeArgsForCall(0) 184 | assert.EqualValues(t, &stubEmployees[0], ret) 185 | assert.Empty(t, rec.Body.String()) 186 | } 187 | } 188 | 189 | func TestCreateEmployeeInvalid(t *testing.T) { 190 | t.Parallel() 191 | mockRepo := &databasefakes.FakeRepository{} 192 | server := public.NewAPIServer(mockRepo) 193 | badEmployee := database.EmployeeDBModel{Name: "Joker"} 194 | 195 | e := echo.New() 196 | e.Validator = &validator.EchoValidator{} 197 | payload, err := json.Marshal(badEmployee) 198 | assert.NoError(t, err) 199 | 200 | req := httptest.NewRequest(http.MethodPut, "/example/employee", bytes.NewBuffer(payload)) 201 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 202 | rec := httptest.NewRecorder() 203 | c := e.NewContext(req, rec) 204 | logging.AddToContext(c, zap.L()) 205 | 206 | err = server.CreateEmployee(c) 207 | var httpError *echo.HTTPError 208 | if assert.ErrorAs(t, err, &httpError) { 209 | assert.Equal(t, http.StatusBadRequest, httpError.Code) 210 | assert.Regexp(t, `Error:Field validation`, httpError.Message) 211 | assert.Equal(t, 0, mockRepo.AddEmployeeCallCount()) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "expvar" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "time" 11 | 12 | dbcommons "github.com/Wikia/go-commons/database" 13 | "github.com/Wikia/go-commons/tracing" 14 | "github.com/Wikia/go-service-example/internal/database" 15 | "github.com/labstack/echo/v4" 16 | dblogger "gorm.io/gorm/logger" 17 | 18 | "github.com/Wikia/go-service-example/api/admin" 19 | "github.com/Wikia/go-service-example/api/public" 20 | "github.com/Wikia/go-service-example/cmd/openapi" 21 | 22 | "github.com/Wikia/go-service-example/metrics" 23 | "github.com/ardanlabs/conf" 24 | "github.com/pkg/errors" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "go.uber.org/zap" 27 | "go.uber.org/zap/zapcore" 28 | gormopentracing "gorm.io/plugin/opentracing" 29 | ) 30 | 31 | var ( 32 | version = "dev" 33 | commit = "none" 34 | date = "unknown" 35 | builtBy = "unknown" 36 | ) 37 | 38 | // AppName should hold unique name of your service. 39 | // Please be aware that this is also used as a prefix for environment variables used in config 40 | const AppName = "example" 41 | const ShutdownTimeout = 10 42 | 43 | func main() { 44 | if err := run(); err != nil { 45 | zap.L().With(zap.Error(err)).Error("error running service") 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func startServer(logger *zap.Logger, e *echo.Echo, host string) { 51 | if err := e.Start(host); err != nil && errors.Is(err, http.ErrServerClosed) { 52 | logger.With(zap.Error(err)).With(zap.String("host", host)).Fatal("error starting/running server") 53 | } 54 | } 55 | 56 | func run() error { 57 | var cfg struct { 58 | Environment string `conf:"default:prod,help:name of the environment app is running in (prod/dev/localhost)"` 59 | Datacenter string `conf:"help:name of the environment app is running on"` 60 | K8S struct { 61 | PodName string `conf:"help:name of the pod running the app"` 62 | } 63 | Web struct { 64 | APIHost string `conf:"default:0.0.0.0:3000"` 65 | InternalHost string `conf:"default:0.0.0.0:4000"` 66 | DebugHost string `conf:"default:0.0.0.0:5000"` 67 | ReadTimeout time.Duration `conf:"default:5s"` 68 | WriteTimeout time.Duration `conf:"default:5s"` 69 | ShutdownTimeout time.Duration `conf:"default:5s"` 70 | } 71 | Logging struct { 72 | Type string `conf:"default:prod,help:can be one of prod/dev/localhost"` 73 | Level string `conf:"default:info"` 74 | } 75 | DB struct { 76 | Driver string `conf:"default:sqlite3"` 77 | User string `conf:"default:root"` 78 | Password string `conf:"default:root"` 79 | Host string `conf:"default:localhost"` 80 | Sources []string 81 | Replicas []string 82 | ConnMaxIdleTime time.Duration `conf:"default:1h"` 83 | ConnMaxLifeTime time.Duration `conf:"default:12h"` 84 | MaxIdleConns int `conf:"default:10"` // tune this to your needs 85 | MaxOpenConns int `conf:"default:20"` // this as well 86 | } 87 | } 88 | 89 | if err := conf.Parse(os.Args[1:], AppName, &cfg); err != nil { 90 | if errors.Is(err, conf.ErrHelpWanted) { 91 | usage, err := conf.Usage(AppName, &cfg) 92 | if err != nil { 93 | return errors.Wrap(err, "generating config usage") 94 | } 95 | 96 | fmt.Println(usage) 97 | 98 | return nil 99 | } 100 | 101 | return errors.Wrap(err, "parsing config") 102 | } 103 | 104 | // ========================================================================= 105 | // Logging 106 | 107 | var ( 108 | logger *zap.Logger 109 | logCfg zap.Config 110 | err error 111 | ) 112 | 113 | const LocalhostEnv = "localhost" 114 | if cfg.Logging.Type == "dev" || cfg.Logging.Type == LocalhostEnv { 115 | logCfg = zap.NewDevelopmentConfig() 116 | } else { 117 | logCfg = zap.NewProductionConfig() 118 | } 119 | 120 | if cfg.Environment == LocalhostEnv { 121 | logCfg.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder 122 | } 123 | 124 | logLevel := zap.InfoLevel 125 | err = logLevel.Set(cfg.Logging.Level) 126 | 127 | if err == nil { 128 | logCfg.Level = zap.NewAtomicLevelAt(logLevel) 129 | logger, err = logCfg.Build() 130 | } 131 | 132 | if err != nil { 133 | panic(fmt.Sprintf("could not initialize log: %v", err)) 134 | } 135 | 136 | logger = logger.With( 137 | zap.String("appname", AppName), 138 | zap.String("version", version), 139 | zap.String("git_commit", commit), 140 | zap.String("build_date", date), 141 | zap.String("build_by", builtBy), 142 | zap.String("environment", cfg.Environment), 143 | zap.String("datacenter", cfg.Datacenter), 144 | zap.String("pod_name", cfg.K8S.PodName), 145 | ) 146 | 147 | logger.With(zap.Reflect("config", cfg)).Info("starting service") 148 | 149 | zap.ReplaceGlobals(logger) 150 | 151 | // ========================================================================= 152 | // DB 153 | db, err := dbcommons.GetConnection(logger, dblogger.Info, cfg.DB.Sources, cfg.DB.Replicas, cfg.DB.ConnMaxIdleTime, cfg.DB.ConnMaxLifeTime, cfg.DB.MaxIdleConns, cfg.DB.MaxOpenConns) 154 | 155 | if err != nil { 156 | logger.With(zap.Error(err)).Panic("could not connect to the database") 157 | } 158 | 159 | // Init for this example 160 | res := db.Raw("SHOW TABLES LIKE 'employees'") 161 | 162 | var result string 163 | err = res.Row().Scan(&result) 164 | 165 | if err != nil || len(result) == 0 { 166 | logger.Info("no tables found - initializing database") 167 | 168 | if err = database.InitData(db); err != nil { 169 | logger.With(zap.Error(err)).Warn("could not initialize database") 170 | } 171 | } 172 | 173 | // Print the build version for our logs. Also expose it under /debug/vars. 174 | expvar.NewString("build").Set(commit) 175 | expvar.NewString("version").Set(version) 176 | logger.Info("started: Application initializing") 177 | 178 | defer logger.Info("application terminated") 179 | 180 | // metrics 181 | registry := prometheus.DefaultRegisterer 182 | metrics.RegisterMetrics(prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_", AppName), registry)) 183 | 184 | // tracer 185 | tracer, closer, err := tracing.InitJaegerTracer(AppName, logger.Sugar(), registry) 186 | if err != nil { 187 | return errors.Wrap(err, "error initializing tracer") 188 | } 189 | 190 | defer func() { 191 | err := closer.Close() 192 | if err != nil { 193 | logger.With(zap.Error(err)).Error("could not close tracer") 194 | } 195 | }() 196 | 197 | err = db.Use(gormopentracing.New(gormopentracing.WithTracer(tracer))) 198 | 199 | if err != nil { 200 | logger.With(zap.Error(err)).Error("could not initialize tracing for the database") 201 | } 202 | 203 | // swagger 204 | swagger, err := openapi.GetSwagger() 205 | 206 | if err != nil { 207 | logger.With(zap.Error(err)).Fatal("could not load Swagger Spec") 208 | } 209 | 210 | swagger.Servers = nil 211 | 212 | internalAPI := admin.NewInternalAPI(logger, swagger) 213 | internalAPI.HideBanner = cfg.Environment != LocalhostEnv 214 | internalAPI.HidePort = cfg.Environment != LocalhostEnv 215 | go startServer(logger, internalAPI, cfg.Web.InternalHost) 216 | 217 | sqlRepo := database.NewSQLRepository(db) 218 | publicAPI := public.NewPublicAPI(logger, tracer, AppName, sqlRepo, swagger) 219 | publicAPI.HideBanner = true // no need to see it twice 220 | publicAPI.HidePort = cfg.Environment != LocalhostEnv 221 | go startServer(logger, publicAPI, cfg.Web.APIHost) 222 | 223 | // Wait for interrupt signal to gracefully shut down the server with a timeout of 10 seconds. 224 | // Use a buffered channel to avoid missing signals as recommended for signal.Notify 225 | quit := make(chan os.Signal, 1) 226 | signal.Notify(quit, os.Interrupt) 227 | <-quit 228 | ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout*time.Second) 229 | defer cancel() 230 | if err := publicAPI.Shutdown(ctx); err != nil { 231 | publicAPI.Logger.Fatal(err) 232 | } 233 | if err := internalAPI.Shutdown(ctx); err != nil { 234 | internalAPI.Logger.Fatal(err) 235 | } 236 | 237 | return nil 238 | } 239 | -------------------------------------------------------------------------------- /cmd/openapi/example-server.gen.go: -------------------------------------------------------------------------------- 1 | // Package openapi provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/deepmap/oapi-codegen version v1.8.1 DO NOT EDIT. 4 | package openapi 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | "path" 14 | "strings" 15 | 16 | "github.com/deepmap/oapi-codegen/pkg/runtime" 17 | "github.com/getkin/kin-openapi/openapi3" 18 | "github.com/labstack/echo/v4" 19 | ) 20 | 21 | // ServerInterface represents all server handlers. 22 | type ServerInterface interface { 23 | 24 | // (PUT /example/employee) 25 | CreateEmployee(ctx echo.Context) error 26 | 27 | // (GET /example/employee/all) 28 | GetAllEmployees(ctx echo.Context) error 29 | 30 | // (DELETE /example/employee/{id}) 31 | DeleteEmployee(ctx echo.Context, id int64) error 32 | 33 | // (GET /example/employee/{id}) 34 | FindEmployeeByID(ctx echo.Context, id int64) error 35 | 36 | // (GET /example/hello) 37 | Greet(ctx echo.Context) error 38 | } 39 | 40 | // ServerInterfaceWrapper converts echo contexts to parameters. 41 | type ServerInterfaceWrapper struct { 42 | Handler ServerInterface 43 | } 44 | 45 | // CreateEmployee converts echo context to params. 46 | func (w *ServerInterfaceWrapper) CreateEmployee(ctx echo.Context) error { 47 | var err error 48 | 49 | // Invoke the callback with all the unmarshalled arguments 50 | err = w.Handler.CreateEmployee(ctx) 51 | return err 52 | } 53 | 54 | // GetAllEmployees converts echo context to params. 55 | func (w *ServerInterfaceWrapper) GetAllEmployees(ctx echo.Context) error { 56 | var err error 57 | 58 | // Invoke the callback with all the unmarshalled arguments 59 | err = w.Handler.GetAllEmployees(ctx) 60 | return err 61 | } 62 | 63 | // DeleteEmployee converts echo context to params. 64 | func (w *ServerInterfaceWrapper) DeleteEmployee(ctx echo.Context) error { 65 | var err error 66 | // ------------- Path parameter "id" ------------- 67 | var id int64 68 | 69 | err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id) 70 | if err != nil { 71 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) 72 | } 73 | 74 | // Invoke the callback with all the unmarshalled arguments 75 | err = w.Handler.DeleteEmployee(ctx, id) 76 | return err 77 | } 78 | 79 | // FindEmployeeByID converts echo context to params. 80 | func (w *ServerInterfaceWrapper) FindEmployeeByID(ctx echo.Context) error { 81 | var err error 82 | // ------------- Path parameter "id" ------------- 83 | var id int64 84 | 85 | err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id) 86 | if err != nil { 87 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) 88 | } 89 | 90 | // Invoke the callback with all the unmarshalled arguments 91 | err = w.Handler.FindEmployeeByID(ctx, id) 92 | return err 93 | } 94 | 95 | // Greet converts echo context to params. 96 | func (w *ServerInterfaceWrapper) Greet(ctx echo.Context) error { 97 | var err error 98 | 99 | // Invoke the callback with all the unmarshalled arguments 100 | err = w.Handler.Greet(ctx) 101 | return err 102 | } 103 | 104 | // This is a simple interface which specifies echo.Route addition functions which 105 | // are present on both echo.Echo and echo.Group, since we want to allow using 106 | // either of them for path registration 107 | type EchoRouter interface { 108 | CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 109 | DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 110 | GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 111 | HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 112 | OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 113 | PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 114 | POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 115 | PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 116 | TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 117 | } 118 | 119 | // RegisterHandlers adds each server route to the EchoRouter. 120 | func RegisterHandlers(router EchoRouter, si ServerInterface) { 121 | RegisterHandlersWithBaseURL(router, si, "") 122 | } 123 | 124 | // Registers handlers, and prepends BaseURL to the paths, so that the paths 125 | // can be served under a prefix. 126 | func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { 127 | 128 | wrapper := ServerInterfaceWrapper{ 129 | Handler: si, 130 | } 131 | 132 | router.PUT(baseURL+"/example/employee", wrapper.CreateEmployee) 133 | router.GET(baseURL+"/example/employee/all", wrapper.GetAllEmployees) 134 | router.DELETE(baseURL+"/example/employee/:id", wrapper.DeleteEmployee) 135 | router.GET(baseURL+"/example/employee/:id", wrapper.FindEmployeeByID) 136 | router.GET(baseURL+"/example/hello", wrapper.Greet) 137 | 138 | } 139 | 140 | // Base64 encoded, gzipped, json marshaled Swagger object 141 | var swaggerSpec = []string{ 142 | 143 | "H4sIAAAAAAAC/8xWzY7bNhB+FWLaoyK5m6AHnbpZu4WBIAmS9pTugZFGEgOKZDmj9RqG3r0gZcnyyl23", 144 | "wKLtxbDF+ft+OPIBCts6a9AwQX4AKhpsZfy6aZ22e8TwXWr9oYL8ywG+91hBDt9lp7zsmJS9x92U1CcH", 145 | "cN469Kww1lNl+KysbyVDDsrwj28gAd47HH5ijR76PgGPf3TKYwn5l5B1PwXZr9+wYOjv+2Qa750ijuUZ", 146 | "29jnuQln4401pfdyH35vvLc+FDgfu7AlPh389c2FwRNokUjWMfp4SOyVqRegYs1T/BJgAnMulzMp3l/o", 147 | "koCR7d9oH6OSocqyd4hWprIDeMOyiPxiK5WGHJyWHLh4pX+qpCltmxa2hbE1fDwei19RhsedD0kNs6M8", 148 | "y3a7XTrL6hMokQqvHCtrIIdbQbJ1GsXtx63gRrJQWnfEXjKSIBXP7j79thaBDhmySFgjRqoIEtCqQEOR", 149 | "huNMt04WDYqbdHVxIBmPU+vr7JhL2bvt3eb9582rm3SVNtzqaBj0LX2oPqN/UAUei+RZRjtZ1+hTZbMY", 150 | "kgVzKNYhZPM44BmTEnhATwPYH9JVugqFrUMjnYIcXsdHCTjJTVQ6w6FAhnMzdFGRc+ruPEaSDO7EFBxr", 151 | "DzxtyyloczoOtkDit7bcj3qjidWlc1oVMTX7RqHFuB2uXbKzNdAvNB7PBFshyxLm3mTfYTQrORt0CK1u", 152 | "VjdLtCNCMYYOXqpkp/nFgAwb4QKEzuCjw4KxFDjG9MlSrExqHZrUeEGwT8idNySk1pNgJIitx1IoI7hB", 153 | "QXtibNPfzULJX5BvtZ77/glpq5ejYb5pL7AxaaEV8UyQv+DkoMp+YEMj45KX4TkJKUiZWuNEjvgqCctw", 154 | "3QM127WgLmDCckHOOpaY2dxJL1tk9BRfYucNt2thq1MXtuI4WtiDYeNJbk4LTi0dm8yIvP5+u19I9eYZ", 155 | "fw+jlP+5vZMrJjYXZJoU3K4XEv2sTDkK9HYfA/6hSBVy0fxrGr38dXr2Kv0v11qDWtur+4xaFd54tUdk", 156 | "ZWohoztGQBd3WQi9vsEYHzlzWqonWJ/+2VngiqOcL6Y+AUL/MHrt+X8p9/2fAQAA//94kLxdJwsAAA==", 157 | } 158 | 159 | // GetSwagger returns the content of the embedded swagger specification file 160 | // or error if failed to decode 161 | func decodeSpec() ([]byte, error) { 162 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 163 | if err != nil { 164 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 165 | } 166 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 167 | if err != nil { 168 | return nil, fmt.Errorf("error decompressing spec: %s", err) 169 | } 170 | var buf bytes.Buffer 171 | _, err = buf.ReadFrom(zr) 172 | if err != nil { 173 | return nil, fmt.Errorf("error decompressing spec: %s", err) 174 | } 175 | 176 | return buf.Bytes(), nil 177 | } 178 | 179 | var rawSpec = decodeSpecCached() 180 | 181 | // a naive cached of a decoded swagger spec 182 | func decodeSpecCached() func() ([]byte, error) { 183 | data, err := decodeSpec() 184 | return func() ([]byte, error) { 185 | return data, err 186 | } 187 | } 188 | 189 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 190 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 191 | var res = make(map[string]func() ([]byte, error)) 192 | if len(pathToFile) > 0 { 193 | res[pathToFile] = rawSpec 194 | } 195 | 196 | return res 197 | } 198 | 199 | // GetSwagger returns the Swagger specification corresponding to the generated code 200 | // in this file. The external references of Swagger specification are resolved. 201 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 202 | // Externally referenced files must be embedded in the corresponding golang packages. 203 | // Urls can be supported but this task was out of the scope. 204 | func GetSwagger() (swagger *openapi3.T, err error) { 205 | var resolvePath = PathToRawSpec("") 206 | 207 | loader := openapi3.NewLoader() 208 | loader.IsExternalRefsAllowed = true 209 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 210 | var pathToFile = url.String() 211 | pathToFile = path.Clean(pathToFile) 212 | getSpec, ok := resolvePath[pathToFile] 213 | if !ok { 214 | err1 := fmt.Errorf("path not found: %s", pathToFile) 215 | return nil, err1 216 | } 217 | return getSpec() 218 | } 219 | var specData []byte 220 | specData, err = rawSpec() 221 | if err != nil { 222 | return 223 | } 224 | swagger, err = loader.LoadFromData(specData) 225 | if err != nil { 226 | return 227 | } 228 | return 229 | } 230 | -------------------------------------------------------------------------------- /ci/get_gotestsum.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2021-07-10T22:41:18Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 118 | } 119 | echoerr() { 120 | echo "$@" 1>&2 121 | } 122 | log_prefix() { 123 | echo "$0" 124 | } 125 | _logp=6 126 | log_set_priority() { 127 | _logp="$1" 128 | } 129 | log_priority() { 130 | if test -z "$1"; then 131 | echo "$_logp" 132 | return 133 | fi 134 | [ "$1" -le "$_logp" ] 135 | } 136 | log_tag() { 137 | case $1 in 138 | 0) echo "emerg" ;; 139 | 1) echo "alert" ;; 140 | 2) echo "crit" ;; 141 | 3) echo "err" ;; 142 | 4) echo "warning" ;; 143 | 5) echo "notice" ;; 144 | 6) echo "info" ;; 145 | 7) echo "debug" ;; 146 | *) echo "$1" ;; 147 | esac 148 | } 149 | log_debug() { 150 | log_priority 7 || return 0 151 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 152 | } 153 | log_info() { 154 | log_priority 6 || return 0 155 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 156 | } 157 | log_err() { 158 | log_priority 3 || return 0 159 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 160 | } 161 | log_crit() { 162 | log_priority 2 || return 0 163 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 164 | } 165 | uname_os() { 166 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 167 | case "$os" in 168 | cygwin_nt*) os="windows" ;; 169 | mingw*) os="windows" ;; 170 | msys_nt*) os="windows" ;; 171 | esac 172 | echo "$os" 173 | } 174 | uname_arch() { 175 | arch=$(uname -m) 176 | case $arch in 177 | x86_64) arch="amd64" ;; 178 | x86) arch="386" ;; 179 | i686) arch="386" ;; 180 | i386) arch="386" ;; 181 | aarch64) arch="arm64" ;; 182 | armv5*) arch="armv5" ;; 183 | armv6*) arch="armv6" ;; 184 | armv7*) arch="armv7" ;; 185 | esac 186 | echo ${arch} 187 | } 188 | uname_os_check() { 189 | os=$(uname_os) 190 | case "$os" in 191 | darwin) return 0 ;; 192 | dragonfly) return 0 ;; 193 | freebsd) return 0 ;; 194 | linux) return 0 ;; 195 | android) return 0 ;; 196 | nacl) return 0 ;; 197 | netbsd) return 0 ;; 198 | openbsd) return 0 ;; 199 | plan9) return 0 ;; 200 | solaris) return 0 ;; 201 | windows) return 0 ;; 202 | esac 203 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 204 | return 1 205 | } 206 | uname_arch_check() { 207 | arch=$(uname_arch) 208 | case "$arch" in 209 | 386) return 0 ;; 210 | amd64) return 0 ;; 211 | arm64) return 0 ;; 212 | armv5) return 0 ;; 213 | armv6) return 0 ;; 214 | armv7) return 0 ;; 215 | ppc64) return 0 ;; 216 | ppc64le) return 0 ;; 217 | mips) return 0 ;; 218 | mipsle) return 0 ;; 219 | mips64) return 0 ;; 220 | mips64le) return 0 ;; 221 | s390x) return 0 ;; 222 | amd64p32) return 0 ;; 223 | esac 224 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 225 | return 1 226 | } 227 | untar() { 228 | tarball=$1 229 | case "${tarball}" in 230 | *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; 231 | *.tar) tar --no-same-owner -xf "${tarball}" ;; 232 | *.zip) unzip "${tarball}" ;; 233 | *) 234 | log_err "untar unknown archive format for ${tarball}" 235 | return 1 236 | ;; 237 | esac 238 | } 239 | http_download_curl() { 240 | local_file=$1 241 | source_url=$2 242 | header=$3 243 | if [ -z "$header" ]; then 244 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 245 | else 246 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 247 | fi 248 | if [ "$code" != "200" ]; then 249 | log_debug "http_download_curl received HTTP status $code" 250 | return 1 251 | fi 252 | return 0 253 | } 254 | http_download_wget() { 255 | local_file=$1 256 | source_url=$2 257 | header=$3 258 | if [ -z "$header" ]; then 259 | wget -q -O "$local_file" "$source_url" 260 | else 261 | wget -q --header "$header" -O "$local_file" "$source_url" 262 | fi 263 | } 264 | http_download() { 265 | log_debug "http_download $2" 266 | if is_command curl; then 267 | http_download_curl "$@" 268 | return 269 | elif is_command wget; then 270 | http_download_wget "$@" 271 | return 272 | fi 273 | log_crit "http_download unable to find wget or curl" 274 | return 1 275 | } 276 | http_copy() { 277 | tmp=$(mktemp) 278 | http_download "${tmp}" "$1" "$2" || return 1 279 | body=$(cat "$tmp") 280 | rm -f "${tmp}" 281 | echo "$body" 282 | } 283 | github_release() { 284 | owner_repo=$1 285 | version=$2 286 | test -z "$version" && version="latest" 287 | giturl="https://github.com/${owner_repo}/releases/${version}" 288 | json=$(http_copy "$giturl" "Accept:application/json") 289 | test -z "$json" && return 1 290 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 291 | test -z "$version" && return 1 292 | echo "$version" 293 | } 294 | hash_sha256() { 295 | TARGET=${1:-/dev/stdin} 296 | if is_command gsha256sum; then 297 | hash=$(gsha256sum "$TARGET") || return 1 298 | echo "$hash" | cut -d ' ' -f 1 299 | elif is_command sha256sum; then 300 | hash=$(sha256sum "$TARGET") || return 1 301 | echo "$hash" | cut -d ' ' -f 1 302 | elif is_command shasum; then 303 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 304 | echo "$hash" | cut -d ' ' -f 1 305 | elif is_command openssl; then 306 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 307 | echo "$hash" | cut -d ' ' -f a 308 | else 309 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 310 | return 1 311 | fi 312 | } 313 | hash_sha256_verify() { 314 | TARGET=$1 315 | checksums=$2 316 | if [ -z "$checksums" ]; then 317 | log_err "hash_sha256_verify checksum file not specified in arg2" 318 | return 1 319 | fi 320 | BASENAME=${TARGET##*/} 321 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 322 | if [ -z "$want" ]; then 323 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 324 | return 1 325 | fi 326 | got=$(hash_sha256 "$TARGET") 327 | if [ "$want" != "$got" ]; then 328 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 329 | return 1 330 | fi 331 | } 332 | cat /dev/null <