├── fsutil ├── test-data │ └── fscopy │ │ ├── top.txt │ │ ├── level1 │ │ ├── l1.txt │ │ └── level2 │ │ │ └── deep.txt │ │ └── levelA │ │ ├── a.txt │ │ └── levelB │ │ └── b.txt ├── fsrecursivecopy_test.go ├── fsrecursivecopy.go ├── filesystem_test.go └── filesystem.go ├── .gitattributes ├── Makefile ├── actor └── actor.go ├── logutil ├── swap_signal_windows.go ├── swap_signal.go └── logutil.go ├── ulid └── ulid.go ├── .gitignore ├── .circleci └── config.yml ├── stringutil └── stringutil.go ├── .github ├── actionlint-matcher.json └── workflows │ ├── go.yml │ └── lint.yml ├── tlsutil ├── tlsutil_test.go └── tlsutil.go ├── entrypoint └── entrypoint.go ├── httputil ├── middleware.go ├── middleware_example_test.go └── httputil.go ├── instrumentation └── instrumentation.go ├── LICENSE ├── go.mod ├── pgutil ├── pgutil_test.go └── pgutil.go ├── README.md ├── contexts └── uuid │ └── uuid.go ├── health ├── health.go └── health_test.go ├── testutil ├── testutil.go └── testutil_test.go ├── munemo ├── dialects.go ├── munemo_test.go └── munemo.go ├── workflow.md ├── env ├── env.go └── env_test.go ├── .golangci.yml ├── dbutil └── dbutil.go ├── styleguide.md ├── version ├── version_test.go └── version.go ├── debug └── debug.go └── go.sum /fsutil/test-data/fscopy/top.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /fsutil/test-data/fscopy/level1/l1.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /fsutil/test-data/fscopy/levelA/a.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /fsutil/test-data/fscopy/levelA/levelB/b.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /fsutil/test-data/fscopy/level1/level2/deep.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # set line endings for go files. Mostly needed for golang-ci 2 | *.go text eol=lf 3 | 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: deps test 2 | 3 | deps: 4 | go get -u github.com/golang/dep/cmd/dep 5 | dep ensure 6 | 7 | test: 8 | go test -cover -race -v $(shell go list ./... | grep -v /vendor/) 9 | -------------------------------------------------------------------------------- /actor/actor.go: -------------------------------------------------------------------------------- 1 | package actor 2 | 3 | // Actor is a struct that can be used to represent interruptible workloads 4 | type Actor struct { 5 | Execute func() error 6 | Interrupt func(error) 7 | } 8 | -------------------------------------------------------------------------------- /logutil/swap_signal_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package logutil 5 | 6 | import "github.com/go-kit/kit/log" 7 | 8 | func swapLevelHandler(base log.Logger, swapLogger *log.SwapLogger, debug bool) { 9 | // noop for now 10 | } 11 | -------------------------------------------------------------------------------- /ulid/ulid.go: -------------------------------------------------------------------------------- 1 | package ulid 2 | 3 | import ( 4 | "crypto/rand" 5 | 6 | "github.com/oklog/ulid" 7 | ) 8 | 9 | // New returns a Universally Unique Lexicographically Sortable Identifier via 10 | // github.com/oklog/ulid 11 | func New() string { 12 | return ulid.MustNew(ulid.Now(), rand.Reader).String() 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | coverage.out 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 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | .glide/ 16 | 17 | vendor 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build-go1.21: 4 | docker: 5 | - image: golang:1.21 6 | working_directory: /go/src/github.com/kolide/kit 7 | steps: &steps 8 | - checkout 9 | - run: GO111MODULE=on go mod download 10 | - run: GO111MODULE=on go test -race -cover -v $(go list ./... | grep -v /vendor/) 11 | 12 | workflows: 13 | version: 2 14 | build: 15 | jobs: 16 | - build-go1.21 17 | -------------------------------------------------------------------------------- /stringutil/stringutil.go: -------------------------------------------------------------------------------- 1 | package stringutil 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // RandomString returns a 'random' string. Don't rely on this to have 8 | // a secure level of entropy. 9 | func RandomString(n int) string { 10 | letterBytes := "abcdefghijklmnopqrstuvwxyz" 11 | b := make([]byte, n) 12 | for i := range b { 13 | /* #nosec */ 14 | b[i] = letterBytes[rand.Intn(len(letterBytes))] 15 | } 16 | return string(b) 17 | } 18 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tlsutil/tlsutil_test.go: -------------------------------------------------------------------------------- 1 | package tlsutil 2 | 3 | import ( 4 | "crypto/tls" 5 | "testing" 6 | ) 7 | 8 | func TestNewConfig(t *testing.T) { 9 | t.Parallel() 10 | 11 | // default, should have Modern compatibility. 12 | cfg := NewConfig() 13 | if have, want := cfg.MinVersion, uint16(tls.VersionTLS12); have != want { 14 | t.Errorf("have %d, want %d", have, want) 15 | } 16 | 17 | // test WithProfile 18 | cfg = NewConfig(WithProfile(Old)) 19 | if have, want := cfg.MinVersion, uint16(tls.VersionTLS10); have != want { 20 | t.Errorf("have %d, want %d", have, want) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /entrypoint/entrypoint.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package entrypoint replaces the shell version of 3 | 4 | exec $@ 5 | 6 | which is often use when creating docker entrypoint scripts to wrap a 7 | binary with some initial setup. 8 | */ 9 | package entrypoint 10 | 11 | import ( 12 | "flag" 13 | "log" 14 | "os" 15 | "os/exec" 16 | "syscall" 17 | ) 18 | 19 | func Exec() { 20 | flag.Parse() 21 | if len(os.Args) == 1 { 22 | return 23 | } 24 | cmd, err := exec.LookPath(os.Args[1]) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | if err := syscall.Exec(cmd, flag.Args(), os.Environ()); err != nil { 29 | log.Fatal(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /httputil/middleware.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import "net/http" 4 | 5 | // Middleware is a chainable decorator for HTTP Handlers. 6 | type Middleware func(http.Handler) http.Handler 7 | 8 | // Chain is a helper function for composing middlewares. Requests will 9 | // traverse them in the order they're declared. That is, the first middleware 10 | // is treated as the outermost middleware. 11 | // 12 | // Chain is identical to the go-kit helper for Endpoint Middleware. 13 | func Chain(outer Middleware, others ...Middleware) Middleware { 14 | return func(next http.Handler) http.Handler { 15 | for i := len(others) - 1; i >= 0; i-- { // reverse 16 | next = others[i](next) 17 | } 18 | return outer(next) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /instrumentation/instrumentation.go: -------------------------------------------------------------------------------- 1 | // package instrumentation providies utilities for instrumenting Go code. 2 | package instrumentation 3 | 4 | import ( 5 | "go.opencensus.io/stats/view" 6 | "go.opencensus.io/trace" 7 | ) 8 | 9 | // NewNopCensusExporter creates a NoOp exporter for the OpenCensus Trace package. 10 | // This exporter can be used for tests/local development and does not require the user to provide 11 | // authentication to a remote tracing API. 12 | func NewNopCensusExporter() *exporter { return &exporter{} } 13 | 14 | type exporter struct{} 15 | 16 | // ExportView logs the view data. 17 | func (e *exporter) ExportView(vd *view.Data) {} 18 | 19 | // ExportSpan logs the trace span. 20 | func (e *exporter) ExportSpan(vd *trace.SpanData) {} 21 | -------------------------------------------------------------------------------- /logutil/swap_signal.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package logutil 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/go-kit/kit/log" 12 | "github.com/go-kit/kit/log/level" 13 | ) 14 | 15 | func swapLevelHandler(base log.Logger, swapLogger *log.SwapLogger, debug bool) { 16 | sigChan := make(chan os.Signal, 1) 17 | signal.Notify(sigChan, syscall.SIGUSR2) 18 | for { 19 | <-sigChan 20 | if debug { 21 | newLogger := level.NewFilter(base, level.AllowInfo()) 22 | swapLogger.Swap(newLogger) 23 | } else { 24 | newLogger := level.NewFilter(base, level.AllowDebug()) 25 | swapLogger.Swap(newLogger) 26 | } 27 | level.Info(swapLogger).Log("msg", "swapping level", "debug", !debug) 28 | debug = !debug 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /httputil/middleware_example_test.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | ) 8 | 9 | func ExampleChain() { 10 | h := Chain( 11 | annotate("one"), 12 | annotate("two"), 13 | annotate("three"), 14 | )(myHandler()) 15 | 16 | srv := httptest.NewServer(h) 17 | defer srv.Close() 18 | 19 | resp, err := http.Get(srv.URL) // nolint:noctx 20 | if err != nil { 21 | panic(err) 22 | } 23 | defer resp.Body.Close() 24 | 25 | // Output: 26 | // annotate: one 27 | // annotate: two 28 | // annotate: three 29 | } 30 | 31 | func annotate(s string) Middleware { 32 | return func(next http.Handler) http.Handler { 33 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | fmt.Println("annotate: ", s) 35 | next.ServeHTTP(w, r) 36 | }) 37 | } 38 | } 39 | 40 | func myHandler() http.Handler { 41 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kolide 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 | -------------------------------------------------------------------------------- /fsutil/fsrecursivecopy_test.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "os" 7 | "path" 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | //go:embed test-data/fscopy 15 | var embeddedSourceData embed.FS 16 | 17 | func TestCopyFSToDisk(t *testing.T) { 18 | t.Parallel() 19 | 20 | subdir, err := fs.Sub(embeddedSourceData, "test-data/fscopy") 21 | require.NoError(t, err) 22 | 23 | destDir := t.TempDir() 24 | 25 | require.NoError(t, CopyFSToDisk(subdir, destDir, CommonFileMode)) 26 | 27 | var tests = []struct { 28 | path string 29 | }{ 30 | {path: "top.txt"}, 31 | {path: path.Join("level1", "level2", "deep.txt")}, 32 | {path: path.Join("level1", "l1.txt")}, 33 | {path: path.Join("levelA", "levelB", "b.txt")}, 34 | {path: path.Join("levelA", "a.txt")}, 35 | } 36 | 37 | for _, tt := range tests { 38 | tt := tt 39 | t.Run(tt.path, func(t *testing.T) { 40 | t.Parallel() 41 | 42 | expected := "hello\n" 43 | if runtime.GOOS == "windows" { 44 | expected = "hello\r\n" 45 | } 46 | 47 | contents, err := os.ReadFile(path.Join(destDir, tt.path)) 48 | require.NoError(t, err) 49 | require.Equal(t, expected, string(contents)) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kolide/kit 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc 9 | github.com/go-kit/kit v0.7.0 10 | github.com/google/uuid v1.3.0 11 | github.com/jmoiron/sqlx v0.0.0-20180406164412-2aeb6a910c2b 12 | github.com/oklog/ulid v0.3.0 13 | github.com/opencensus-integrations/ocsql v0.1.1 14 | github.com/pkg/errors v0.8.0 15 | github.com/stretchr/testify v1.2.1 16 | go.opencensus.io v0.22.1 17 | google.golang.org/grpc v1.56.3 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.0 // indirect 22 | github.com/go-logfmt/logfmt v0.3.0 // indirect 23 | github.com/go-sql-driver/mysql v1.4.1 // indirect 24 | github.com/go-stack/stack v1.7.0 // indirect 25 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect 28 | github.com/lib/pq v1.0.0 // indirect 29 | github.com/mattn/go-sqlite3 v1.10.0 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | golang.org/x/net v0.38.0 // indirect 32 | golang.org/x/sys v0.31.0 // indirect 33 | golang.org/x/text v0.23.0 // indirect 34 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 35 | google.golang.org/protobuf v1.33.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /pgutil/pgutil_test.go: -------------------------------------------------------------------------------- 1 | package pgutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestConversion(t *testing.T) { 10 | t.Parallel() 11 | 12 | var tests = []struct { 13 | in string 14 | opts []Opts 15 | out string 16 | err bool 17 | }{ 18 | { 19 | in: "postgres://myuser:mypass@localhost/somedatabase", 20 | out: "host=localhost port=5432 dbname=somedatabase sslmode=require user=myuser password=mypass", 21 | }, 22 | { 23 | in: "postgres://myuser@localhost:1234/somedatabase", 24 | out: "host=localhost port=1234 dbname=somedatabase sslmode=require user=myuser", 25 | }, 26 | { 27 | in: "postgres://myuser:mypass@localhost/somedatabase", 28 | opts: []Opts{WithSSL(SSLBlank)}, 29 | out: "host=localhost port=5432 dbname=somedatabase sslmode= user=myuser password=mypass", 30 | }, 31 | { 32 | in: "postgres://myuser:mypass@localhost/somedatabase", 33 | opts: []Opts{WithSSL(SSLDisable)}, 34 | out: "host=localhost port=5432 dbname=somedatabase sslmode=disable user=myuser password=mypass", 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | tt := tt 40 | t.Run("", func(t *testing.T) { 41 | t.Parallel() 42 | 43 | c, err := NewFromURL(tt.in, tt.opts...) 44 | if tt.err { 45 | require.Error(t, err) 46 | return 47 | } 48 | 49 | require.NoError(t, err) 50 | require.Equal(t, tt.out, c.String()) 51 | }) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kolide Kit [](https://godoc.org/github.com/kolide/kit) 2 | 3 | Kolide Kit is a collection of Go libraries used in projects at Kolide. This repository also includes a few other features which are useful for Go developers: 4 | 5 | - A lightweight style guide 6 | - Links to libraries which are commonly used at Kolide 7 | - Links to learning resources outlining some Go best practices 8 | 9 | ## Install 10 | 11 | ``` 12 | git clone git@github.com:kolide/kit.git $GOPATH/src/github.com/kolide/kit 13 | ``` 14 | 15 | ## Documentation 16 | 17 | Run `godoc -http=:6060` and then open `http://localhost:6060/pkg/github.com/kolide/kit/` in your browser. You'll see all the available packages in this repository. 18 | 19 | ## Style Guide 20 | 21 | You will also be able to find Kolide's Go style guide at [styleguide.md](./styleguide.md). We write a lot of Go at Kolide and we like talking about how we can all write better, more consistent Go. In our style guide, we've amalgamated the results of numerous internal discussions and agreed upon best approaches. 22 | 23 | ## Git and Dependency Workflow 24 | 25 | Our development and dependency management workflow is outlined in [workflow.md](./workflow.md). At a high-level, we use [`glide`](https://github.com/Masterminds/glide) most of the time, but we use [`dep`](https://github.com/golang/dep) for newer projects. Our git workflow has us using forks and feature branches extensively. 26 | -------------------------------------------------------------------------------- /contexts/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | // Package uuid provides helpers for propagating UUIDs across services. 2 | package uuid 3 | 4 | import ( 5 | "context" 6 | 7 | grpctransport "github.com/go-kit/kit/transport/grpc" 8 | "github.com/google/uuid" 9 | "google.golang.org/grpc/metadata" 10 | ) 11 | 12 | // Use a private type to prevent name collisions with other packages. 13 | type key string 14 | 15 | const uuidKey key = "UUID" 16 | 17 | // NewContext creates a new context with the UUID set to the provided value 18 | func NewContext(ctx context.Context, uuid string) context.Context { 19 | return context.WithValue(ctx, uuidKey, uuid) 20 | } 21 | 22 | // FromContext returns the UUID value stored in ctx, if any. 23 | func FromContext(ctx context.Context) (string, bool) { 24 | uuid, ok := ctx.Value(uuidKey).(string) 25 | return uuid, ok 26 | } 27 | 28 | // NewForRequest returns a Random (Version 4) UUID string. 29 | // If the UUID fails to generate an empty string will be returned. 30 | func NewForRequest() string { 31 | uuid, err := uuid.NewRandom() 32 | if err != nil { 33 | return "" 34 | } 35 | return uuid.String() 36 | } 37 | 38 | // Attach adds the UUID values stored in context to the gRPC request metadata. 39 | func Attach() grpctransport.ClientOption { 40 | return grpctransport.ClientBefore( 41 | func(ctx context.Context, md *metadata.MD) context.Context { 42 | uuid, _ := FromContext(ctx) 43 | return grpctransport.SetRequestHeader("uuid", uuid)(ctx, md) 44 | }, 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /health/health.go: -------------------------------------------------------------------------------- 1 | // Package health adds methods for checking the health of service dependencies. 2 | package health 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/go-kit/kit/log" 8 | ) 9 | 10 | // Checker returns an error indicating if a service is in an unhealthy state. 11 | // Checkers should be implemented by dependencies which can fail, like a DB or mail service. 12 | type Checker interface { 13 | HealthCheck() error 14 | } 15 | 16 | // Handler returns an http.Handler that checks the status of all the dependencies. 17 | // Handler responds with either: 18 | // 200 OK if the server can successfully communicate with it's backends or 19 | // 500 if any of the backends are reporting an issue. 20 | func Handler(logger log.Logger, checkers map[string]Checker) http.HandlerFunc { 21 | return func(w http.ResponseWriter, r *http.Request) { 22 | healthy := CheckHealth(logger, checkers) 23 | if !healthy { 24 | w.WriteHeader(http.StatusInternalServerError) 25 | return 26 | } 27 | } 28 | } 29 | 30 | // CheckHealth checks multiple checkers returning false if any of them fail. 31 | // CheckHealth logs the reason a checker fails. 32 | func CheckHealth(logger log.Logger, checkers map[string]Checker) bool { 33 | healthy := true 34 | for name, hc := range checkers { 35 | if err := hc.HealthCheck(); err != nil { 36 | log.With(logger, "component", "healthz").Log("err", err, "health-checker", name) 37 | healthy = false 38 | continue 39 | } 40 | } 41 | return healthy 42 | } 43 | 44 | // Nop creates a noop checker. Useful in tests. 45 | func Nop() Checker { 46 | return nop{} 47 | } 48 | 49 | type nop struct{} 50 | 51 | func (c nop) HealthCheck() error { 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /health/health_test.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/go-kit/kit/log" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCheckHealth(t *testing.T) { 15 | t.Parallel() 16 | 17 | checkers := map[string]Checker{ 18 | "fail": fail{}, 19 | "pass": Nop(), 20 | } 21 | 22 | healthy := CheckHealth(log.NewNopLogger(), checkers) 23 | require.False(t, healthy) 24 | 25 | checkers = map[string]Checker{ 26 | "pass": Nop(), 27 | } 28 | healthy = CheckHealth(log.NewNopLogger(), checkers) 29 | require.True(t, healthy) 30 | } 31 | 32 | type fail struct{} 33 | 34 | func (c fail) HealthCheck() error { 35 | return errors.New("fail") 36 | } 37 | 38 | func TestHealthzHandler(t *testing.T) { 39 | t.Parallel() 40 | 41 | logger := log.NewNopLogger() 42 | failing := Handler(logger, map[string]Checker{ 43 | "mock": healthcheckFunc(func() error { 44 | return errors.New("health check failed") 45 | })}) 46 | 47 | ok := Handler(logger, map[string]Checker{ 48 | "mock": healthcheckFunc(func() error { 49 | return nil 50 | })}) 51 | 52 | var httpTests = []struct { 53 | wantHeader int 54 | handler http.Handler 55 | }{ 56 | {200, ok}, 57 | {500, failing}, 58 | } 59 | for _, tt := range httpTests { 60 | tt := tt 61 | t.Run("", func(t *testing.T) { 62 | t.Parallel() 63 | 64 | rr := httptest.NewRecorder() 65 | req := httptest.NewRequest("GET", "/healthz", nil) 66 | tt.handler.ServeHTTP(rr, req) 67 | assert.Equal(t, rr.Code, tt.wantHeader) 68 | }) 69 | } 70 | 71 | } 72 | 73 | type healthcheckFunc func() error 74 | 75 | func (fn healthcheckFunc) HealthCheck() error { 76 | return fn() 77 | } 78 | -------------------------------------------------------------------------------- /testutil/testutil.go: -------------------------------------------------------------------------------- 1 | // Package testutil provides utilities for use in tests. 2 | package testutil 3 | 4 | import ( 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // ErrorAfter will return an error after the provided timeout has passed if 12 | // the provided WaitGroup has not unblocked. All Add() calls to the WaitGroup 13 | // must be performed before calling this function. 14 | func ErrorAfter(timeout time.Duration, wg *sync.WaitGroup) error { 15 | done := make(chan struct{}) 16 | go func() { 17 | // Turn the blocking wg.Wait() into a selectable channel close 18 | wg.Wait() 19 | close(done) 20 | }() 21 | select { 22 | case <-time.After(timeout): 23 | return fmt.Errorf("test exceeded timeout: %v", timeout) 24 | case <-done: 25 | return nil 26 | } 27 | } 28 | 29 | // ErrorAfterFunc will return an error after the provided timeout has passed if 30 | // the provided function has not returned. 31 | func ErrorAfterFunc(timeout time.Duration, f func()) error { 32 | var wg sync.WaitGroup 33 | wg.Add(1) 34 | go func() { 35 | defer wg.Done() 36 | f() 37 | }() 38 | return ErrorAfter(timeout, &wg) 39 | } 40 | 41 | // FatalAfterFunc will fatal the test after the provided timeout has passed if 42 | // the provided function has not returned. 43 | func FatalAfterFunc(t testing.TB, timeout time.Duration, f func()) { 44 | if err := ErrorAfterFunc(timeout, f); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | 49 | // FatalAfter will fatal the test after the provided timeout has passed if the 50 | // provided WaitGroup has not unblocked. All Add() calls to the WaitGroup must 51 | // be performed before calling this function. 52 | func FatalAfter(t testing.TB, timeout time.Duration, wg *sync.WaitGroup) { 53 | if err := ErrorAfter(timeout, wg); err != nil { 54 | t.Fatal(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /httputil/httputil.go: -------------------------------------------------------------------------------- 1 | // Package httputil provides utilities on top of the net/http package. 2 | package httputil 3 | 4 | import ( 5 | "crypto/tls" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/kolide/kit/tlsutil" 10 | ) 11 | 12 | // Option configures an HTTP Server. 13 | type Option func(*http.Server) 14 | 15 | // WithTLSConfig allows overriding the default TLS Config in the call to NewServer. 16 | func WithTLSConfig(cfg *tls.Config) Option { 17 | return func(s *http.Server) { 18 | s.TLSConfig = cfg 19 | } 20 | } 21 | 22 | // WithReadTimeout sets the ReadTimeout option 23 | func WithReadTimeout(t time.Duration) Option { 24 | return func(s *http.Server) { 25 | s.ReadTimeout = t 26 | } 27 | } 28 | 29 | // WithWriteTimeout sets the WriteTimeout option 30 | func WithWriteTimeout(t time.Duration) Option { 31 | return func(s *http.Server) { 32 | s.WriteTimeout = t 33 | } 34 | } 35 | 36 | // WithReadHeaderTimeout sets the ReadHeaderTimeout option 37 | func WithReadHeaderTimeout(t time.Duration) Option { 38 | return func(s *http.Server) { 39 | s.ReadHeaderTimeout = t 40 | } 41 | } 42 | 43 | // WithIdleTimeout sets the IdleTimeout option 44 | func WithIdleTimeout(t time.Duration) Option { 45 | return func(s *http.Server) { 46 | s.IdleTimeout = t 47 | } 48 | } 49 | 50 | // NewServer creates an HTTP Server with pre-configured timeouts and a secure TLS Config. 51 | func NewServer(addr string, h http.Handler, opts ...Option) *http.Server { 52 | srv := http.Server{ 53 | Addr: addr, 54 | Handler: h, 55 | ReadTimeout: 25 * time.Second, 56 | WriteTimeout: 40 * time.Second, 57 | ReadHeaderTimeout: 5 * time.Second, 58 | IdleTimeout: 5 * time.Minute, 59 | MaxHeaderBytes: 1 << 18, // 0.25 MB (262144 bytes) 60 | } 61 | 62 | for _, opt := range opts { 63 | opt(&srv) 64 | } 65 | 66 | // set a strict TLS config by default. 67 | if srv.TLSConfig == nil { 68 | srv.TLSConfig = tlsutil.NewConfig() 69 | } 70 | 71 | return &srv 72 | } 73 | -------------------------------------------------------------------------------- /fsutil/fsrecursivecopy.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path" 8 | ) 9 | 10 | // CopyFSToDisk copies an embedded FS to a given directory. Because go's embed does not preserve the file mode, you must 11 | // also pass a function that will return the desired file mode for each file. 12 | func CopyFSToDisk(src fs.FS, destDir string, modeSetter func(fs.FileInfo) os.FileMode) error { 13 | if err := fs.WalkDir(src, ".", genCopyToDiskFunc(src, destDir, modeSetter)); err != nil { 14 | return fmt.Errorf("walking directory: %w", err) 15 | } 16 | 17 | return nil 18 | } 19 | 20 | // CommonFileMode is a function that returns the common file permissions: 0644 for all files, and 0755 for 21 | // all directories. It is provided as a helper for CopyFSToDisk's common use case 22 | func CommonFileMode(fi fs.FileInfo) fs.FileMode { 23 | if fi.IsDir() { 24 | return 0755 25 | } else { 26 | return 0644 27 | } 28 | } 29 | 30 | // genCopyToDiskFunc returns fs.WalkDirFunc function that will 31 | // copy files to disk in a given location. 32 | func genCopyToDiskFunc(srcFS fs.FS, destDir string, modeSetter func(fs.FileInfo) os.FileMode) fs.WalkDirFunc { 33 | return func(filepath string, d fs.DirEntry, err error) error { 34 | if err != nil { 35 | return err 36 | } 37 | 38 | fileinfo, err := d.Info() 39 | if err != nil { 40 | return fmt.Errorf("getting file info: %w", err) 41 | } 42 | 43 | fullpath := path.Join(destDir, filepath) 44 | 45 | // If it's a directory, make it under destdir 46 | if d.IsDir() { 47 | if err := os.MkdirAll(fullpath, modeSetter(fileinfo)); err != nil { 48 | return fmt.Errorf("making directory %s: %w", fullpath, err) 49 | } 50 | return nil 51 | } 52 | 53 | data, err := fs.ReadFile(srcFS, filepath) 54 | if err != nil { 55 | return fmt.Errorf("reading file from FS %s: %w", filepath, err) 56 | } 57 | 58 | if err := os.WriteFile(fullpath, data, modeSetter(fileinfo)); err != nil { 59 | return fmt.Errorf("writing %s: %w", filepath, err) 60 | } 61 | 62 | return nil 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /munemo/dialects.go: -------------------------------------------------------------------------------- 1 | package munemo 2 | 3 | // dialect represents a munemo dielect. 4 | type dialect struct { 5 | symbols []string 6 | negativeSymbol string 7 | } 8 | 9 | // Munemo2 is a reworked set of symbols. These are alphebetically sortable. 10 | var Munemo2 = dialect{ 11 | // unused: q 12 | symbols: []string{ 13 | "ba", "be", "bi", "bo", "bu", 14 | "ca", "ce", "ci", "co", "cu", 15 | "da", "de", "di", "do", "du", 16 | "fa", "fe", "fi", "fo", "fu", 17 | "ga", "ge", "gi", "go", "gu", 18 | "ha", "he", "hi", "ho", "hu", 19 | "ja", "je", "ji", "jo", "ju", 20 | "ka", "ke", "ki", "ko", "ku", 21 | "la", "le", "li", "lo", "lu", 22 | "ma", "me", "mi", "mo", "mu", 23 | "na", "ne", "ni", "no", "nu", 24 | "pa", "pe", "pi", "po", "pu", 25 | "ra", "re", "ri", "ro", "ru", 26 | "sa", "se", "si", "so", "su", 27 | "ta", "te", "ti", "to", "tu", 28 | "va", "ve", "vi", "vo", "vu", 29 | "wa", "we", "wi", "wo", "wu", 30 | "xa", "xe", "xi", "xo", "xu", 31 | "ya", "ye", "yi", "yo", "yu", 32 | "za", "ze", "zi", "zo", "zu", 33 | }, 34 | negativeSymbol: "aa", 35 | } 36 | 37 | // Orignal is the original munemo spec. It comes from 38 | // https://github.com/jmettraux/munemo and is deprecated because it's 39 | // non-sortable and variable length. 40 | var Original = dialect{ 41 | symbols: []string{ 42 | "ba", "bi", "bu", "be", "bo", 43 | "cha", "chi", "chu", "che", "cho", 44 | "da", "di", "du", "de", "do", 45 | "fa", "fi", "fu", "fe", "fo", 46 | "ga", "gi", "gu", "ge", "go", 47 | "ha", "hi", "hu", "he", "ho", 48 | "ja", "ji", "ju", "je", "jo", 49 | "ka", "ki", "ku", "ke", "ko", 50 | "la", "li", "lu", "le", "lo", 51 | "ma", "mi", "mu", "me", "mo", 52 | "na", "ni", "nu", "ne", "no", 53 | "pa", "pi", "pu", "pe", "po", 54 | "ra", "ri", "ru", "re", "ro", 55 | "sa", "si", "su", "se", "so", 56 | "sha", "shi", "shu", "she", "sho", 57 | "ta", "ti", "tu", "te", "to", 58 | "tsa", "tsi", "tsu", "tse", "tso", 59 | "wa", "wi", "wu", "we", "wo", 60 | "ya", "yi", "yu", "ye", "yo", 61 | "za", "zi", "zu", "ze", "zo", 62 | }, 63 | negativeSymbol: "xa", 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: read 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: [main, master] 11 | tags: '*' 12 | pull_request: 13 | branches: '**' 14 | merge_group: 15 | types: [checks_requested] 16 | 17 | 18 | jobs: 19 | go_test: 20 | name: go test 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: 26 | - ubuntu-latest 27 | - macos-latest 28 | - windows-latest 29 | steps: 30 | - name: Check out code 31 | id: checkout 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 1 35 | 36 | - name: Setup Go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version-file: './go.mod' 40 | check-latest: true 41 | cache: false 42 | 43 | - id: go-cache-paths 44 | shell: bash 45 | run: | 46 | echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" 47 | echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" 48 | 49 | - name: cache restore - GOCACHE 50 | uses: actions/cache/restore@v4 51 | with: 52 | path: ${{ steps.go-cache-paths.outputs.go-build }} 53 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 54 | enableCrossOsArchive: true 55 | 56 | - name: cache restore - GOMODCACHE 57 | uses: actions/cache/restore@v4 58 | with: 59 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 60 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 61 | enableCrossOsArchive: true 62 | 63 | - name: Go Test 64 | shell: bash 65 | run: go test -race -cover -coverprofile=coverage.out ./... 66 | 67 | - name: Upload coverage 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: ${{ runner.os }}-coverage.out 71 | path: ./coverage.out 72 | if-no-files-found: error 73 | 74 | ci_mergeable: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - run: true 78 | needs: 79 | - go_test 80 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: read 6 | 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | branches: '**' 12 | merge_group: 13 | types: [checks_requested] 14 | 15 | 16 | jobs: 17 | golangci: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [macos-latest, windows-latest, ubuntu-latest] 22 | name: lint 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version-file: './go.mod' 29 | check-latest: true 30 | 31 | - name: golangci-lint 32 | uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea 33 | with: 34 | skip-save-cache: true 35 | 36 | 37 | 38 | govulncheck: 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | os: [macos-latest, windows-latest, ubuntu-latest] 43 | name: govulncheck 44 | runs-on: ${{ matrix.os }} 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - id: govulncheck 49 | uses: golang/govulncheck-action@v1 50 | with: 51 | go-version-file: './go.mod' 52 | check-latest: true 53 | go-package: ./... 54 | actionlint: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Set up Go 1.x 59 | uses: actions/setup-go@v5 60 | with: 61 | go-version-file: './go.mod' 62 | check-latest: true 63 | cache: false 64 | - name: install actionlint 65 | run: go install github.com/rhysd/actionlint/cmd/actionlint@latest 66 | - name: actionlint 67 | run: | 68 | echo "::add-matcher::.github/actionlint-matcher.json" 69 | actionlint -color 70 | 71 | 72 | # This job is here as a github status check -- it allows us to move 73 | # the merge dependency from being on all the jobs to this single 74 | # one. 75 | lint_mergeable: 76 | runs-on: ubuntu-latest 77 | steps: 78 | - run: true 79 | needs: 80 | - golangci 81 | - govulncheck 82 | - actionlint -------------------------------------------------------------------------------- /workflow.md: -------------------------------------------------------------------------------- 1 | # Workflows 2 | 3 | ## Git 4 | 5 | At Kolide, we use GitHub for source control. 6 | 7 | * Projects live in the [GOPATH](https://github.com/golang/go/wiki/GOPATH) at the original `$GOPATH/src/github.com/kolide/$repo` path. 8 | * `github.com/kolide/$repo` is used as the git origin, with your fork being added as a remote. The workflow for a new feature branch becomes: 9 | 10 | ``` 11 | # First you would clone a repo 12 | git clone git@github.com:kolide/kit.git $GOPATH/src/github.com/kolide/kit 13 | cd $GOPATH/src/github.com/kolide/kit 14 | 15 | # Add your fork as a git remote 16 | $username = "groob" # this should be whatever your GitHub username is 17 | git remote add $username git@github.com:$username/kit.git 18 | 19 | # Pull from origin 20 | git pull origin master --rebase 21 | 22 | # Create your feature 23 | git checkout -b feature-branch 24 | 25 | # Push to your fork 26 | git push -u $username feature-branch 27 | 28 | # Open a pull request on GitHub. 29 | 30 | # Continue to push to your fork as you iterate 31 | git add . 32 | git commit 33 | git push $username feature-branch 34 | ``` 35 | 36 | * Prefer small, self contained feature branches. 37 | * Request code reviews from at least one person on your team. 38 | * You can commit to your branch however many times you like, but we have found that using the "Squash and Merge" feature on GitHub works well for us. Once a Pull Request goes through code review and receives approval, the original author should squash and merge the pull request, adding a final commit message which will show up in the master branch's commit history. 39 | 40 | ## Go dependencies 41 | 42 | Historically we've used [`glide`](https://github.com/Masterminds/glide#glide-vendor-package-management-for-golang) to manage Go dependencies, but we've started to adopt [`dep`](https://github.com/golang/dep) for newer projects. 43 | 44 | Using dep requires that you edit the `Gopkg.toml` file with constraints and overrides for the project. See the [oficial docs](https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md) for an up to date guide on the Gopkg file format. 45 | You can run `dep ensure -examples` to see a list of commonly used `dep` commands. 46 | -------------------------------------------------------------------------------- /env/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package env provides utility functions for loading environment variables with defaults. 3 | 4 | A common use of the env package is for combining flag with environment variables in a Go program. 5 | 6 | Example: 7 | 8 | func main() { 9 | var ( 10 | flProject = flag.String("http.addr", env.String("HTTP_ADDRESS", ":https"), "HTTP server address") 11 | ) 12 | flag.Parse() 13 | } 14 | */ 15 | package env 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strconv" 21 | "time" 22 | ) 23 | 24 | // String returns the environment variable value specified by the key parameter, 25 | // otherwise returning a default value if set. 26 | func String(key, def string) string { 27 | if env, ok := os.LookupEnv(key); ok { 28 | return env 29 | } 30 | return def 31 | } 32 | 33 | // Int returns the environment variable value specified by the key parameter, 34 | // parsed as an integer. If the environment variable is not set, the default 35 | // value is returned. If parsing the integer fails, Int will exit the program. 36 | func Int(key string, def int) int { 37 | if env, ok := os.LookupEnv(key); ok { 38 | i, err := strconv.Atoi(env) 39 | if err != nil { 40 | fmt.Fprintf(os.Stderr, "env: parse int from flag: %s\n", err) 41 | os.Exit(1) 42 | } 43 | return i 44 | } 45 | return def 46 | } 47 | 48 | // Bool returns the environment variable value specified by the key parameter 49 | // (parsed as a boolean), otherwise returning a default value if set. 50 | func Bool(key string, def bool) bool { 51 | env, ok := os.LookupEnv(key) 52 | if !ok { 53 | return def 54 | } 55 | 56 | switch env { 57 | case "true", "T", "TRUE", "1": 58 | return true 59 | case "false", "F", "FALSE", "0": 60 | return false 61 | default: 62 | return def 63 | } 64 | } 65 | 66 | // Duration returns the environment variable value specified by the key parameter, 67 | // otherwise returning a default value if set. 68 | // If the time.Duration value cannot be parsed, Duration will exit the program 69 | // with an error status. 70 | func Duration(key string, def time.Duration) time.Duration { 71 | if env, ok := os.LookupEnv(key); ok { 72 | t, err := time.ParseDuration(env) 73 | if err != nil { 74 | fmt.Fprintf(os.Stderr, "env: parse time.Duration from flag: %s\n", err) 75 | os.Exit(1) 76 | } 77 | return t 78 | } 79 | return def 80 | } 81 | -------------------------------------------------------------------------------- /logutil/logutil.go: -------------------------------------------------------------------------------- 1 | // Package logutil has utilities for working with the Go Kit log package. 2 | package logutil 3 | 4 | import ( 5 | "os" 6 | 7 | "github.com/go-kit/kit/log" 8 | "github.com/go-kit/kit/log/level" 9 | ) 10 | 11 | // Fatal logs a error message and exits the process. 12 | func Fatal(logger log.Logger, args ...interface{}) { 13 | level.Info(logger).Log(args...) 14 | os.Exit(1) 15 | } 16 | 17 | // SetLevelKey changes the "level" key in a Go Kit logger, allowing the user 18 | // to set it to something else. Useful for deploying services to GCP, as 19 | // stackdriver expects a "severity" key instead. 20 | // 21 | // see https://github.com/go-kit/kit/issues/503 22 | func SetLevelKey(logger log.Logger, key interface{}) log.Logger { 23 | return log.LoggerFunc(func(keyvals ...interface{}) error { 24 | for i := 1; i < len(keyvals); i += 2 { 25 | if _, ok := keyvals[i].(level.Value); ok { 26 | // overwriting the key without copying keyvals 27 | // techically violates the log.Logger contract 28 | // but is safe in this context because none 29 | // of the loggers in this program retain a reference 30 | // to keyvals 31 | keyvals[i-1] = key 32 | break 33 | } 34 | } 35 | return logger.Log(keyvals...) 36 | }) 37 | } 38 | 39 | // NewServerLogger creates a standard logger for Kolide services. 40 | // The logger will output JSON structured logs with a 41 | // "severity" field set to either "info" or "debug". 42 | // The acceptable level can be swapped by sending SIGUSR2 to the process. 43 | func NewServerLogger(debug bool) log.Logger { 44 | base := log.NewJSONLogger(log.NewSyncWriter(os.Stderr)) 45 | return newLogger(debug, base) 46 | } 47 | 48 | // NewCLILogger creates a standard logger for Kolide CLI tools. 49 | // The logger will output leveled logs with a 50 | // "severity" field set to either "info" or "debug". 51 | // The acceptable level can be swapped by sending SIGUSR2 to the process. 52 | func NewCLILogger(debug bool) log.Logger { 53 | base := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 54 | return newLogger(debug, base) 55 | } 56 | 57 | func newLogger(debug bool, base log.Logger) log.Logger { 58 | base = log.With(base, "ts", log.DefaultTimestampUTC) 59 | base = SetLevelKey(base, "severity") 60 | base = level.NewInjector(base, level.InfoValue()) 61 | 62 | lev := level.AllowInfo() 63 | if debug { 64 | lev = level.AllowDebug() 65 | } 66 | 67 | base = log.With(base, "caller", log.Caller(6)) 68 | 69 | var swapLogger log.SwapLogger 70 | swapLogger.Swap(level.NewFilter(base, lev)) 71 | 72 | go swapLevelHandler(base, &swapLogger, debug) 73 | return &swapLogger 74 | } 75 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | linters: 5 | enable: 6 | - bodyclose 7 | - containedctx 8 | - gofmt 9 | - govet 10 | - ineffassign 11 | - misspell 12 | - noctx 13 | - perfsprint 14 | - rowserrcheck 15 | - sloglint 16 | - sqlclosecheck 17 | - staticcheck 18 | - usetesting 19 | - unconvert 20 | - unused 21 | - gocritic 22 | - nakedret 23 | - predeclared 24 | - revive 25 | - exhaustive 26 | disable: 27 | - errcheck 28 | - gosec 29 | - gosimple 30 | 31 | linters-settings: 32 | errcheck: 33 | exclude-functions: [github.com/go-kit/kit/log:Log] 34 | gofmt: 35 | simplify: false 36 | gocritic: 37 | disabled-checks: 38 | - ifElseChain 39 | - elseif 40 | sloglint: 41 | kv-only: true 42 | context: "all" 43 | key-naming-case: snake 44 | static-msg: true 45 | revive: 46 | rules: 47 | - name: superfluous-else 48 | severity: warning 49 | disabled: false 50 | arguments: 51 | - "preserveScope" 52 | - name: package-comments 53 | disabled: false 54 | - name: context-as-argument 55 | disabled: false 56 | - name: context-keys-type 57 | disabled: false 58 | - name: error-return 59 | disabled: false 60 | - name: errorf 61 | disabled: false 62 | - name: unreachable-code 63 | disabled: false 64 | - name: early-return 65 | disabled: false 66 | - name: confusing-naming 67 | disabled: false 68 | - name: defer 69 | disabled: false 70 | staticcheck: 71 | checks: ["all"] 72 | 73 | issues: 74 | exclude-rules: 75 | # False positive: https://github.com/kunwardeep/paralleltest/issues/8. 76 | - linters: 77 | - paralleltest 78 | text: "does not use range value in test Run" 79 | # We prefer fmt.Sprintf over string concatenation for readability 80 | - linters: [perfsprint] 81 | text: "fmt.Sprintf can be replaced with string concatenation" 82 | - linters: [perfsprint] 83 | text: "fmt.Sprintf can be replaced with faster hex.EncodeToString" 84 | - linters: [perfsprint] 85 | text: "fmt.Sprintf can be replaced with faster strconv.FormatBool" 86 | - linters: [perfsprint] 87 | text: "fmt.Sprintf can be replaced with faster strconv.FormatInt" 88 | - linters: [perfsprint] 89 | text: "fmt.Sprintf can be replaced with faster strconv.FormatUint" 90 | - linters: [perfsprint] 91 | text: "fmt.Sprintf can be replaced with faster strconv.Itoa" 92 | - linters: [perfsprint] 93 | text: "fmt.Sprint can be replaced with faster strconv.Itoa" 94 | exclude-dirs: 95 | - test-cmds 96 | -------------------------------------------------------------------------------- /pgutil/pgutil.go: -------------------------------------------------------------------------------- 1 | // Package pgutil provides utilities for Postgres 2 | package pgutil 3 | 4 | import ( 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ConnectionOptions represents the configurable options of a connection to a 13 | // Postgres database 14 | type ConnectionOptions struct { 15 | Host string 16 | Port string 17 | User string 18 | Password string 19 | DBName string 20 | SSLMode string 21 | } 22 | 23 | type Opts func(*ConnectionOptions) 24 | 25 | // Supported SSL modes. 26 | type sslMode string 27 | 28 | const ( 29 | SSLBlank sslMode = "" 30 | SSLDisable sslMode = "disable" 31 | SSLAllow sslMode = "allow" 32 | SSLPrefer sslMode = "prefer" 33 | SSLRequire sslMode = "require" 34 | SSLVerifyCa sslMode = "verify-ca" 35 | SSLVerifyFull sslMode = "verify-full" 36 | ) 37 | 38 | // WithSSL sets the sslmode parameter for postgresql. See the 39 | // postgresql documentation at 40 | // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING 41 | func WithSSL(requestedSslMode sslMode) Opts { 42 | return func(c *ConnectionOptions) { 43 | c.SSLMode = string(requestedSslMode) 44 | } 45 | } 46 | 47 | // NewFromURL returns a ConnectionOptions from a given URL. This uses 48 | // a format like `postgres://myuser:mypass@localhost/somedatabase`, 49 | // which is commonly found on hosting platforms. 50 | func NewFromURL(rawurl string, opts ...Opts) (ConnectionOptions, error) { 51 | var c ConnectionOptions 52 | parsed, err := url.Parse(rawurl) 53 | if err != nil { 54 | return c, errors.Wrap(err, "url parse") 55 | } 56 | 57 | c = ConnectionOptions{ 58 | Host: parsed.Host, 59 | User: parsed.User.Username(), 60 | DBName: strings.TrimPrefix(parsed.Path, "/"), 61 | SSLMode: string(SSLRequire), 62 | } 63 | 64 | if pass, ok := parsed.User.Password(); ok { 65 | c.Password = pass 66 | } 67 | 68 | // Split the URL host/port into parts 69 | hostComponents := strings.Split(parsed.Host, ":") 70 | switch len(hostComponents) { 71 | case 1: 72 | c.Host = hostComponents[0] 73 | c.Port = "5432" 74 | case 2: 75 | c.Host = hostComponents[0] 76 | c.Port = hostComponents[1] 77 | default: 78 | return c, errors.Errorf("Could not parse %s as host:port", parsed.Host) 79 | } 80 | 81 | for _, opt := range opts { 82 | opt(&c) 83 | } 84 | 85 | return c, nil 86 | } 87 | 88 | // String implements the Stringer interface so that a pgutil.ConnectionOptions 89 | // can be converted into a value key/value connection string 90 | func (c ConnectionOptions) String() string { 91 | s := fmt.Sprintf( 92 | "host=%s port=%s dbname=%s sslmode=%s user=%s", 93 | c.Host, c.Port, c.DBName, c.SSLMode, c.User, 94 | ) 95 | 96 | if c.Password != "" { 97 | s = fmt.Sprintf("%s password=%s", s, c.Password) 98 | } 99 | return s 100 | } 101 | -------------------------------------------------------------------------------- /env/env_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestDuration(t *testing.T) { //nolint:paralleltest 10 | var tests = []struct { 11 | value time.Duration 12 | }{ 13 | {value: 1 * time.Second}, 14 | {value: 1 * time.Minute}, 15 | {value: 1 * time.Hour}, 16 | } 17 | 18 | for _, tt := range tests { //nolint:paralleltest 19 | t.Run(tt.value.String(), func(t *testing.T) { 20 | key := strings.ToUpper(tt.value.String()) 21 | t.Setenv(key, tt.value.String()) 22 | 23 | def := 10 * time.Minute 24 | if have, want := Duration(key, def), tt.value; have != want { 25 | t.Errorf("have %s, want %s", have, want) 26 | } 27 | }) 28 | } 29 | 30 | // test default value 31 | def := 10 * time.Minute 32 | if have, want := Duration("TEST_DEFAULT", def), def; have != want { 33 | t.Errorf("have %s, want %s", have, want) 34 | } 35 | } 36 | 37 | func TestString(t *testing.T) { //nolint:paralleltest 38 | var tests = []struct { 39 | value string 40 | }{ 41 | {value: "foo"}, 42 | {value: "bar"}, 43 | {value: "baz"}, 44 | } 45 | 46 | for _, tt := range tests { //nolint:paralleltest 47 | t.Run(tt.value, func(t *testing.T) { 48 | key := strings.ToUpper(tt.value) 49 | t.Setenv(key, tt.value) 50 | 51 | def := "default_value" 52 | if have, want := String(key, def), tt.value; have != want { 53 | t.Errorf("have %s, want %s", have, want) 54 | } 55 | }) 56 | } 57 | 58 | // test default value 59 | def := "default_value" 60 | if have, want := String("TEST_DEFAULT", def), def; have != want { 61 | t.Errorf("have %s, want %s", have, want) 62 | } 63 | } 64 | 65 | func TestBool(t *testing.T) { //nolint:paralleltest 66 | var tests = []struct { 67 | env string 68 | value bool 69 | }{ 70 | {env: "TRUE", value: true}, 71 | {env: "true", value: true}, 72 | {env: "1", value: true}, 73 | {env: "F", value: false}, 74 | {env: "FALSE", value: false}, 75 | {env: "false", value: false}, 76 | {env: "0", value: false}, 77 | } 78 | 79 | for _, tt := range tests { //nolint:paralleltest 80 | t.Run(tt.env, func(t *testing.T) { 81 | key := "TEST_BOOL" 82 | t.Setenv(key, tt.env) 83 | 84 | def := false 85 | if have, want := Bool(key, def), tt.value; have != want { 86 | t.Errorf("have %v, want %v", have, want) 87 | } 88 | def = true 89 | if have, want := Bool(key, def), tt.value; have != want { 90 | t.Errorf("have %v, want %v", have, want) 91 | } 92 | }) 93 | } 94 | 95 | // test default value 96 | def := true 97 | if have, want := Bool("TEST_DEFAULT", def), def; have != want { 98 | t.Errorf("have %v, want %v", have, want) 99 | } 100 | } 101 | 102 | func TestInt(t *testing.T) { //nolint:paralleltest 103 | var tests = []struct { 104 | env string 105 | value int 106 | }{ 107 | {env: "1337", value: 1337}, 108 | {env: "1", value: 1}, 109 | {env: "-34", value: -34}, 110 | {env: "0", value: 0}, 111 | } 112 | 113 | for _, tt := range tests { //nolint:paralleltest 114 | t.Run(tt.env, func(t *testing.T) { 115 | key := "TEST_INT" 116 | t.Setenv(key, tt.env) 117 | 118 | if have, want := Int(key, 10), tt.value; have != want { 119 | t.Errorf("have %v, want %v", have, want) 120 | } 121 | }) 122 | } 123 | 124 | // test default value 125 | def := 11 126 | if have, want := Int("TEST_DEFAULT", def), def; have != want { 127 | t.Errorf("have %v, want %v", have, want) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /fsutil/filesystem_test.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestUntarBundle(t *testing.T) { 16 | t.Parallel() 17 | 18 | // Create tarball contents 19 | originalDir := t.TempDir() 20 | topLevelFile := filepath.Join(originalDir, "testfile.txt") 21 | var topLevelFileMode fs.FileMode = 0655 22 | require.NoError(t, os.WriteFile(topLevelFile, []byte("test1"), topLevelFileMode)) 23 | internalDir := filepath.Join(originalDir, "some", "path", "to") 24 | var nestedFileMode fs.FileMode = 0755 25 | require.NoError(t, os.MkdirAll(internalDir, nestedFileMode)) 26 | nestedFile := filepath.Join(internalDir, "anotherfile.txt") 27 | require.NoError(t, os.WriteFile(nestedFile, []byte("test2"), nestedFileMode)) 28 | 29 | // Create test tarball 30 | tarballDir := t.TempDir() 31 | tarballFile := filepath.Join(tarballDir, "test.gz") 32 | createTar(t, tarballFile, originalDir) 33 | 34 | // Confirm we can untar the tarball successfully 35 | newDir := t.TempDir() 36 | require.NoError(t, UntarBundle(filepath.Join(newDir, "anything"), tarballFile)) 37 | 38 | // Confirm the tarball has the contents we expect 39 | newTopLevelFile := filepath.Join(newDir, filepath.Base(topLevelFile)) 40 | require.FileExists(t, newTopLevelFile) 41 | newNestedFile := filepath.Join(newDir, "some", "path", "to", filepath.Base(nestedFile)) 42 | require.FileExists(t, newNestedFile) 43 | 44 | // Confirm each file retained its original permissions 45 | // windows doesn't really support posix permissions, and golang doesn't fake much of it. So skip these on windows 46 | if runtime.GOOS != "windows" { 47 | topLevelFileInfo, err := os.Stat(newTopLevelFile) 48 | require.NoError(t, err) 49 | require.Equal(t, topLevelFileMode.String(), topLevelFileInfo.Mode().String()) 50 | nestedFileInfo, err := os.Stat(newNestedFile) 51 | require.NoError(t, err) 52 | require.Equal(t, nestedFileMode.String(), nestedFileInfo.Mode().String()) 53 | } 54 | } 55 | 56 | // createTar is a helper to create a test tar 57 | func createTar(t *testing.T, createLocation string, sourceDir string) { 58 | tarballFile, err := os.Create(createLocation) 59 | require.NoError(t, err) 60 | defer tarballFile.Close() 61 | 62 | gzw := gzip.NewWriter(tarballFile) 63 | defer gzw.Close() 64 | 65 | tw := tar.NewWriter(gzw) 66 | defer tw.Close() 67 | 68 | require.NoError(t, tw.AddFS(os.DirFS(sourceDir))) 69 | } 70 | 71 | func TestSanitizeExtractPath(t *testing.T) { 72 | t.Parallel() 73 | 74 | var tests = []struct { 75 | filepath string 76 | destination string 77 | expectError bool 78 | }{ 79 | { 80 | filepath: "file", 81 | destination: "/tmp", 82 | expectError: false, 83 | }, 84 | { 85 | filepath: "subdir/../subdir/file", 86 | destination: "/tmp", 87 | expectError: false, 88 | }, 89 | 90 | { 91 | filepath: "../../../file", 92 | destination: "/tmp", 93 | expectError: true, 94 | }, 95 | { 96 | filepath: "./././file", 97 | destination: "/tmp", 98 | expectError: false, 99 | }, 100 | } 101 | 102 | for _, tt := range tests { 103 | if tt.expectError { 104 | require.Error(t, sanitizeExtractPath(tt.filepath, tt.destination), tt.filepath) 105 | } else { 106 | require.NoError(t, sanitizeExtractPath(tt.filepath, tt.destination), tt.filepath) 107 | } 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /munemo/munemo_test.go: -------------------------------------------------------------------------------- 1 | package munemo 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // testCase contains a single munemo test case. If the string and int 12 | // are defined, they are expected to convert between them. If the 13 | // string and error are defined, it represents an error state. (All 14 | // ints are expected to be able to translate) 15 | type testCase struct { 16 | i int 17 | s string 18 | e string 19 | skipInt bool // special handle a zero conversion 20 | } 21 | 22 | var originalTests = []testCase{ 23 | {s: "dibaba", i: 110000}, 24 | {s: "xadibaba", i: -110000}, 25 | {s: "didaba", i: 111000}, 26 | {s: "dihisho", i: 112674}, 27 | {s: "ba", i: 0}, 28 | {s: "xaba", i: 0, skipInt: true}, 29 | {s: "shuposhe", i: 725973}, 30 | {s: "xabi", i: -1}, 31 | {s: "babi", i: 1, skipInt: true}, // leading zero 32 | 33 | {s: "hello", e: "decode failed: unknown syllable llo"}, 34 | } 35 | 36 | var munemo2Tests = []testCase{ 37 | {i: -1, s: "aabe"}, 38 | {i: -100, s: "aabeba"}, 39 | {i: -32, s: "aabaji", skipInt: true}, // leading zero 40 | {i: -99, s: "aazu"}, 41 | {i: 0, s: "ba"}, 42 | {i: 1, s: "be"}, 43 | {i: 100, s: "beba"}, 44 | {i: 101, s: "bebe"}, 45 | {i: 25437225, s: "halotiha"}, 46 | {i: 33, s: "bajo", skipInt: true}, // leading zero 47 | {i: 392406, s: "kuguce"}, 48 | {i: 73543569, s: "tonukasu"}, 49 | {i: 936710, s: "yosida"}, 50 | {i: 99, s: "zu"}, 51 | 52 | {s: "hello", e: "decode failed: unknown syllable llo"}, 53 | {s: "qabixabi", e: "decode failed: unknown syllable qabixabi"}, 54 | } 55 | 56 | func TestMunemoMunemo2(t *testing.T) { 57 | t.Parallel() 58 | mg := New() 59 | testMunemo(t, mg, munemo2Tests) 60 | } 61 | 62 | func TestMunemoOriginal(t *testing.T) { 63 | t.Parallel() 64 | mg := New(WithDialect(Original)) 65 | testMunemo(t, mg, originalTests) 66 | } 67 | 68 | func testMunemo(t *testing.T, mg *munemoGenerator, tests []testCase) { 69 | for _, tt := range tests { 70 | tt := tt 71 | if tt.e == "" { 72 | // If we lack an error, this is a legit conversion. Try both ways 73 | if !tt.skipInt { 74 | t.Run(fmt.Sprintf("string/%d", tt.i), func(t *testing.T) { 75 | t.Parallel() 76 | 77 | ret := mg.String(tt.i) 78 | require.Equal(t, tt.s, ret) 79 | }) 80 | } 81 | 82 | t.Run(fmt.Sprintf("int/%s", tt.s), func(t *testing.T) { 83 | t.Parallel() 84 | 85 | ret, err := mg.Int(tt.s) 86 | assert.Equal(t, tt.i, ret) 87 | assert.NoError(t, err) 88 | }) 89 | } else { 90 | // Having an error, means we're looking for an error 91 | t.Run(fmt.Sprintf("interr/%s", tt.s), func(t *testing.T) { 92 | t.Parallel() 93 | 94 | ret, err := mg.Int(tt.s) 95 | require.Equal(t, tt.i, ret) 96 | require.EqualError(t, err, tt.e) 97 | }) 98 | } 99 | 100 | } 101 | 102 | } 103 | 104 | func TestLegacyInterfaces(t *testing.T) { 105 | t.Parallel() 106 | 107 | for _, tt := range originalTests { 108 | tt := tt 109 | if tt.e == "" { 110 | // If we lack an error, this is a legit conversion. Try both ways 111 | if !tt.skipInt { 112 | t.Run(fmt.Sprintf("Munemo/%d", tt.i), func(t *testing.T) { 113 | t.Parallel() 114 | 115 | ret := Munemo(tt.i) 116 | require.Equal(t, tt.s, ret) 117 | }) 118 | } 119 | 120 | t.Run(fmt.Sprintf("UnMunemo/%s", tt.s), func(t *testing.T) { 121 | t.Parallel() 122 | 123 | ret, err := UnMunemo(tt.s) 124 | assert.Equal(t, tt.i, ret) 125 | assert.NoError(t, err) 126 | }) 127 | } else { 128 | // Having an error, means we're looking for an error 129 | t.Run(fmt.Sprintf("UnMunemo/%s", tt.s), func(t *testing.T) { 130 | t.Parallel() 131 | 132 | ret, err := UnMunemo(tt.s) 133 | require.Equal(t, tt.i, ret) 134 | require.EqualError(t, err, tt.e) 135 | }) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /testutil/testutil_test.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestErrorAfterSuccess(t *testing.T) { 10 | t.Parallel() 11 | 12 | var wg sync.WaitGroup 13 | wg.Add(1) 14 | go func() { 15 | wg.Done() 16 | }() 17 | if err := ErrorAfter(1*time.Second, &wg); err != nil { 18 | t.Errorf("ErrorAfter should have succeeded, but failed with error: %v", err) 19 | } 20 | } 21 | 22 | func TestErrorAfterError(t *testing.T) { 23 | t.Parallel() 24 | 25 | var wg sync.WaitGroup 26 | wg.Add(1) 27 | go func() { 28 | time.Sleep(100 * time.Millisecond) 29 | wg.Done() 30 | }() 31 | if err := ErrorAfter(10*time.Millisecond, &wg); err == nil { 32 | t.Error("ErrorAfter should have errored") 33 | } 34 | } 35 | 36 | func TestErrorAfterFuncSuccess(t *testing.T) { 37 | t.Parallel() 38 | 39 | err := ErrorAfterFunc(100*time.Millisecond, func() { 40 | time.Sleep(1 * time.Millisecond) 41 | }) 42 | if err != nil { 43 | t.Errorf("ErrorAfterFunc should have succeeded, but failed with error: %v", err) 44 | } 45 | } 46 | 47 | func TestErrorAfterFuncError(t *testing.T) { 48 | t.Parallel() 49 | 50 | err := ErrorAfterFunc(1*time.Millisecond, func() { 51 | time.Sleep(100 * time.Millisecond) 52 | }) 53 | if err == nil { 54 | t.Error("ErrorAfterFunc should have errored") 55 | } 56 | } 57 | 58 | // Wrapper for testing.T with Fatal and Fatalf mocked 59 | type mockFatal struct { 60 | testing.T 61 | // DidFatal records whether a call to Fatal or Fatalf occurred 62 | DidFatal bool 63 | // mut should be locked when accessing DidFatal 64 | mut sync.Mutex 65 | } 66 | 67 | func (m *mockFatal) Fatal(...interface{}) { 68 | m.mut.Lock() 69 | defer m.mut.Unlock() 70 | m.DidFatal = true 71 | } 72 | 73 | func (m *mockFatal) Fatalf(string, ...interface{}) { 74 | m.mut.Lock() 75 | defer m.mut.Unlock() 76 | m.DidFatal = true 77 | } 78 | 79 | func TestFatalAfterFuncSuccess(t *testing.T) { 80 | t.Parallel() 81 | 82 | var m mockFatal 83 | 84 | // Should not fatal 85 | FatalAfterFunc(&m, 100*time.Millisecond, func() { 86 | time.Sleep(1 * time.Millisecond) 87 | }) 88 | 89 | m.mut.Lock() 90 | defer m.mut.Unlock() 91 | if m.DidFatal { 92 | t.Error("FatalAfter should have succeeded") 93 | } 94 | } 95 | 96 | func TestFatalAfterFuncFatal(t *testing.T) { 97 | t.Parallel() 98 | 99 | var m mockFatal 100 | 101 | // Should fatal 102 | FatalAfterFunc(&m, 1*time.Millisecond, func() { 103 | time.Sleep(100 * time.Millisecond) 104 | }) 105 | 106 | m.mut.Lock() 107 | defer m.mut.Unlock() 108 | if !m.DidFatal { 109 | t.Error("FatalAfter should have fataled") 110 | } 111 | } 112 | 113 | func ExampleFatalAfterFunc_success() { 114 | var t testing.T 115 | // This test will pass 116 | FatalAfterFunc(&t, 10*time.Millisecond, func() { 117 | time.Sleep(1 * time.Millisecond) 118 | }) 119 | } 120 | 121 | func ExampleFatalAfterFunc_fatal() { 122 | var t testing.T 123 | // This test will fatal 124 | FatalAfterFunc(&t, 10*time.Millisecond, func() { 125 | time.Sleep(1 * time.Millisecond) 126 | }) 127 | } 128 | 129 | func TestFatalAfterSuccess(t *testing.T) { 130 | t.Parallel() 131 | 132 | var wg sync.WaitGroup 133 | wg.Add(1) 134 | go func() { 135 | wg.Done() 136 | }() 137 | 138 | var m mockFatal 139 | // Should not fatal 140 | FatalAfter(&m, 1*time.Second, &wg) 141 | m.mut.Lock() 142 | defer m.mut.Unlock() 143 | if m.DidFatal { 144 | t.Error("FatalAfter should have succeeded") 145 | } 146 | } 147 | 148 | func TestFatalAfterFatal(t *testing.T) { 149 | t.Parallel() 150 | 151 | var wg sync.WaitGroup 152 | wg.Add(1) 153 | go func() { 154 | time.Sleep(100 * time.Millisecond) 155 | wg.Done() 156 | }() 157 | 158 | var m mockFatal 159 | FatalAfter(&m, 10*time.Millisecond, &wg) 160 | 161 | // Don't allow test to exit before fatal occurs 162 | time.Sleep(20 * time.Millisecond) 163 | 164 | m.mut.Lock() 165 | defer m.mut.Unlock() 166 | if !m.DidFatal { 167 | t.Error("FatalAfter should have fataled") 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /dbutil/dbutil.go: -------------------------------------------------------------------------------- 1 | // Package dbutil provides utilities for managing connections to a SQL database. 2 | package dbutil 3 | 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/go-kit/kit/log" 10 | "github.com/go-kit/kit/log/level" 11 | "github.com/jmoiron/sqlx" 12 | "github.com/opencensus-integrations/ocsql" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type dbConfig struct { 17 | logger log.Logger 18 | maxAttempts int 19 | wrapWithCensus bool 20 | alreadyRegisteredDriver bool 21 | censusTraceOptions []ocsql.TraceOption 22 | } 23 | 24 | // WithLogger configures a logger Option. 25 | func WithLogger(logger log.Logger) Option { 26 | return func(c *dbConfig) { 27 | c.logger = logger 28 | } 29 | } 30 | 31 | // WithMaxAttempts configures the number of maximum attempts to make 32 | func WithMaxAttempts(maxAttempts int) Option { 33 | return func(c *dbConfig) { 34 | c.maxAttempts = maxAttempts 35 | } 36 | } 37 | 38 | func WithCensusDriver() Option { 39 | return func(c *dbConfig) { 40 | c.wrapWithCensus = true 41 | } 42 | } 43 | 44 | func WithCensusTraceOptions(opts ...ocsql.TraceOption) Option { 45 | return func(c *dbConfig) { 46 | c.censusTraceOptions = opts 47 | } 48 | } 49 | 50 | // alreadyRegisteredDriver is a private option to use when passing options from 51 | // OpenDBX to OpenDB. 52 | func alreadyRegisteredDriver() Option { 53 | return func(c *dbConfig) { 54 | c.alreadyRegisteredDriver = true 55 | } 56 | } 57 | 58 | // Option provides optional configuration for managing DB connections. 59 | type Option func(*dbConfig) 60 | 61 | // OpenDB creates a sql.DB connection to the database driver. 62 | // OpenDB uses a linear backoff timer when attempting to establish a connection, 63 | // only returning after the connection is successful or the number of attempts exceeds 64 | // the maxAttempts value(defaults to 15 attempts). 65 | func OpenDB(driver, dsn string, opts ...Option) (*sql.DB, error) { 66 | config := &dbConfig{ 67 | logger: log.NewNopLogger(), 68 | censusTraceOptions: []ocsql.TraceOption{ocsql.WithAllTraceOptions()}, 69 | maxAttempts: 15, 70 | } 71 | 72 | for _, opt := range opts { 73 | opt(config) 74 | } 75 | 76 | if config.wrapWithCensus && !config.alreadyRegisteredDriver { 77 | driverName, err := ocsql.Register(driver, config.censusTraceOptions...) 78 | if err != nil { 79 | return nil, errors.Wrapf(err, "wrapping driver %s with opencensus sql %s", driver, driverName) 80 | } 81 | driver = driverName 82 | } 83 | 84 | db, err := sql.Open(driver, dsn) 85 | if err != nil { 86 | return nil, errors.Wrapf(err, "opening %s connection, dsn=%s", driver, dsn) 87 | } 88 | 89 | var dbError error 90 | for attempt := 0; attempt < config.maxAttempts; attempt++ { 91 | dbError = db.Ping() 92 | if dbError == nil { 93 | // we're connected! 94 | break 95 | } 96 | interval := time.Duration(attempt) * time.Second 97 | level.Info(config.logger).Log(driver, fmt.Sprintf( 98 | "could not connect to db: %v, sleeping %v", dbError, interval)) 99 | time.Sleep(interval) 100 | } 101 | if dbError != nil { 102 | return nil, dbError 103 | } 104 | 105 | return db, nil 106 | } 107 | 108 | // dbutil.OpenDBX is similar to dbutil.OpenDB, except it returns a *sqlx.DB from 109 | // the popular github.com/jmoiron/sqlx package. 110 | func OpenDBX(driver, dsn string, opts ...Option) (*sqlx.DB, error) { 111 | config := &dbConfig{ 112 | censusTraceOptions: []ocsql.TraceOption{ocsql.WithAllTraceOptions()}, 113 | } 114 | for _, opt := range opts { 115 | opt(config) 116 | } 117 | 118 | driverName := driver 119 | if config.wrapWithCensus && !config.alreadyRegisteredDriver { 120 | d, err := ocsql.Register(driver, config.censusTraceOptions...) 121 | if err != nil { 122 | return nil, errors.Wrapf(err, "wrapping driver %s with opencensus sql %s", driver, driverName) 123 | } 124 | opts = append(opts, alreadyRegisteredDriver()) 125 | driverName = d 126 | } 127 | 128 | db, err := OpenDB(driverName, dsn, opts...) 129 | if err != nil { 130 | return nil, errors.Wrap(err, "opening database/sql database connection") 131 | } 132 | 133 | // never wrap the NewDb with a driver name, just sql.Open 134 | return sqlx.NewDb(db, driver), nil 135 | } 136 | -------------------------------------------------------------------------------- /munemo/munemo.go: -------------------------------------------------------------------------------- 1 | // Munemo is a reversible numeric encoding library. It's designed to 2 | // take id numbers, and present them in more human friendly forms. It 3 | // does this by using a set of symbols, and doing a base conversion to 4 | // them. There are a couple of known dialects. 5 | // 6 | // Original: This is compatible with the original symbol set. This has 7 | // the disadvantage of being variable length, and non-sortable. 8 | // 9 | // Munemo2: This symbol set was developed as a replacement. All 10 | // symbols are 2 characters, and it creates sortable strings. 11 | // 12 | // It is inspired by the ruby library 13 | // https://github.com/jmettraux/munemo. 14 | package munemo 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | ) 20 | 21 | type munemoGenerator struct { 22 | dialect dialect 23 | } 24 | 25 | // Option is the functional option type for munemoGenerator 26 | type Option func(*munemoGenerator) 27 | 28 | // WithDialect defines the dialect to be used 29 | func WithDialect(d dialect) Option { 30 | return func(mg *munemoGenerator) { 31 | mg.dialect = d 32 | } 33 | } 34 | 35 | // New returns a munemo generator for the given dialect. 36 | func New(opts ...Option) *munemoGenerator { 37 | mg := &munemoGenerator{ 38 | dialect: Munemo2, 39 | } 40 | for _, opt := range opts { 41 | opt(mg) 42 | } 43 | 44 | return mg 45 | } 46 | 47 | // String takes an integer and returns the mumemo encoded string 48 | func (mg *munemoGenerator) String(id int) string { 49 | m := newMunemo(mg.dialect) 50 | m.calculate(id) 51 | return m.string() 52 | } 53 | 54 | // Int takes a string, and returns an integer. In the case of error, 55 | // an error is returned. 56 | func (mg *munemoGenerator) Int(s string) (int, error) { 57 | m := newMunemo(mg.dialect) 58 | err := m.decode(s) 59 | return m.int(), err 60 | } 61 | 62 | // Munemo is a legacy interface to munemo encoding. It defaults to the 63 | // original dialect 64 | func Munemo(id int) string { 65 | m := newMunemo(Original) 66 | m.calculate(id) 67 | return m.string() 68 | } 69 | 70 | // UnMunemo is a legacy interface to reverse munemo encoding. It 71 | // defaults to the original dialect. 72 | func UnMunemo(s string) (int, error) { 73 | m := newMunemo(Original) 74 | err := m.decode(s) 75 | return m.int(), err 76 | } 77 | 78 | type munemo struct { 79 | negativeSymbol string 80 | symbols []string 81 | buffer *bytes.Buffer 82 | number int 83 | symbolValues map[string]int 84 | sign int 85 | } 86 | 87 | func newMunemo(d dialect) *munemo { 88 | m := &munemo{ 89 | symbols: d.symbols, 90 | negativeSymbol: d.negativeSymbol, 91 | sign: 1, 92 | symbolValues: make(map[string]int), 93 | buffer: new(bytes.Buffer), 94 | } 95 | for k, v := range m.symbols { 96 | m.symbolValues[v] = k 97 | } 98 | 99 | return m 100 | } 101 | 102 | func (m *munemo) string() string { 103 | return m.buffer.String() 104 | } 105 | 106 | func (m *munemo) int() int { 107 | return m.number * m.sign 108 | } 109 | 110 | func (m *munemo) decode(s string) error { 111 | // negative if the first two bytes match the negative symbol 112 | if s[0:2] == m.negativeSymbol { 113 | m.sign = -1 114 | s = s[2:] 115 | } 116 | 117 | // As long as there are characters, parse them 118 | // Read the first syllable, interpret, remove. 119 | for { 120 | if s == "" { 121 | break 122 | } 123 | 124 | // Syllables are 2 or 3 letters. Check to see if the first 2 or 3 125 | // characters are in our array of syllables. 126 | if val, ok := m.symbolValues[s[0:2]]; ok { 127 | m.number = len(m.symbols)*m.number + val 128 | s = s[2:] 129 | } else if val, ok := m.symbolValues[s[0:3]]; ok { 130 | m.number = len(m.symbols)*m.number + val 131 | s = s[3:] 132 | } else { 133 | m.number = 0 134 | return fmt.Errorf("decode failed: unknown syllable %s", s) 135 | } 136 | } 137 | // No errors! 138 | return nil 139 | } 140 | 141 | func (m *munemo) calculate(number int) { 142 | if number < 0 { 143 | m.buffer.Write([]byte(m.negativeSymbol)) 144 | number = -number 145 | } 146 | 147 | modulo := number % len(m.symbols) 148 | result := number / len(m.symbols) 149 | 150 | if result > 0 { 151 | m.calculate(result) 152 | } 153 | 154 | m.buffer.Write([]byte(m.symbols[modulo])) 155 | } 156 | -------------------------------------------------------------------------------- /fsutil/filesystem.go: -------------------------------------------------------------------------------- 1 | // Package fsutil provides filesystem-related functions. 2 | package fsutil 3 | 4 | import ( 5 | "archive/tar" 6 | "compress/gzip" 7 | "fmt" 8 | "io" 9 | "io/fs" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/kolide/kit/env" 15 | ) 16 | 17 | const ( 18 | // DirMode is the default permission used when creating directories 19 | DirMode = 0755 20 | // FileMode is the default permission used when creating files 21 | FileMode = 0644 22 | ) 23 | 24 | // Gopath will return the current GOPATH as set by environment variables and 25 | // will fall back to ~/go if a GOPATH is not set. 26 | func Gopath() string { 27 | home := env.String("HOME", "~/") 28 | return env.String("GOPATH", filepath.Join(home, "go")) 29 | } 30 | 31 | // CopyDir is a utility to assist with copying a directory from src to dest. 32 | // Note that directory permissions are not maintained, but the permissions of 33 | // the files in those directories are. 34 | func CopyDir(src, dest string) error { 35 | dir, err := os.Open(src) 36 | if err != nil { 37 | return err 38 | } 39 | if err := os.MkdirAll(dest, DirMode); err != nil { 40 | return err 41 | } 42 | 43 | files, err := dir.Readdir(-1) 44 | if err != nil { 45 | return err 46 | } 47 | for _, file := range files { 48 | srcptr := filepath.Join(src, file.Name()) 49 | dstptr := filepath.Join(dest, file.Name()) 50 | if file.IsDir() { 51 | if err := CopyDir(srcptr, dstptr); err != nil { 52 | return err 53 | } 54 | } else { 55 | if err := CopyFile(srcptr, dstptr); err != nil { 56 | return err 57 | } 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | // CopyFile is a utility to assist with copying a file from src to dest. 64 | // Note that file permissions are maintained. 65 | func CopyFile(src, dest string) error { 66 | source, err := os.Open(src) 67 | if err != nil { 68 | return err 69 | } 70 | defer source.Close() 71 | 72 | destfile, err := os.Create(dest) 73 | if err != nil { 74 | return err 75 | } 76 | defer destfile.Close() 77 | 78 | _, err = io.Copy(destfile, source) 79 | if err != nil { 80 | return err 81 | } 82 | sourceinfo, err := os.Stat(src) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return os.Chmod(dest, sourceinfo.Mode()) 88 | } 89 | 90 | // UntarBundle will untar a source tar.gz archive to the supplied 91 | // destination. Note that this calls `filepath.Dir(destination)`, 92 | // which has the effect of stripping the last component from 93 | // destination. 94 | func UntarBundle(destination string, source string) error { 95 | f, err := os.Open(source) 96 | if err != nil { 97 | return fmt.Errorf("opening source: %w", err) 98 | } 99 | defer f.Close() 100 | 101 | gzr, err := gzip.NewReader(f) 102 | if err != nil { 103 | return fmt.Errorf("creating gzip reader from %s: %w", source, err) 104 | } 105 | defer gzr.Close() 106 | 107 | tr := tar.NewReader(gzr) 108 | for { 109 | header, err := tr.Next() 110 | if err == io.EOF { 111 | break 112 | } 113 | if err != nil { 114 | return fmt.Errorf("reading tar file: %w", err) 115 | } 116 | 117 | if err := sanitizeExtractPath(filepath.Dir(destination), header.Name); err != nil { 118 | return fmt.Errorf("checking filename: %w", err) 119 | } 120 | 121 | destPath := filepath.Join(filepath.Dir(destination), header.Name) 122 | info := header.FileInfo() 123 | if info.IsDir() { 124 | if err = os.MkdirAll(destPath, info.Mode()); err != nil { 125 | return fmt.Errorf("creating directory %s for tar file: %w", destPath, err) 126 | } 127 | continue 128 | } 129 | 130 | if err := writeBundleFile(destPath, info.Mode(), tr); err != nil { 131 | return fmt.Errorf("writing file: %w", err) 132 | } 133 | } 134 | return nil 135 | } 136 | 137 | func writeBundleFile(destPath string, perm fs.FileMode, srcReader io.Reader) error { 138 | file, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) 139 | if err != nil { 140 | return fmt.Errorf("opening %s: %w", destPath, err) 141 | } 142 | defer file.Close() 143 | if _, err := io.Copy(file, srcReader); err != nil { 144 | return fmt.Errorf("copying to %s: %w", destPath, err) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | // sanitizeExtractPath checks that the supplied extraction path is nor 151 | // vulnerable to zip slip attacks. See https://snyk.io/research/zip-slip-vulnerability 152 | func sanitizeExtractPath(filePath string, destination string) error { 153 | destpath := filepath.Join(destination, filePath) 154 | if !strings.HasPrefix(destpath, filepath.Clean(destination)+string(os.PathSeparator)) { 155 | return fmt.Errorf("%s: illegal file path", filePath) 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /styleguide.md: -------------------------------------------------------------------------------- 1 | # Go Style Guide 2 | 3 | It helps keep development and code review by having general consensus on a set of best practices which we all follow. Our internal style guide is a set of code standards that we try to adhere to whenever possible. Some high-level guidance is: 4 | 5 | * Defer to the Go [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments#go-code-review-comments). We largely follow the same conventions in our code. 6 | * Follow these [best practices](https://peter.bourgon.org/go-best-practices-2016/) from Peter Bourgon. 7 | * Avoid package level variables and `init`. Avoiding global state leads to code which is more readable, testable and maintainable. See [this blog](https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html). 8 | * Write tests using the [testify library](https://godoc.org/github.com/stretchr/testify/assert). 9 | * Preferably write your tests as a [table test](https://github.com/golang/go/wiki/TableDrivenTests). 10 | * Use [subtests](https://blog.golang.org/subtests) to run your table driven tests. Subtests provide a way to better handle test failures and and [parallelize](https://rakyll.org/parallelize-test-tables/) tests. Consider the following example test: 11 | ```go 12 | func TestAuthenticatedHost(t *testing.T) { 13 | // set up test dependencies 14 | ctx := context.Background() 15 | goodNodeKey, err := svc.EnrollAgent(ctx, "foobarbaz", "host123") 16 | 17 | // use require if the test cannot continue if the assertion fails 18 | require.Nil(t, err) 19 | require.NotEmpty(t, goodNodeKey) 20 | 21 | // create a []struct for your test cases 22 | var authenticatedHostTests = []struct { 23 | nodeKey string 24 | shouldErr bool 25 | }{ 26 | { 27 | nodeKey: "invalid", 28 | shouldErr: true, 29 | }, 30 | { 31 | nodeKey: "", 32 | shouldErr: true, 33 | }, 34 | { 35 | nodeKey: goodNodeKey, 36 | shouldErr: false, 37 | }, 38 | } 39 | 40 | // use subtests to run through your test cases. 41 | for _, tt := range authenticatedHostTests { 42 | t.Run("", func(t *testing.T) { 43 | var r = struct{ NodeKey string }{NodeKey: tt.nodeKey} 44 | _, err = endpoint(context.Background(), r) 45 | if tt.shouldErr { 46 | assert.IsType(t, osqueryError{}, err) 47 | } else { 48 | assert.Nil(t, err) 49 | } 50 | }) 51 | } 52 | 53 | } 54 | ``` 55 | 56 | * Use functional options for optional function parameters. [blog](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis), [video](https://www.youtube.com/watch?v=24lFtGHWxAQ) 57 | 58 | Example: 59 | Let's say you have a `Client` struct, which will implement an API client and has a default timeout of 5 seconds. One way to create the Client would be to write a function like: 60 | ```go 61 | NewClient(baseurl *url.URL, timeout time.Duration, debugMode bool) *Client 62 | ``` 63 | 64 | But every time you'll want to add a new configuration parameter, you'll have to make a breaking change to NewClient. A cleaner, more extensible solution is to write it with the following pattern: 65 | ```go 66 | // Declare a function type for modifying the client 67 | type Option(*Client) 68 | 69 | // WithTimeout sets the timeout on the Client. 70 | func WithTimeout(d time.Duration) Option { 71 | return func(c *Client) { 72 | c.timeout = d 73 | } 74 | } 75 | 76 | func Debug() Option { 77 | return func(c *Client) { 78 | c.debug = true 79 | } 80 | } 81 | ``` 82 | 83 | Now you can write the client which will accept a variadic number of option arguments. 84 | 85 | ```go 86 | NewClient(baseurl *url.URL, opts ...Option) *Client { 87 | // create a client with some default values. 88 | client := &Client{ 89 | timeout: 5 * time.Minute, 90 | } 91 | 92 | // loop through the provided options and override any of the defaults. 93 | for _, opt := range opts { 94 | opt(&client) 95 | } 96 | 97 | return &client 98 | } 99 | ``` 100 | 101 | * Propagate a context through your API. 102 | The `context` package provides a standard way for managing cancellations and request scoped values in a Go program. When writing server and client code, it is recommended to add `context.Context` as the first argument to your methods. 103 | For example, if you have a function like: 104 | 105 | ```go 106 | func User(id uint) (*User, error) 107 | ``` 108 | 109 | you should instead write it as: 110 | 111 | ```go 112 | func User(ctx context.Context, id uint) (*User, error) 113 | ``` 114 | 115 | 116 | See the following resources on `context.Context`: 117 | * https://blog.golang.org/context 118 | * https://peter.bourgon.org/blog/2016/07/11/context.html 119 | * [justforfunc video on context use](https://www.youtube.com/watch?v=LSzR0VEraWw&index=1&list=PL64wiCrrxh4Jisi7OcCJIUpguV_f5jGnZ) 120 | * [GolangUK talk](https://www.youtube.com/watch?v=r4Mlm6qEWRs) 121 | -------------------------------------------------------------------------------- /version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestVersion(t *testing.T) { 12 | t.Parallel() 13 | 14 | now := time.Now().String() 15 | version = "test" 16 | buildDate = now 17 | 18 | info := Version() 19 | 20 | if have, want := info.Version, version; have != want { 21 | t.Errorf("have %s, want %s", have, want) 22 | } 23 | 24 | if have, want := info.BuildDate, now; have != want { 25 | t.Errorf("have %s, want %s", have, want) 26 | } 27 | 28 | if have, want := info.BuildUser, "unknown"; have != want { 29 | t.Errorf("have %s, want %s", have, want) 30 | } 31 | } 32 | 33 | func Test_VersionNum(t *testing.T) { 34 | t.Parallel() 35 | 36 | var tests = map[string]struct { 37 | semver string 38 | expectedVersionNum int 39 | }{ 40 | "empty version": { 41 | semver: "", 42 | expectedVersionNum: 0, 43 | }, 44 | "unset version": { 45 | semver: "unknown", 46 | expectedVersionNum: 0, 47 | }, 48 | "basic version": { 49 | semver: "1.2.3", 50 | expectedVersionNum: 1002003, 51 | }, 52 | "4_part_version": { 53 | semver: "1.2.3.4", 54 | expectedVersionNum: 1002003, 55 | }, 56 | "max version": { 57 | semver: "999.999.999", 58 | expectedVersionNum: 999999999, 59 | }, 60 | "semver with leading v": { 61 | semver: "v1.1.2", 62 | expectedVersionNum: 1001002, 63 | }, 64 | "semver with leading zeros": { 65 | semver: "01.01.002", 66 | expectedVersionNum: 1001002, 67 | }, 68 | "semver with trailing branch info": { 69 | semver: "1.10.3-1-g98Paoe", 70 | expectedVersionNum: 1010003, 71 | }, 72 | "semver with leading v and trailing branch info": { 73 | semver: "v1.10.3-1-g98Paoe", 74 | expectedVersionNum: 1010003, 75 | }, 76 | "zero version": { 77 | semver: "0.0.0", 78 | expectedVersionNum: 0, 79 | }, 80 | } 81 | 82 | for name, tt := range tests { 83 | tt := tt 84 | t.Run(name, func(t *testing.T) { 85 | t.Parallel() 86 | require.Equal(t, tt.expectedVersionNum, VersionNumFromSemver(tt.semver)) 87 | }) 88 | } 89 | } 90 | 91 | func Test_SemverFromVersionNum(t *testing.T) { 92 | t.Parallel() 93 | 94 | var tests = map[string]struct { 95 | versionNum int 96 | expectedSemver string 97 | }{ 98 | "zero version": { 99 | versionNum: 0, 100 | expectedSemver: "0.0.0", 101 | }, 102 | "1.10.3": { 103 | versionNum: 1010003, 104 | expectedSemver: "1.10.3", 105 | }, 106 | "max version": { 107 | versionNum: 999999999, 108 | expectedSemver: "999.999.999", 109 | }, 110 | "1.112.43": { 111 | versionNum: 1112043, 112 | expectedSemver: "1.112.43", 113 | }, 114 | } 115 | 116 | for name, tt := range tests { 117 | tt := tt 118 | t.Run(name, func(t *testing.T) { 119 | t.Parallel() 120 | require.Equal(t, tt.expectedSemver, SemverFromVersionNum(tt.versionNum)) 121 | }) 122 | } 123 | } 124 | 125 | func Test_VersionNumComparisons(t *testing.T) { 126 | t.Parallel() 127 | 128 | var tests = map[string]struct { 129 | lesserVersion string 130 | greaterVersion string 131 | }{ 132 | "empty version": { 133 | lesserVersion: "", 134 | greaterVersion: "0.0.1", 135 | }, 136 | "basic versions": { 137 | lesserVersion: "1.2.3", 138 | greaterVersion: "1.2.4", 139 | }, 140 | "max versions": { 141 | lesserVersion: "999.999.998", 142 | greaterVersion: "999.999.999", 143 | }, 144 | "large minor versions, no collisions": { 145 | lesserVersion: "v1.999.999", 146 | greaterVersion: "v2.0.0", 147 | }, 148 | } 149 | 150 | for name, tt := range tests { 151 | tt := tt 152 | t.Run(name, func(t *testing.T) { 153 | t.Parallel() 154 | lesserParsed := VersionNumFromSemver(tt.lesserVersion) 155 | greaterParsed := VersionNumFromSemver(tt.greaterVersion) 156 | require.True(t, lesserParsed < greaterParsed, 157 | fmt.Sprintf("expected %s to parse as lesser than %s. got lesser %d >= greater %d", 158 | tt.lesserVersion, 159 | tt.greaterVersion, 160 | lesserParsed, 161 | greaterParsed, 162 | ), 163 | ) 164 | }) 165 | } 166 | } 167 | 168 | func Test_VersionNumIsReversible(t *testing.T) { 169 | t.Parallel() 170 | 171 | var tests = map[string]struct { 172 | testedVersion string 173 | }{ 174 | "zero version": { 175 | testedVersion: "0.0.0", 176 | }, 177 | "basic version": { 178 | testedVersion: "1.2.3", 179 | }, 180 | "max version": { 181 | testedVersion: "999.999.999", 182 | }, 183 | "random version": { 184 | testedVersion: "107.61.10", 185 | }, 186 | "random version 2": { 187 | testedVersion: "0.118.919", 188 | }, 189 | } 190 | 191 | for name, tt := range tests { 192 | tt := tt 193 | t.Run(name, func(t *testing.T) { 194 | t.Parallel() 195 | require.Equal(t, tt.testedVersion, SemverFromVersionNum(VersionNumFromSemver(tt.testedVersion))) 196 | }) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tlsutil/tlsutil.go: -------------------------------------------------------------------------------- 1 | // Package tlsutil provides utilities on top of the standard library TLS package. 2 | package tlsutil 3 | 4 | import ( 5 | "crypto/tls" 6 | "fmt" 7 | ) 8 | 9 | // Profile represents a collection of TLS CipherSuites and their compatibility with Web Browsers. 10 | // The different profile types are defined on the Mozilla wiki: https://wiki.mozilla.org/Security/Server_Side_TLS 11 | type Profile int 12 | 13 | const ( 14 | // Modern CipherSuites only. 15 | // This configuration is compatible with Firefox 27, Chrome 30, IE 11 on Windows 7, 16 | // Edge, Opera 17, Safari 9, Android 5.0, and Java 8. 17 | Modern Profile = iota 18 | 19 | // Intermediate supports a wider range of CipherSuites than Modern and 20 | // is compatible with Firefox 1, Chrome 1, IE 7, Opera 5 and Safari 1. 21 | Intermediate 22 | 23 | // Old provides backwards compatibility for legacy clients. 24 | // Should only be used as a last resort. 25 | Old 26 | ) 27 | 28 | func (p Profile) String() string { 29 | switch p { 30 | case Modern: 31 | return "modern" 32 | case Intermediate: 33 | return "intermediate" 34 | case Old: 35 | return "old" 36 | default: 37 | panic("unknown TLS profile constant: " + fmt.Sprintf("%d", p)) 38 | } 39 | } 40 | 41 | // Option is a TLS Config option. Options can be provided to the NewConfig function 42 | // when creating a TLS Config. 43 | type Option func(*tls.Config) 44 | 45 | // WithProfile overrides the default Profile when creating a new *tls.Config. 46 | func WithProfile(p Profile) Option { 47 | return func(config *tls.Config) { 48 | setProfile(config, p) 49 | } 50 | } 51 | 52 | // WithCertificates builds the tls.Config.NameToCertificate from the CommonName and 53 | // SubjectAlternateName fields of the provided certificate. 54 | // 55 | // WithCertificates is useful for creating a TLS Config for servers which require SNI, 56 | // for example reverse proxies. 57 | func WithCertificates(certs []tls.Certificate) Option { 58 | return func(config *tls.Config) { 59 | config.Certificates = append(config.Certificates, certs...) 60 | } 61 | } 62 | 63 | // NewConfig returns a configured *tls.Config. By default, the TLS Config is set to 64 | // MinVersion of TLS 1.2 and a Modern Profile. 65 | // 66 | // Use one of the available Options to modify the default config. 67 | func NewConfig(opts ...Option) *tls.Config { 68 | cfg := tls.Config{PreferServerCipherSuites: true} 69 | 70 | for _, opt := range opts { 71 | opt(&cfg) 72 | } 73 | 74 | // if a Profile was not specified, default to Modern. 75 | if cfg.MinVersion == 0 { 76 | setProfile(&cfg, Modern) 77 | } 78 | 79 | return &cfg 80 | } 81 | 82 | func setProfile(cfg *tls.Config, profile Profile) { 83 | switch profile { 84 | case Modern: 85 | cfg.MinVersion = tls.VersionTLS12 86 | cfg.CurvePreferences = append(cfg.CurvePreferences, 87 | tls.CurveP256, 88 | tls.CurveP384, 89 | tls.CurveP521, 90 | tls.X25519, 91 | ) 92 | cfg.CipherSuites = append(cfg.CipherSuites, 93 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 94 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 95 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 96 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 97 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 98 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 99 | ) 100 | case Intermediate: 101 | cfg.MinVersion = tls.VersionTLS10 102 | cfg.CurvePreferences = append(cfg.CurvePreferences, 103 | tls.CurveP256, 104 | tls.CurveP384, 105 | tls.CurveP521, 106 | tls.X25519, 107 | ) 108 | cfg.CipherSuites = append(cfg.CipherSuites, 109 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 110 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 111 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 112 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 113 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, 114 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 115 | tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 116 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 117 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 118 | tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 119 | tls.TLS_RSA_WITH_RC4_128_SHA, 120 | tls.TLS_RSA_WITH_AES_128_CBC_SHA, 121 | tls.TLS_RSA_WITH_AES_256_CBC_SHA, 122 | tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, 123 | ) 124 | case Old: 125 | cfg.MinVersion = tls.VersionTLS10 126 | cfg.CurvePreferences = append(cfg.CurvePreferences, 127 | tls.CurveP256, 128 | tls.CurveP384, 129 | tls.CurveP521, 130 | tls.X25519, 131 | ) 132 | cfg.CipherSuites = append(cfg.CipherSuites, 133 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 134 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 135 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 136 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 137 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, 138 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 139 | tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 140 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 141 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 142 | tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 143 | tls.TLS_RSA_WITH_RC4_128_SHA, 144 | tls.TLS_RSA_WITH_AES_128_CBC_SHA, 145 | tls.TLS_RSA_WITH_AES_256_CBC_SHA, 146 | tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, 147 | ) 148 | default: 149 | panic("invalid tls profile " + profile.String()) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /debug/debug.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | nhpprof "net/http/pprof" 8 | "net/url" 9 | "runtime/pprof" 10 | "strings" 11 | 12 | "github.com/alecthomas/template" 13 | "github.com/go-kit/kit/log" 14 | "github.com/go-kit/kit/log/level" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // Server is the debug server struct. It should be created through StartServer. 19 | type Server struct { 20 | serv *http.Server 21 | addr string 22 | authToken string 23 | logger log.Logger 24 | prefix string 25 | } 26 | 27 | // Option is the functional option type for Server. 28 | type Option func(*Server) 29 | 30 | // WithAddr sets the address to bind to. 31 | func WithAddr(addr string) Option { 32 | return func(s *Server) { 33 | s.addr = addr 34 | } 35 | } 36 | 37 | // WithAuthToken sets the auth token to use. If it is unset, there is no auth. 38 | func WithAuthToken(token string) Option { 39 | return func(s *Server) { 40 | s.authToken = token 41 | } 42 | } 43 | 44 | // WithLogger sets the logger to use. 45 | func WithLogger(logger log.Logger) Option { 46 | return func(s *Server) { 47 | s.logger = logger 48 | } 49 | } 50 | 51 | // WithPrefix sets the URL prefix to use. 52 | func WithPrefix(prefix string) Option { 53 | return func(s *Server) { 54 | s.prefix = prefix 55 | } 56 | } 57 | 58 | // StartServer creates and starts a new debug server using the provided 59 | // functional Options. 60 | func StartServer(opts ...Option) (*Server, error) { 61 | s := &Server{ 62 | addr: ":63809", 63 | authToken: "", 64 | logger: log.NewNopLogger(), 65 | prefix: "/", 66 | } 67 | for _, opt := range opts { 68 | opt(s) 69 | } 70 | 71 | m := http.NewServeMux() 72 | h := handler(s.authToken, s.logger) 73 | if s.authToken != "" { 74 | h = authHandler(s.authToken, s.logger) 75 | } 76 | m.Handle(s.prefix, http.StripPrefix(s.prefix, h)) 77 | s.serv = &http.Server{ 78 | Handler: m, 79 | } 80 | 81 | return s, s.Start() 82 | } 83 | 84 | func (s *Server) Start() error { 85 | l, err := net.Listen("tcp", s.addr) 86 | if err != nil { 87 | return errors.Wrap(err, "opening socket") 88 | } 89 | 90 | go func() { 91 | defer l.Close() 92 | if err := s.serv.Serve(l); err != nil && err != http.ErrServerClosed { 93 | level.Info(s.logger).Log("msg", "debug server failed", "err", err) 94 | } 95 | }() 96 | 97 | url := url.URL{ 98 | Scheme: "http", 99 | Host: l.Addr().String(), 100 | Path: s.prefix, 101 | RawQuery: "token=" + s.authToken, 102 | } 103 | addr := url.String() 104 | level.Info(s.logger).Log( 105 | "msg", "debug server started", 106 | "addr", addr, 107 | ) 108 | 109 | return nil 110 | } 111 | 112 | // Shutdown stops the running debug server. 113 | func (s *Server) Shutdown() error { 114 | err := s.serv.Shutdown(context.Background()) 115 | return errors.Wrap(err, "shutting down server") 116 | } 117 | 118 | // The below handler code is adapted from MIT licensed github.com/e-dard/netbug 119 | func handler(token string, logger log.Logger) http.HandlerFunc { 120 | info := struct { 121 | Profiles []*pprof.Profile 122 | Token string 123 | }{ 124 | Profiles: pprof.Profiles(), 125 | Token: url.QueryEscape(token), 126 | } 127 | 128 | return func(w http.ResponseWriter, r *http.Request) { 129 | name := strings.TrimPrefix(r.URL.Path, "/") 130 | switch name { 131 | case "": 132 | // Index page. 133 | if err := indexTmpl.Execute(w, info); err != nil { 134 | level.Info(logger).Log( 135 | "msg", "error rendering debug template", 136 | "err", err, 137 | ) 138 | return 139 | } 140 | case "cmdline": 141 | nhpprof.Cmdline(w, r) 142 | case "profile": 143 | nhpprof.Profile(w, r) 144 | case "trace": 145 | nhpprof.Trace(w, r) 146 | case "symbol": 147 | nhpprof.Symbol(w, r) 148 | default: 149 | // Provides access to all profiles under runtime/pprof 150 | nhpprof.Handler(name).ServeHTTP(w, r) 151 | } 152 | } 153 | } 154 | 155 | // authHandler wraps the basic handler, checking the auth token. 156 | func authHandler(token string, logger log.Logger) http.HandlerFunc { 157 | return func(w http.ResponseWriter, r *http.Request) { 158 | if r.FormValue("token") == token { 159 | handler(token, logger).ServeHTTP(w, r) 160 | } else { 161 | http.Error(w, "Request must include valid token.", http.StatusUnauthorized) 162 | } 163 | } 164 | } 165 | 166 | var indexTmpl = template.Must(template.New("index").Parse(` 167 |
168 || {{.Count}} | {{.Name}} 176 | {{end}} 177 | |
| CPU 178 | | |
| 5-second trace 179 | | |
| 30-second trace 180 | |
| cmdline 185 | | |
| symbol 186 | | |
| full goroutine stack dump 187 | |