diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..38f5147 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: CI +on: + push: + branches: + - main + - "v*" + tags: + - "v*" + pull_request: + +jobs: + lint: + name: Go Lint + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.x + + - name: Install Staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@2023.1.3 + + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Lint Go Code + run: staticcheck ./... + + test: + name: Go Test + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + go-version: [1.20.x, 1.21.x] + env: + GOPATH: ${{ github.workspace }}/go + GOBIN: ${{ github.workspace }}/go/bin + GOTEST_GITHUB_ACTIONS: 1 + defaults: + run: + working-directory: ${{ env.GOPATH }}/src/github.com/trisacrypto/courier + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Speedup + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Checkout Code + uses: actions/checkout@v3 + with: + path: ${{ env.GOPATH }}/src/github.com/trisacrypto/courier + + - name: Run Unit Tests + run: go test -v -coverprofile=coverage.txt -covermode=atomic --race ./... \ No newline at end of file diff --git a/cmd/courier/main.go b/cmd/courier/main.go new file mode 100644 index 0000000..6234fd8 --- /dev/null +++ b/cmd/courier/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + courier "github.com/trisacrypto/courier/pkg" + "github.com/trisacrypto/courier/pkg/api/v1" + "github.com/trisacrypto/courier/pkg/config" + "github.com/urfave/cli/v2" +) + +func main() { + // Load the dotenv file if it exists + godotenv.Load() + + // Create the CLI application + app := &cli.App{ + Name: "courier", + Version: courier.Version(), + Usage: "a standalone certificate delivery service", + Flags: []cli.Flag{}, + Commands: []*cli.Command{ + { + Name: "serve", + Usage: "run the courier server", + Category: "server", + Action: serve, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "addr", + Aliases: []string{"a"}, + Usage: "address:port to bind the server on", + EnvVars: []string{"COURIER_BIND_ADDR"}, + Required: true, + }, + }, + }, + { + Name: "status", + Usage: "get the status of the courier server", + Category: "client", + Action: status, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u", "endpoint"}, + Usage: "url to connect to the courier server", + EnvVars: []string{"COURIER_CLIENT_URL"}, + Required: true, + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} + +//=========================================================================== +// CLI Actions +//=========================================================================== + +// Serve the courier service. +func serve(c *cli.Context) (err error) { + var conf config.Config + if conf, err = config.New(); err != nil { + return cli.Exit(err, 1) + } + + if addr := c.String("addr"); addr != "" { + conf.BindAddr = addr + } + + var srv *courier.Server + if srv, err = courier.New(conf); err != nil { + return cli.Exit(err, 1) + } + + if err = srv.Serve(); err != nil { + return cli.Exit(err, 1) + } + + return nil +} + +// Get the status of the courier service. +func status(c *cli.Context) (err error) { + var client api.CourierClient + if client, err = api.New(c.String("url")); err != nil { + return cli.Exit(err, 1) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var rep *api.StatusReply + if rep, err = client.Status(ctx); err != nil { + return cli.Exit(err, 1) + } + + return printJSON(rep) +} + +//=========================================================================== +// Helpers +//=========================================================================== + +// Print an object as encoded JSON to stdout. +func printJSON(v interface{}) (err error) { + var data []byte + if data, err = json.MarshalIndent(v, "", " "); err != nil { + return err + } + + fmt.Println(string(data)) + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da827e9 --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module github.com/trisacrypto/courier + +go 1.20 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/joho/godotenv v1.4.0 + github.com/rotationalio/confire v1.0.0 + github.com/rs/zerolog v1.31.0 + github.com/stretchr/testify v1.8.4 + github.com/trisacrypto/trisa v0.99999.1 + github.com/urfave/cli/v2 v2.15.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..62b5a94 --- /dev/null +++ b/go.sum @@ -0,0 +1,120 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rotationalio/confire v1.0.0 h1:Ex1jtwVyvuMhFY0EXfgbMsvd9MPO5V9LvJZ0q740M9k= +github.com/rotationalio/confire v1.0.0/go.mod h1:ug7pBDiZZl/4JjXJ2Effmj+L+0T2DBbG+Us1qQcRex0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/trisacrypto/trisa v0.99999.1 h1:v9GrlFEWA/vrOotyLWUpcN9BWIQ242WR2gD98QnmAz8= +github.com/trisacrypto/trisa v0.99999.1/go.mod h1:frXzP50dxWq2rL6fiYjKBXMhRHbmGpW0pNhiBTavIjc= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.15.0 h1:/U7qTMlBYcmo/Z34PaaVY0Gw04xoGJqEdRAiWNHNyy8= +github.com/urfave/cli/v2 v2.15.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/pkg/api/v1/api.go b/pkg/api/v1/api.go new file mode 100644 index 0000000..4c11878 --- /dev/null +++ b/pkg/api/v1/api.go @@ -0,0 +1,19 @@ +package api + +import "context" + +type CourierClient interface { + Status(context.Context) (*StatusReply, error) +} + +// Reply encodes generic JSON responses from the API. +type Reply struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +type StatusReply struct { + Status string `json:"status"` + Uptime string `json:"uptime,omitempty"` + Version string `json:"version,omitempty"` +} diff --git a/pkg/api/v1/client.go b/pkg/api/v1/client.go new file mode 100644 index 0000000..1688b4a --- /dev/null +++ b/pkg/api/v1/client.go @@ -0,0 +1,128 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "time" +) + +// New creates a new API client that implements the CourierClient interface. +func New(endpoint string, opts ...ClientOption) (_ CourierClient, err error) { + if endpoint == "" { + return nil, ErrEndpointRequired + } + + // Create a client with the parsed endpoint. + c := &APIv1{} + if c.url, err = url.Parse(endpoint); err != nil { + return nil, err + } + + // Apply options + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } + } + + // If a client hasn't been specified, create the default client. + if c.client == nil { + c.client = &http.Client{ + Transport: nil, + CheckRedirect: nil, + Timeout: 30 * time.Second, + } + } + return c, nil +} + +// APIv1 implements the CourierClient interface. +type APIv1 struct { + url *url.URL + client *http.Client +} + +var _ CourierClient = &APIv1{} + +//=========================================================================== +// Client Methods +//=========================================================================== + +// Status returns the status of the courier service. +func (c *APIv1) Status(ctx context.Context) (out *StatusReply, err error) { + // Create the HTTP request + var req *http.Request + if req, err = c.NewRequest(ctx, http.MethodGet, "/v1/status", nil, nil); err != nil { + return nil, err + } + + // Do the request + var rep *http.Response + if rep, err = c.client.Do(req); err != nil { + return nil, err + } + defer rep.Body.Close() + + // Catch status errors + if rep.StatusCode != http.StatusOK && rep.StatusCode != http.StatusServiceUnavailable { + return nil, NewStatusError(rep.StatusCode, rep.Status) + } + + // Decode the response + out = &StatusReply{} + if err = json.NewDecoder(rep.Body).Decode(out); err != nil { + return nil, err + } + return out, nil +} + +//=========================================================================== +// Client Helpers +//=========================================================================== + +const ( + userAgent = "Courier API Client/v1" + accept = "application/json" + acceptLang = "en-US,en" + acceptEncode = "gzip, deflate, br" + contentType = "application/json; charset=utf-8" +) + +// NewRequest creates an http.Request with the specified context and method, resolving +// the path to the root endpoint of the API (e.g. /v1) and serializes the data to JSON. +func (c *APIv1) NewRequest(ctx context.Context, method, path string, data interface{}, params *url.Values) (req *http.Request, err error) { + // Resolve the URL reference from the path + endpoint := c.url.ResolveReference(&url.URL{Path: path}) + if params != nil && len(*params) > 0 { + endpoint.RawQuery = params.Encode() + } + + var body io.ReadWriter + switch { + case data == nil: + body = nil + default: + body = &bytes.Buffer{} + if err = json.NewEncoder(body).Encode(data); err != nil { + return nil, err + } + } + + // Create the http request + if req, err = http.NewRequestWithContext(ctx, method, endpoint.String(), body); err != nil { + return nil, err + } + + // Set the headers on the request + req.Header.Add("User-Agent", userAgent) + req.Header.Add("Accept", accept) + req.Header.Add("Accept-Language", acceptLang) + req.Header.Add("Accept-Encoding", acceptEncode) + req.Header.Add("Content-Type", contentType) + + return req, nil +} diff --git a/pkg/api/v1/errors.go b/pkg/api/v1/errors.go new file mode 100644 index 0000000..0c23966 --- /dev/null +++ b/pkg/api/v1/errors.go @@ -0,0 +1,38 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +var ( + notFound = Reply{Success: false, Error: "resource not found"} + notAllowed = Reply{Success: false, Error: "method not allowed"} + ErrEndpointRequired = errors.New("endpoint is required") +) + +func NewStatusError(code int, err string) error { + return StatusError{Code: code, Err: err} +} + +type StatusError struct { + Code int + Err string +} + +func (e StatusError) Error() string { + return fmt.Sprintf("[%d]: %s", e.Code, e.Err) +} + +// NotFound returns a standard 404 response. +func NotFound(c *gin.Context) { + c.JSON(http.StatusNotFound, notFound) +} + +// MethodNotAllowed returns a standard 405 response. +func MethodNotAllowed(c *gin.Context) { + c.JSON(http.StatusMethodNotAllowed, notAllowed) +} diff --git a/pkg/api/v1/options.go b/pkg/api/v1/options.go new file mode 100644 index 0000000..05fe69d --- /dev/null +++ b/pkg/api/v1/options.go @@ -0,0 +1,4 @@ +package api + +// ClientOption allows the API client to be configured when it is created. +type ClientOption func(c *APIv1) error diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..7c56158 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,159 @@ +package config + +import ( + "crypto/tls" + "crypto/x509" + + "github.com/rotationalio/confire" + "github.com/trisacrypto/trisa/pkg/trust" +) + +type Config struct { + BindAddr string `split_words:"true" default:":8842"` + Mode string `split_words:"true" default:"release"` + MTLS MTLSConfig `split_words:"true"` + processed bool +} + +type MTLSConfig struct { + Insecure bool `split_words:"true" default:"true"` + CertPath string `split_words:"true"` + PoolPath string `split_words:"true"` + pool *x509.CertPool + cert tls.Certificate +} + +// Create a new Config struct using values from the environment prefixed with COURIER. +func New() (conf Config, err error) { + if err = confire.Process("courier", &conf); err != nil { + return conf, err + } + + conf.processed = true + return conf, nil +} + +// Return true if the configuration has not been processed (e.g. not loaded from the +// environment or configuration file). +func (c Config) IsZero() bool { + return !c.processed +} + +// Mark a configuration as processed, for cases where the configuration is manually +// created (e.g. in tests). +func (c Config) Mark() (Config, error) { + if err := c.Validate(); err != nil { + return c, err + } + c.processed = true + return c, nil +} + +// Validate the configuration. +func (c Config) Validate() (err error) { + if c.BindAddr == "" { + return ErrMissingBindAddr + } + + if c.Mode == "" { + return ErrMissingServerMode + } + + if err = c.MTLS.Validate(); err != nil { + return err + } + + return nil +} + +func (c *MTLSConfig) Validate() error { + if c.Insecure { + return nil + } + + if c.CertPath == "" || c.PoolPath == "" { + return ErrMissingCertPaths + } + + return nil +} + +func (c *MTLSConfig) ParseTLSConfig() (_ *tls.Config, err error) { + if c.Insecure { + return nil, ErrTLSNotConfigured + } + + var certPool *x509.CertPool + if certPool, err = c.GetCertPool(); err != nil { + return nil, err + } + + var cert tls.Certificate + if cert, err = c.GetCert(); err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{ + tls.CurveP521, + tls.CurveP384, + tls.CurveP256, + }, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + }, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, + }, nil +} + +func (c *MTLSConfig) GetCertPool() (_ *x509.CertPool, err error) { + if c.pool == nil { + if err = c.load(); err != nil { + return nil, err + } + } + return c.pool, nil +} + +func (c *MTLSConfig) GetCert() (_ tls.Certificate, err error) { + if len(c.cert.Certificate) == 0 { + if err = c.load(); err != nil { + return c.cert, err + } + } + return c.cert, nil +} + +func (c *MTLSConfig) load() (err error) { + var sz *trust.Serializer + if sz, err = trust.NewSerializer(false); err != nil { + return err + } + + var pool trust.ProviderPool + if pool, err = sz.ReadPoolFile(c.PoolPath); err != nil { + return err + } + + var provider *trust.Provider + if provider, err = sz.ReadFile(c.CertPath); err != nil { + return err + } + + if c.pool, err = pool.GetCertPool(false); err != nil { + return err + } + + if c.cert, err = provider.GetKeyPair(); err != nil { + return err + } + + return nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..39dc4f6 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,134 @@ +package config_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/trisacrypto/courier/pkg/config" +) + +// Define a test environment for the config tests. +var testEnv = map[string]string{ + "COURIER_BIND_ADDR": ":8080", + "COURIER_MODE": "debug", + "COURIER_MTLS_INSECURE": "false", + "COURIER_MTLS_CERT_PATH": "/path/to/cert", + "COURIER_MTLS_POOL_PATH": "/path/to/pool", +} + +func TestConfig(t *testing.T) { + // Set required environment variables + prevEnv := curEnv() + t.Cleanup(func() { + for key, val := range prevEnv { + if val != "" { + os.Setenv(key, val) + } else { + os.Unsetenv(key) + } + } + }) + setEnv() + + conf, err := config.New() + require.NoError(t, err, "could not create config from test environment") + require.False(t, conf.IsZero(), "config should be processed") + + require.Equal(t, testEnv["COURIER_BIND_ADDR"], conf.BindAddr) + require.Equal(t, testEnv["COURIER_MODE"], conf.Mode) + require.False(t, conf.MTLS.Insecure) + require.Equal(t, testEnv["COURIER_MTLS_CERT_PATH"], conf.MTLS.CertPath) + require.Equal(t, testEnv["COURIER_MTLS_POOL_PATH"], conf.MTLS.PoolPath) +} + +func TestValidate(t *testing.T) { + t.Run("ValidInsecure", func(t *testing.T) { + conf := config.Config{ + BindAddr: ":8080", + Mode: "debug", + MTLS: config.MTLSConfig{ + Insecure: true, + }, + } + require.NoError(t, conf.Validate(), "insecure config should be valid") + }) + + t.Run("ValidSecure", func(t *testing.T) { + conf := config.Config{ + BindAddr: ":8080", + Mode: "debug", + MTLS: config.MTLSConfig{ + CertPath: "/path/to/cert", + PoolPath: "/path/to/pool", + }, + } + require.NoError(t, conf.Validate(), "secure config should be valid") + }) + + t.Run("MissingBindAddr", func(t *testing.T) { + conf := config.Config{ + Mode: "debug", + MTLS: config.MTLSConfig{ + Insecure: true, + }, + } + require.ErrorIs(t, conf.Validate(), config.ErrMissingBindAddr, "config should be invalid") + }) + + t.Run("MissingServerMode", func(t *testing.T) { + conf := config.Config{ + BindAddr: ":8080", + MTLS: config.MTLSConfig{ + Insecure: true, + }, + } + require.ErrorIs(t, conf.Validate(), config.ErrMissingServerMode, "config should be invalid") + }) + + t.Run("MissingCertPaths", func(t *testing.T) { + conf := config.Config{ + BindAddr: ":8080", + Mode: "debug", + MTLS: config.MTLSConfig{ + Insecure: false, + }, + } + require.ErrorIs(t, conf.Validate(), config.ErrMissingCertPaths, "config should be invalid") + }) +} + +// Returns the current environment for the specified keys, or if no keys are specified +// then returns the current environment for all keys in testEnv. +func curEnv(keys ...string) map[string]string { + env := make(map[string]string) + if len(keys) > 0 { + for _, envvar := range keys { + if val, ok := os.LookupEnv(envvar); ok { + env[envvar] = val + } + } + } else { + for key := range testEnv { + env[key] = os.Getenv(key) + } + } + + return env +} + +// Sets the environment variable from the testEnv, if no keys are specified, then sets +// all environment variables from the test env. +func setEnv(keys ...string) { + if len(keys) > 0 { + for _, key := range keys { + if val, ok := testEnv[key]; ok { + os.Setenv(key, val) + } + } + } else { + for key, val := range testEnv { + os.Setenv(key, val) + } + } +} diff --git a/pkg/config/errors.go b/pkg/config/errors.go new file mode 100644 index 0000000..698c3a3 --- /dev/null +++ b/pkg/config/errors.go @@ -0,0 +1,10 @@ +package config + +import "errors" + +var ( + ErrMissingBindAddr = errors.New("invalid configuration: missing bindaddr") + ErrMissingServerMode = errors.New("invalid configuration: missing server mode (debug, release, test)") + ErrMissingCertPaths = errors.New("invalid configuration: missing cert path or pool path") + ErrTLSNotConfigured = errors.New("cannot create TLS configuration in insecure mode") +) diff --git a/pkg/server.go b/pkg/server.go new file mode 100644 index 0000000..dc73ed8 --- /dev/null +++ b/pkg/server.go @@ -0,0 +1,185 @@ +package courier + +import ( + "context" + "net" + "net/http" + "os" + "os/signal" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + "github.com/trisacrypto/courier/pkg/api/v1" + "github.com/trisacrypto/courier/pkg/config" +) + +// New creates a new server object from configuration but does not serve it yet. +func New(conf config.Config) (s *Server, err error) { + // Load config from environment if it's empty + if conf.IsZero() { + if conf, err = config.New(); err != nil { + return nil, err + } + } + + // Create the server object + s = &Server{ + conf: conf, + echan: make(chan error, 1), + } + + // TODO: Initialize the configured stores + + // Create the router + gin.SetMode(conf.Mode) + s.router = gin.New() + if err = s.setupRoutes(); err != nil { + return nil, err + } + + // Create the http server + s.srv = &http.Server{ + Addr: conf.BindAddr, + Handler: s.router, + } + + // Use TLS if configured + if !conf.MTLS.Insecure { + if s.srv.TLSConfig, err = conf.MTLS.ParseTLSConfig(); err != nil { + return nil, err + } + } + + return s, nil +} + +// Server defines the courier service and its webhook handlers. +type Server struct { + sync.RWMutex + conf config.Config + srv *http.Server + router *gin.Engine + started time.Time + healthy bool + url string + echan chan error +} + +// Serve API requests. +func (s *Server) Serve() (err error) { + // Catch OS signals for graceful shutdowns + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + go func() { + <-quit + s.echan <- s.Shutdown() + }() + + // Set healthy status + s.SetHealthy(true) + + // Create the listen socket + var sock net.Listener + if sock, err = net.Listen("tcp", s.conf.BindAddr); err != nil { + return err + } + + // Set the URL from the socket + s.SetURL(sock) + s.started = time.Now() + + // Serve the API + go func() { + if err = s.srv.Serve(sock); err != nil && err != http.ErrServerClosed { + s.echan <- err + } + }() + + log.Info().Str("listen", s.url).Str("version", Version()).Msg("courier server started") + + // Wait for shutdown or an error + if err = <-s.echan; err != nil { + return err + } + return nil +} + +// Shutdown the server gracefully. +func (s *Server) Shutdown() (err error) { + log.Info().Msg("gracefully shutting down courier server") + + s.SetHealthy(false) + s.srv.SetKeepAlivesEnabled(false) + + // Ensure shutdown happens within 30 seconds + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err = s.srv.Shutdown(ctx); err != nil { + return err + } + + // TODO: Close the stores + + log.Debug().Msg("successfully shut down courier server") + return nil +} + +// Setup the routes for the courier service. +func (s *Server) setupRoutes() (err error) { + middlewares := []gin.HandlerFunc{ + gin.Logger(), + gin.Recovery(), + s.Available(), + } + + // Add the middlewares to the router + s.router.Use(middlewares...) + + // API routes + v1 := s.router.Group("/v1") + { + // Status route + v1.GET("/status", s.Status) + + // TODO: Password and certificate routes + } + + // Not found and method not allowed routes + s.router.NoRoute(api.NotFound) + s.router.NoMethod(api.MethodNotAllowed) + + return nil +} + +// Set the healthy status of the server. +func (s *Server) SetHealthy(healthy bool) { + s.Lock() + s.healthy = healthy + s.Unlock() +} + +// Set the URL of the server from the socket +func (s *Server) SetURL(sock net.Listener) { + s.Lock() + sockAddr := sock.Addr().String() + if s.conf.MTLS.Insecure { + s.url = "http://" + sockAddr + } else { + s.url = "https://" + sockAddr + } + s.Unlock() +} + +//=========================================================================== +// Helpers for testing +//=========================================================================== + +// URL returns the URL of the server. +func (s *Server) URL() string { + s.RLock() + defer s.RUnlock() + return s.url +} diff --git a/pkg/server_test.go b/pkg/server_test.go new file mode 100644 index 0000000..f4b2368 --- /dev/null +++ b/pkg/server_test.go @@ -0,0 +1,58 @@ +package courier_test + +import ( + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + courier "github.com/trisacrypto/courier/pkg" + "github.com/trisacrypto/courier/pkg/api/v1" + "github.com/trisacrypto/courier/pkg/config" +) + +// The courier test suite allows us to test the courier API by making actual requests +// to an in-memory server. +type courierTestSuite struct { + suite.Suite + courier *courier.Server + client api.CourierClient +} + +func (s *courierTestSuite) SetupSuite() { + require := s.Require() + + // Configuration to start a fully functional server for localhost testing. + conf, err := config.Config{ + BindAddr: "127.0.0.1:0", + Mode: gin.TestMode, + MTLS: config.MTLSConfig{ + Insecure: true, + }, + }.Mark() + require.NoError(err, "could not create test configuration") + + // Create the server + s.courier, err = courier.New(conf) + require.NoError(err, "could not create test server") + + // Start the server, which will run for the duration of the test suite + go s.courier.Serve() + + // Wait for the server to start serving the API + time.Sleep(500 * time.Millisecond) + + // Create an API client to use in tests + url := s.courier.URL() + s.client, err = api.New(url) + require.NoError(err, "could not create test client") +} + +func (s *courierTestSuite) TearDownSuite() { + require := s.Require() + require.NoError(s.courier.Shutdown(), "could not shutdown test server in suite teardown") +} + +func TestCourier(t *testing.T) { + suite.Run(t, new(courierTestSuite)) +} diff --git a/pkg/status.go b/pkg/status.go new file mode 100644 index 0000000..b131e4d --- /dev/null +++ b/pkg/status.go @@ -0,0 +1,50 @@ +package courier + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/trisacrypto/courier/pkg/api/v1" +) + +const ( + serverStatusOK = "ok" + serverStatusStopping = "stopping" +) + +// Status returns the status of the server. +func (s *Server) Status(c *gin.Context) { + // At this point the status is always OK, the available middleware will handle the + // stopping status. + out := &api.StatusReply{ + Status: serverStatusOK, + Version: Version(), + Uptime: time.Since(s.started).String(), + } + + c.JSON(http.StatusOK, out) +} + +// Available is middleware that uses the healthy boolean to return a service unavailable +// http status code if the server is shutting down. This middleware must be first in the +// chain to ensure that complex handling to slow the shutdown of the server. +func (s *Server) Available() gin.HandlerFunc { + return func(c *gin.Context) { + // Check health status + s.RLock() + if !s.healthy { + c.JSON(http.StatusServiceUnavailable, api.StatusReply{ + Status: serverStatusStopping, + Uptime: time.Since(s.started).String(), + Version: Version(), + }) + + c.Abort() + s.RUnlock() + return + } + s.RUnlock() + c.Next() + } +} diff --git a/pkg/status_test.go b/pkg/status_test.go new file mode 100644 index 0000000..3bd5e44 --- /dev/null +++ b/pkg/status_test.go @@ -0,0 +1,16 @@ +package courier_test + +import "context" + +func (s *courierTestSuite) TestStatus() { + require := s.Require() + + // Make a request to the status endpoint + status, err := s.client.Status(context.Background()) + require.NoError(err, "could not get status from server") + + // Check that the status is as expected + require.Equal("ok", status.Status, "status should be ok") + require.NotEmpty(status.Uptime, "uptime missing from response") + require.NotEmpty(status.Version, "version missing from response") +} diff --git a/pkg/version.go b/pkg/version.go new file mode 100644 index 0000000..98d3bbb --- /dev/null +++ b/pkg/version.go @@ -0,0 +1,39 @@ +package courier + +import "fmt" + +// Version of the current build +const ( + VersionMajor = 0 + VersionMinor = 1 + VersionPatch = 0 + VersionReleaseLevel = "beta" + VersionReleaseNumber = 1 +) + +// Set the GitVersion via -ldflags="-X 'github.com/rotationalio/ensign/pkg.GitVersion=$(git rev-parse --short HEAD)'" +var GitVersion string + +// Returns the semantic version for the current build. +func Version() string { + var versionCore string + if VersionPatch > 0 || VersionReleaseLevel != "" { + versionCore = fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch) + } else { + versionCore = fmt.Sprintf("%d.%d", VersionMajor, VersionMinor) + } + + if VersionReleaseLevel != "" { + if VersionReleaseNumber > 0 { + versionCore = fmt.Sprintf("%s-%s.%d", versionCore, VersionReleaseLevel, VersionReleaseNumber) + } else { + versionCore = fmt.Sprintf("%s-%s", versionCore, VersionReleaseLevel) + } + } + + if GitVersion != "" { + versionCore = fmt.Sprintf("%s (%s)", versionCore, GitVersion) + } + + return versionCore +}