diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cd88554 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d07d102 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,81 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '17 21 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore index b8d3fd4..c943d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ Thumbs.db .Trashes # Compilation/Build output folder +build/bin/ output/ # Jetbrains Configuration files diff --git a/README.md b/README.md index 4546b28..b3e2abb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # go-kit -CoreLayer Standard Library + +Standard Go Toolset for applications diff --git a/build/package/docker/Dockerfile b/build/package/docker/Dockerfile new file mode 100644 index 0000000..6378d60 --- /dev/null +++ b/build/package/docker/Dockerfile @@ -0,0 +1,55 @@ +##################################### +### Step 1: Build the application ### +##################################### +FROM golang:alpine AS builder + +# Install git +RUN apk update && apk add --no-cache git + +# SECURITY - scratch image does not come with ca-certificates / timezone data +# Install ca-certificates to allow SSL services +RUN apk add --no-cache ca-certificates tzdata && update-ca-certificates +# +# SECURITY - NEVER RUN A PROCESS AS ROOT IN A CONTAINER +# Create user for application +ENV USER=appuser +ENV UID=10001 + +# See https://stackoverflow.com/questions/49955097/how-do-i-add-a-user-when-im-using-alpine-as-a-base-image/55757473#55757473 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + "${USER}" + +# Set the working directory inside builder +WORKDIR $GOPATH/src/go-kit/app/ +COPY . . +RUN go mod tidy +RUN GOOS=linux \ + GOARCH=arm64 \ + go build \ + -ldflags="-w -s -X main.VersionCommit=$(git rev-list --abbrev-commit --abbrev=8 -1 HEAD) -X main.VersionSemVer=$(git name-rev HEAD --name-only)" \ + -o /go/bin/app \ + examples/app/main.go + +################################### +### Step 1: Build a small image ### +################################### + +FROM scratch +# Import the user and group files from the builder +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group + +# Import the executable +COPY --from=builder /go/bin/app /go/bin/app + +# Use an unprivileged user +USER appuser:appuser +ENTRYPOINT ["/go/bin/app", "version"] \ No newline at end of file diff --git a/examples/app/README.md b/examples/app/README.md new file mode 100644 index 0000000..8bea35c --- /dev/null +++ b/examples/app/README.md @@ -0,0 +1,10 @@ +# Run example +```shell +go run -ldflags="-X main.VersionCommit=$(git rev-list --abbrev-commit --abbrev=8 -1 HEAD) -X main.VersionSemVer=$(git name-rev HEAD --name-only)" examples/app/main.go version +``` + +# Build example + +```shell +go build -o build/bin/app -ldflags="-X main.VersionCommit=$(git rev-list --abbrev-commit --abbrev=8 -1 HEAD) -X main.VersionSemVer=$(git name-rev HEAD --name-only)" examples/app/main.go +``` \ No newline at end of file diff --git a/examples/app/main.go b/examples/app/main.go new file mode 100644 index 0000000..56ae9c7 --- /dev/null +++ b/examples/app/main.go @@ -0,0 +1,65 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/corelayer/go-kit/pkg/application" +) + +var ( + VersionSemVer string + VersionCommit string + VersionDate string +) + +func PrintVersion(v *application.Version) error { + fmt.Println("Full version:", v.String()) + fmt.Println("Version:", v.SemVer) + fmt.Println("Commit:", v.Commit) + fmt.Println("Date:", v.Date) + return nil +} + +func main() { + var err error + root := &cobra.Command{ + Use: "app", + Short: "App Short", + Long: "App Long", + Args: cobra.MinimumNArgs(1), + } + + version := application.Version{ + SemVer: VersionSemVer, + Commit: VersionCommit, + Date: VersionDate, + RunE: PrintVersion, + } + + app := application.NewApplication(root, version) + if err = app.RegisterEnvironment("EXAMPLE", []string{"KEY"}); err != nil { + panic(err) + } + + if err = app.Run(); err != nil { + fmt.Println(err) + } +} diff --git a/go.mod b/go.mod index 6b65ce6..a219ff9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,30 @@ module github.com/corelayer/go-kit go 1.22.1 + +require ( + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..73e11e9 --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +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= diff --git a/pkg/application/application.go b/pkg/application/application.go new file mode 100644 index 0000000..d4b34c2 --- /dev/null +++ b/pkg/application/application.go @@ -0,0 +1,54 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import ( + "github.com/spf13/cobra" +) + +func NewApplication(c *cobra.Command, v Version) *Application { + if v.RunE != nil { + c.AddCommand(v.Command()) + c.Version = "" + } else { + c.Version = v.String() + } + + return &Application{ + Root: c, + Version: v, + } +} + +type Application struct { + Root *cobra.Command + Version Version +} + +func (a *Application) RegisterCommands(c []Commander, f func(cmd *cobra.Command)) { + for _, cmdr := range c { + a.Root.AddCommand(cmdr.Initialize(f)) + } +} + +func (a *Application) Run() error { + return a.Root.Execute() +} + +func init() { + Config = NewConfiguration() +} diff --git a/pkg/application/command.go b/pkg/application/command.go new file mode 100644 index 0000000..37e81dc --- /dev/null +++ b/pkg/application/command.go @@ -0,0 +1,42 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import ( + "github.com/spf13/cobra" +) + +type Command struct { + Root *cobra.Command + SubCommands []Commander + Configure func(cmd *cobra.Command) +} + +func (c Command) Initialize(f func(cmd *cobra.Command)) *cobra.Command { + if f != nil { + f(c.Root) + } + + if c.Configure != nil { + c.Configure(c.Root) + } + + for _, subCmd := range c.SubCommands { + c.Root.AddCommand(subCmd.Initialize(f)) + } + return c.Root +} diff --git a/pkg/application/commander.go b/pkg/application/commander.go new file mode 100644 index 0000000..1007939 --- /dev/null +++ b/pkg/application/commander.go @@ -0,0 +1,23 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import "github.com/spf13/cobra" + +type Commander interface { + Initialize(f func(command *cobra.Command)) *cobra.Command +} diff --git a/pkg/application/configuration.go b/pkg/application/configuration.go new file mode 100644 index 0000000..93eec45 --- /dev/null +++ b/pkg/application/configuration.go @@ -0,0 +1,256 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import ( + "path/filepath" + "strings" + "sync" + + "github.com/spf13/viper" +) + +var Config *Configuration + +func RegisterEnvironment(name string, prefix string, keys []string) error { + var ( + err error + eConfig EnvConfiguration + eViper *viper.Viper + ) + + if eConfig, err = NewEnvConfiguration(prefix, keys); err != nil { + return err + } + + if eViper, err = eConfig.GetViper(); err != nil { + return err + } + return Config.SetEnvironment(name, eViper) +} + +func RegisterConfiguration(name string, filename string, searchPaths []string) error { + var ( + err error + fViper *viper.Viper + ) + if Config.FileExists(name) { + return ErrFileConfigurationExists + } + c := NewFileConfiguration(filename, searchPaths) + if fViper, err = c.GetViper(); err != nil { + return err + } + return Config.SetFile(name, fViper) +} + +type ConfigFileFlags struct { + Name string + Paths []string +} + +type ConfigFileParams struct { + File StringVar + Paths StringSliceVar +} + +func NewConfiguration() *Configuration { + return &Configuration{ + files: make(map[string]*viper.Viper), + mux: sync.Mutex{}, + } +} + +type Configuration struct { + env map[string]*viper.Viper + files map[string]*viper.Viper + mux sync.Mutex +} + +func (c *Configuration) GetEnv(name string) (*viper.Viper, error) { + if !c.EnvExists(name) { + return nil, ErrEnvConfigurationNotLoaded + } + c.mux.Lock() + defer c.mux.Unlock() + return c.env[name], nil +} + +func (c *Configuration) GetFile(name string) (*viper.Viper, error) { + if !c.FileExists(name) { + return nil, ErrFileConfigurationNotFound + } + c.mux.Lock() + defer c.mux.Unlock() + return c.files[name], nil +} + +func (c *Configuration) SetEnvironment(name string, v *viper.Viper) error { + if c.EnvExists(name) { + return ErrEnvConfigurationAlreadyLoaded + } + + c.mux.Lock() + defer c.mux.Unlock() + c.env[name] = v + return nil +} + +func (c *Configuration) SetFile(name string, v *viper.Viper) error { + if c.FileExists(name) { + return ErrFileConfigurationExists + } + c.mux.Lock() + defer c.mux.Unlock() + + c.files[name] = v + return v.ReadInConfig() +} + +func (c *Configuration) EnvExists(name string) bool { + c.mux.Lock() + defer c.mux.Unlock() + _, found := c.env[name] + return found +} + +func (c *Configuration) FileExists(name string) bool { + c.mux.Lock() + defer c.mux.Unlock() + _, found := c.files[name] + return found +} + +func NewEnvConfiguration(prefix string, keys []string) (EnvConfiguration, error) { + if len(keys) == 0 { + return EnvConfiguration{}, ErrEnvConfigurationIsEmpty + } + return EnvConfiguration{ + prefix: prefix, + keys: keys, + }, nil +} + +type EnvConfiguration struct { + prefix string + keys []string +} + +func (e EnvConfiguration) GetViper() (*viper.Viper, error) { + var ( + err error + v *viper.Viper + ) + + if !e.HasKeys() { + return nil, ErrEnvConfigurationIsEmpty + } + + v = viper.New() + v.SetEnvPrefix(e.prefix) + + for _, k := range e.keys { + err = v.BindEnv(k) + if err != nil { + return nil, err + } + } + return v, nil +} + +func (e EnvConfiguration) HasKeys() bool { + if len(e.keys) == 0 { + return false + } + return true +} + +func (e EnvConfiguration) Keys() []string { + return e.keys +} + +func (e EnvConfiguration) Prefix() string { + return e.prefix +} + +func NewFileConfiguration(file string, searchPaths []string) FileConfiguration { + // Clean search paths + paths := make([]string, 0) + for _, p := range searchPaths { + if !strings.Contains(p, "..") { + paths = append(paths, filepath.Clean(p)) + } + } + + // Split file into path and filename + path, filename := filepath.Split(file) + + // Make sure to clean path, this also causes the path to either be "." or a full path + if path != "" { + path = filepath.Clean(path) + } + c := FileConfiguration{ + filename: filename, + path: path, + paths: paths, + } + + return c +} + +type FileConfiguration struct { + filename string + path string + paths []string +} + +func (c FileConfiguration) GetViper() (*viper.Viper, error) { + v := viper.New() + + // If a full path is specified, set the config file to that path + if c.path != "" { + fullPath := filepath.Join(c.path, c.filename) + v.SetConfigFile(fullPath) + } else { + configName, configType := c.getViperConfig() + v.SetConfigName(configName) + v.SetConfigType(configType) + + for _, path := range c.paths { + v.AddConfigPath(path) + } + } + return v, v.ReadInConfig() +} + +func (c FileConfiguration) getViperConfig() (string, string) { + var ( + configName string + configType string + ) + + fileExtension := filepath.Ext(c.filename) + if fileExtension == "" { + configType = "yaml" + } else { + configType = strings.TrimPrefix(fileExtension, ".") + } + + configName = strings.TrimSuffix(c.filename, fileExtension) + + return configName, configType +} diff --git a/pkg/application/errors.go b/pkg/application/errors.go new file mode 100644 index 0000000..85afc88 --- /dev/null +++ b/pkg/application/errors.go @@ -0,0 +1,73 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +const ( + ErrEnvConfigurationAlreadyLoadedMessage = " environment is already loaded" + ErrEnvConfigurationIsEmptyMessage = "environment configuration does not define keys" + ErrEnvConfigurationNotLoadedMessage = "environment not loaded" + ErrFileConfigurationExistsMessage = "configuration exists" + ErrFileConfigurationNotFoundMessage = "configuration not found" +) + +var ( + ErrEnvConfigurationAlreadyLoaded = EnvConfigurationAlreadyLoadedError{message: ErrEnvConfigurationAlreadyLoadedMessage} + ErrEnvConfigurationIsEmpty = EnvConfigurationIsEmptyError{message: ErrEnvConfigurationIsEmptyMessage} + ErrEnvConfigurationNotLoaded = EnvConfigurationNotLoadedError{message: ErrEnvConfigurationNotLoadedMessage} + ErrFileConfigurationExists = FileConfigurationExistsError{message: ErrFileConfigurationExistsMessage} + ErrFileConfigurationNotFound = FileConfigurationNotFoundError{message: ErrFileConfigurationNotFoundMessage} +) + +type EnvConfigurationAlreadyLoadedError struct { + message string +} + +func (e EnvConfigurationAlreadyLoadedError) Error() string { + return e.message +} + +type EnvConfigurationIsEmptyError struct { + message string +} + +func (e EnvConfigurationIsEmptyError) Error() string { + return e.message +} + +type EnvConfigurationNotLoadedError struct { + message string +} + +func (e EnvConfigurationNotLoadedError) Error() string { + return e.message +} + +type FileConfigurationExistsError struct { + message string +} + +func (e FileConfigurationExistsError) Error() string { + return e.message +} + +type FileConfigurationNotFoundError struct { + message string +} + +func (e FileConfigurationNotFoundError) Error() string { + return e.message +} diff --git a/pkg/application/flagParams.go b/pkg/application/flagParams.go new file mode 100644 index 0000000..2ed50a0 --- /dev/null +++ b/pkg/application/flagParams.go @@ -0,0 +1,45 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +type BoolVar struct { + Name string + Shorthand string + Value bool + Usage string +} + +type BoolSliceVar struct { + Name string + Shorthand string + Value []bool + Usage string +} + +type StringVar struct { + Name string + Shorthand string + Value string + Usage string +} + +type StringSliceVar struct { + Name string + Shorthand string + Value []string + Usage string +} diff --git a/pkg/application/flags.go b/pkg/application/flags.go new file mode 100644 index 0000000..4b72c2f --- /dev/null +++ b/pkg/application/flags.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import "github.com/spf13/cobra" + +func AddConfigFileFlag(cmd *cobra.Command, f *ConfigFileFlags, p ConfigFileParams) { + cmd.PersistentFlags().StringVarP(&f.Name, p.File.Name, p.File.Shorthand, p.File.Value, p.File.Usage) +} + +func AddConfigFilePathsFlag(cmd *cobra.Command, f *ConfigFileFlags, p ConfigFileParams) { + cmd.PersistentFlags().StringSliceVarP(&f.Paths, p.Paths.Name, p.Paths.Shorthand, p.Paths.Value, p.Paths.Usage) +} + +func AddLogLevelFlag(cmd *cobra.Command, f *LogFlags, p LogParams) { + cmd.PersistentFlags().StringVarP(&f.Level, p.Level.Name, p.Level.Shorthand, p.Level.Value, p.Level.Usage) +} + +func AddLogFormatFlag(cmd *cobra.Command, f *LogFlags, p LogParams) { + cmd.PersistentFlags().StringVarP(&f.Format, p.Format.Name, p.Format.Shorthand, p.Format.Value, p.Format.Usage) +} + +func AddTuiInteractiveFlag(cmd *cobra.Command, f *TuiFlags, p TuiParams) { + cmd.PersistentFlags().BoolVarP(&f.Interactive, p.Interactive.Name, p.Interactive.Shorthand, p.Interactive.Value, p.Interactive.Usage) +} diff --git a/pkg/application/logging.go b/pkg/application/logging.go new file mode 100644 index 0000000..025baaa --- /dev/null +++ b/pkg/application/logging.go @@ -0,0 +1,69 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import ( + "log/slog" + "strings" +) + +const ( + TextLogFormat LogFormat = iota + JsonLogFormat +) + +var logFormat = map[string]LogFormat{ + "text": TextLogFormat, + "json": JsonLogFormat, +} + +type LogFlags struct { + Level string + Format string +} + +type LogFormat int + +func (f LogFormat) String() string { + return [...]string{"none", "text", "json"}[f] +} + +type LogParams struct { + Level StringVar + Format StringVar + Output string +} + +func ParseLogFormat(format string) (LogFormat, bool) { + f, ok := logFormat[strings.ToLower(format)] + return f, ok +} + +func ParseLogLevel(level string) (slog.Level, bool) { + switch level { + case "error": + return slog.LevelError, true + case "warn": + return slog.LevelWarn, true + case "info": + return slog.LevelInfo, true + case "debug": + return slog.LevelDebug, true + default: + return slog.LevelError, false + } +} diff --git a/pkg/application/tui.go b/pkg/application/tui.go new file mode 100644 index 0000000..8e78d41 --- /dev/null +++ b/pkg/application/tui.go @@ -0,0 +1,25 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +type TuiFlags struct { + Interactive bool +} + +type TuiParams struct { + Interactive BoolVar +} diff --git a/pkg/application/version.go b/pkg/application/version.go new file mode 100644 index 0000000..3159f74 --- /dev/null +++ b/pkg/application/version.go @@ -0,0 +1,44 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package application + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +type Version struct { + SemVer string + Commit string + Date string + RunE func(v *Version) error +} + +func (v *Version) Command() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show version information", + RunE: func(cmd *cobra.Command, args []string) error { + return v.RunE(v) + }, + } +} + +func (v *Version) String() string { + return fmt.Sprintf("%s-%s", v.SemVer, v.Commit) +} diff --git a/pkg/client/httpclient.go b/pkg/client/httpclient.go new file mode 100644 index 0000000..7f51e15 --- /dev/null +++ b/pkg/client/httpclient.go @@ -0,0 +1,47 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package client + +import ( + "net/http" + "time" +) + +func NewHttpClient(useragent string, timeout int, followRedirects bool) *http.Client { + return &http.Client{ + Transport: NewHttpTransport(useragent), + CheckRedirect: checkRedirect(followRedirects), + Jar: nil, + Timeout: time.Duration(timeout) * time.Second, + } +} + +func checkRedirect(state bool) func(req *http.Request, via []*http.Request) error { + switch state { + case true: + return nil + case false: + return doNotFollowHttpRedirects + default: + return nil + } +} + +// DoNotFollowHttpRedirects information at https://go.dev/src/net/http/client.go - line 72 +func doNotFollowHttpRedirects(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse +} diff --git a/pkg/client/httptransport.go b/pkg/client/httptransport.go new file mode 100644 index 0000000..b58a7ab --- /dev/null +++ b/pkg/client/httptransport.go @@ -0,0 +1,41 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package client + +import ( + "crypto/tls" + "net/http" +) + +type HttpTransport struct { + T http.RoundTripper + UserAgent string +} + +func (m *HttpTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add("User-Agent", m.UserAgent) + return m.T.RoundTrip(req) +} + +func NewHttpTransport(useragent string) *HttpTransport { + return &HttpTransport{ + T: &http.Transport{ + TLSClientConfig: &tls.Config{}, + }, + UserAgent: useragent, + } +} diff --git a/pkg/pathutils/pathutils.go b/pkg/pathutils/pathutils.go new file mode 100644 index 0000000..3cbe1ab --- /dev/null +++ b/pkg/pathutils/pathutils.go @@ -0,0 +1,122 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pathutils + +import ( + "os" + "path/filepath" + "runtime" + "strings" +) + +func CreateDirectory(path string, permissions os.FileMode) error { + var ( + err error + extendedPath string + src os.FileInfo + ) + + if extendedPath, err = GetExpandedPath(path); err != nil { + return err + } + + src, err = os.Stat(extendedPath) + if os.IsNotExist(err) { + return os.MkdirAll(extendedPath, permissions) + } else if src.Mode().IsRegular() { + return os.ErrExist + } + return nil +} + +// GetExpandedPath sourced from https://github.com/spf13/viper/blob/master/util.go#L108 +func GetExpandedPath(path string) (string, error) { + if path == "$HOME" || strings.HasPrefix(path, "$HOME"+string(os.PathSeparator)) { + path = userHomeDir() + path[5:] + } + + path = os.ExpandEnv(path) + + if filepath.IsAbs(path) { + return filepath.Clean(path), nil + } + + return filepath.Abs(path) +} + +func GetFilenames(path string, extensions []string) ([]string, error) { + return walkPath(path, extensions) +} + +func isValidFile(e os.DirEntry, extensions []string) bool { + ext := filepath.Ext(e.Name()) + for _, v := range extensions { + if v == ext { + return true + } + } + return false +} + +// userHomeDir sourced from https://github.com/spf13/viper/blob/master/util.go#L140 +func userHomeDir() string { + switch runtime.GOOS { + case "windows": + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + default: + return os.Getenv("HOME") + } +} + +func walkPath(path string, extensions []string) ([]string, error) { + var ( + err error + extendedPath string + dirEntries []os.DirEntry + files []string + ) + + extendedPath, err = GetExpandedPath(path) + if err != nil { + return nil, err + } + + dirEntries, err = os.ReadDir(extendedPath) + if err != nil { + return files, err + } + + for _, file := range dirEntries { + if !file.IsDir() { + if isValidFile(file, extensions) { + files = append(files, extendedPath+"/"+file.Name()) + } + } else { + var subDirFiles []string + subDirFiles, err = walkPath(extendedPath+"/"+file.Name(), extensions) + if err != nil { + return files, err + } + files = append(files, subDirFiles...) + } + } + return files, nil +} diff --git a/pkg/server/httpserver.go b/pkg/server/httpserver.go new file mode 100644 index 0000000..f9efd93 --- /dev/null +++ b/pkg/server/httpserver.go @@ -0,0 +1,128 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package server + +import ( + "context" + "errors" + "log/slog" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" +) + +func NewHttpServer(address string, port int, handler http.Handler) *HttpServer { + return &HttpServer{ + Server: &http.Server{ + Addr: address + ":" + strconv.Itoa(port), + Handler: handler}, + } +} + +func NewTlsHttpServer(address string, port int, pubKey string, privKey string, handler http.Handler) *HttpServer { + return &HttpServer{ + Server: &http.Server{ + Addr: address + ":" + strconv.Itoa(port), + Handler: handler, + }, + UseTls: pubKey != "" && privKey != "", + PublicKey: pubKey, + PrivateKey: privKey, + } +} + +type HttpServer struct { + Server *http.Server + UseTls bool + PublicKey string + PrivateKey string +} + +func (s *HttpServer) RunServer(ctx context.Context) { + // HttpServer run context + serverCtx, serverStopCtx := context.WithCancel(ctx) + + // Listen for syscall signals for process to interrupt/quit + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + go s.shutdown(&serverCtx, serverStopCtx, &sig) + go s.start() + + // Wait for server context to be stopped + <-serverCtx.Done() +} + +func (s *HttpServer) start() { + switch s.UseTls { + case true: + s.listenAndServeTls() + case false: + s.listenAndServe() + } + +} + +func (s *HttpServer) shutdown(c *context.Context, cancelFunc context.CancelFunc, sig *chan os.Signal) { + <-*sig + var protocol string + switch s.UseTls { + case true: + protocol = "https://" + case false: + protocol = "http://" + } + + // Shutdown signal with grace period of 30 seconds + shutdownCtx, cancel := context.WithTimeout(*c, 30*time.Second) + defer cancel() + + go func(ctx context.Context, address string) { + <-ctx.Done() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + slog.Error("graceful shutdown timed out", "address", address, "error", ctx.Err()) + os.Exit(1) + } + }(shutdownCtx, protocol+s.Server.Addr) + + slog.Info("shutting down server", "address", protocol+s.Server.Addr) + // Trigger graceful shutdown + err := s.Server.Shutdown(shutdownCtx) + if err != nil { + slog.Error("could not shutdown server", "address", protocol+s.Server.Addr, "error", err) + } + + // Call parent context cancel function to complete graceful exit + cancelFunc() +} + +func (s *HttpServer) listenAndServe() { + slog.Info("server starting", "address", "http://"+s.Server.Addr) + if err := s.Server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("could not start server", "address", s.Server.Addr, "error", err) + } +} + +func (s *HttpServer) listenAndServeTls() { + slog.Info("server starting", "address", "https://"+s.Server.Addr) + if err := s.Server.ListenAndServeTLS(s.PublicKey, s.PrivateKey); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("could not start server", "address", s.Server.Addr, "error", err) + } +} diff --git a/pkg/timestamp/timestamp.go b/pkg/timestamp/timestamp.go new file mode 100644 index 0000000..8cac846 --- /dev/null +++ b/pkg/timestamp/timestamp.go @@ -0,0 +1,37 @@ +/* + * Copyright 2024 CoreLayer BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package timestamp + +import ( + "fmt" + "time" +) + +func Format(t time.Time) string { + return fmt.Sprintf("%d%02d%02d_%02d%02d%02d", + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + t.Second(), + ) +} + +func Now() string { + return Format(time.Now()) +} diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100644 index 0000000..58d2d6b --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +#/* +# * Copyright 2023 CoreLayer BV +# * +# * Licensed under the Apache License, Version 2.0 (the "License"); +# * you may not use this file except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# */ + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" + +cd "$DIR" || exit + +echo "Cleaning up previous builds and packages" +rm -rf build/bin \ No newline at end of file