add version info

This commit is contained in:
Nicola Murino 2019-08-08 10:01:33 +02:00
parent 2aca4479a5
commit 4f4489d3f1
16 changed files with 160 additions and 29 deletions

View file

@ -40,6 +40,17 @@ $ go get -u github.com/drakkan/sftpgo
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`. Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
Version info can be embedded populating the following variables at build time:
- `github.com/drakkan/sftpgo/utils.commit`
- `github.com/drakkan/sftpgo/utils.date`
For example on Linux you can build using the following ldflags:
```bash
-ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --tags --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date --utc +%FT%TZ`"
```
A systemd sample [service](https://github.com/drakkan/sftpgo/tree/master/init/sftpgo.service "systemd service") can be found inside the source tree. A systemd sample [service](https://github.com/drakkan/sftpgo/tree/master/init/sftpgo.service "systemd service") can be found inside the source tree.
Alternately you can use distro packages: Alternately you can use distro packages:
@ -195,7 +206,7 @@ For each account the following properties can be configured:
- `username` - `username`
- `password` used for password authentication. For users created using SFTPGo REST API the password will be stored using argon2id hashing algo. SFTPGo supports checking passwords stored with bcrypt too. Currently, as fallback, there is a clear text password checking but you should not store passwords as clear text and this support could be removed at any time, so please don't depend on it. - `password` used for password authentication. For users created using SFTPGo REST API the password will be stored using argon2id hashing algo. SFTPGo supports checking passwords stored with bcrypt too. Currently, as fallback, there is a clear text password checking but you should not store passwords as clear text and this support could be removed at any time, so please don't depend on it.
- `public_key` array of public keys. At least one public key or the password is mandatory. - `public_keys` array of public keys. At least one public key or the password is mandatory.
- `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path - `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path
- `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo. - `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo.
- `max_sessions` maximum concurrent sessions. 0 means unlimited - `max_sessions` maximum concurrent sessions. 0 means unlimited
@ -223,7 +234,7 @@ If quota tracking is enabled in `sftpgo` configuration file, then the used size
REST API is designed to run on localhost or on a trusted network, if you need HTTPS or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX. REST API is designed to run on localhost or on a trusted network, if you need HTTPS or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
For example you can setup a reverse proxy using apache this way: For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
``` ```
ProxyPass /api/v1 http://127.0.0.1:8080/api/v1 ProxyPass /api/v1 http://127.0.0.1:8080/api/v1
@ -236,7 +247,8 @@ and you can add authentication with something like this:
<Location /api/v1> <Location /api/v1>
AuthType Digest AuthType Digest
AuthName "Private" AuthName "Private"
AuthBasicProvider file AuthDigestDomain "/api/v1"
AuthDigestProvider file
AuthUserFile "/etc/httpd/conf/auth_digest" AuthUserFile "/etc/httpd/conf/auth_digest"
Require valid-user Require valid-user
</Location> </Location>
@ -248,7 +260,7 @@ The OpenAPI 3 schema for the exposed API can be found inside the source tree: [o
A sample CLI client for the REST API can be found inside the source tree [scripts](https://github.com/drakkan/sftpgo/tree/master/scripts "scripts") directory. A sample CLI client for the REST API can be found inside the source tree [scripts](https://github.com/drakkan/sftpgo/tree/master/scripts "scripts") directory.
You can also generate your own REST client using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/) You can also generate your own REST client, in your preferred programming language or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/)
## Logs ## Logs

View file

@ -18,6 +18,7 @@ const (
activeConnectionsPath = "/api/v1/sftp_connection" activeConnectionsPath = "/api/v1/sftp_connection"
quotaScanPath = "/api/v1/quota_scan" quotaScanPath = "/api/v1/quota_scan"
userPath = "/api/v1/user" userPath = "/api/v1/user"
versionPath = "/api/v1/version"
) )
var ( var (

View file

@ -35,6 +35,7 @@ const (
userPath = "/api/v1/user" userPath = "/api/v1/user"
activeConnectionsPath = "/api/v1/sftp_connection" activeConnectionsPath = "/api/v1/sftp_connection"
quotaScanPath = "/api/v1/quota_scan" quotaScanPath = "/api/v1/quota_scan"
versionPath = "/api/v1/version"
) )
var ( var (
@ -393,6 +394,17 @@ func TestStartQuotaScan(t *testing.T) {
} }
} }
func TestGetVersion(t *testing.T) {
_, _, err := api.GetVersion(http.StatusOK)
if err != nil {
t.Errorf("unable to get sftp version: %v", err)
}
_, _, err = api.GetVersion(http.StatusInternalServerError)
if err == nil {
t.Errorf("get version request must succeed, we requested to check a wrong status code")
}
}
func TestGetSFTPConnections(t *testing.T) { func TestGetSFTPConnections(t *testing.T) {
_, _, err := api.GetSFTPConnections(http.StatusOK) _, _, err := api.GetSFTPConnections(http.StatusOK)
if err != nil { if err != nil {
@ -668,6 +680,12 @@ func TestStartQuotaScanNonExistentUserMock(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr.Code) checkResponseCode(t, http.StatusBadRequest, rr.Code)
} }
func TestGetVersionMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, versionPath, nil)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestGetSFTPConnectionsMock(t *testing.T) { func TestGetSFTPConnectionsMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil) req, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil)
rr := executeRequest(req) rr := executeRequest(req)

View file

@ -242,6 +242,24 @@ func CloseSFTPConnection(connectionID string, expectedStatusCode int) ([]byte, e
return body, err return body, err
} }
// GetVersion returns version details
func GetVersion(expectedStatusCode int) (utils.VersionInfo, []byte, error) {
var version utils.VersionInfo
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(versionPath))
if err != nil {
return version, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &version)
} else {
body, _ = getResponseBody(resp)
}
return version, body, err
}
func checkResponse(actual int, expected int) error { func checkResponse(actual int, expected int) error {
if expected != actual { if expected != actual {
return fmt.Errorf("wrong status code: got %v want %v", actual, expected) return fmt.Errorf("wrong status code: got %v want %v", actual, expected)

View file

@ -205,5 +205,9 @@ func TestApiCallToNotListeningServer(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("request to an inactive URL must fail") t.Errorf("request to an inactive URL must fail")
} }
_, _, err = GetVersion(http.StatusOK)
if err == nil {
t.Errorf("request to an inactive URL must fail")
}
SetBaseURL(oldBaseURL) SetBaseURL(oldBaseURL)
} }

View file

@ -5,6 +5,7 @@ import (
"github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"
"github.com/go-chi/render" "github.com/go-chi/render"
@ -30,6 +31,10 @@ func initializeRouter() {
sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed) sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed)
})) }))
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, utils.GetAppVersion())
})
router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) { router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, sftpd.GetConnectionsStats()) render.JSON(w, r, sftpd.GetConnectionsStats())
}) })

View file

@ -7,6 +7,21 @@ info:
servers: servers:
- url: /api/v1 - url: /api/v1
paths: paths:
/version:
get:
tags:
- version
summary: Get version details
operationId: get_version
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref : '#/components/schemas/VersionInfo'
/sftp_connection: /sftp_connection:
get: get:
tags: tags:
@ -654,3 +669,13 @@ components:
type: string type: string
nullable: true nullable: true
description: error description if any description: error description if any
VersionInfo:
type: object
properties:
version:
type: string
build_date:
type: string
commit_hash:
type: string

View file

@ -20,9 +20,10 @@ var (
) )
func init() { func init() {
version := utils.GetAppVersion()
rootCmd.Flags().BoolP("version", "v", false, "") rootCmd.Flags().BoolP("version", "v", false, "")
rootCmd.Version = utils.GetAppVersion() rootCmd.Version = version.GetVersionAsString()
rootCmd.SetVersionTemplate(`{{printf "SFTPGo version "}}{{printf "%s" .Version}} rootCmd.SetVersionTemplate(`{{printf "SFTPGo version: "}}{{printf "%s" .Version}}
`) `)
} }

View file

@ -7,7 +7,6 @@ package config
import ( import (
"fmt" "fmt"
"runtime"
"strings" "strings"
"github.com/drakkan/sftpgo/api" "github.com/drakkan/sftpgo/api"
@ -79,10 +78,7 @@ func init() {
replacer := strings.NewReplacer(".", "__") replacer := strings.NewReplacer(".", "__")
viper.SetEnvKeyReplacer(replacer) viper.SetEnvKeyReplacer(replacer)
viper.SetConfigName(DefaultConfigName) viper.SetConfigName(DefaultConfigName)
if runtime.GOOS == "linux" { setViperAdditionalConfigPaths()
viper.AddConfigPath("$HOME/.config/sftpgo")
viper.AddConfigPath("/etc/sftpgo")
}
viper.AddConfigPath(".") viper.AddConfigPath(".")
viper.AutomaticEnv() viper.AutomaticEnv()
} }

11
config/config_linux.go Normal file
View file

@ -0,0 +1,11 @@
// +build linux
package config
import "github.com/spf13/viper"
// linux specific config search path
func setViperAdditionalConfigPaths() {
viper.AddConfigPath("$HOME/.config/sftpgo")
viper.AddConfigPath("/etc/sftpgo")
}

7
config/config_nolinux.go Normal file
View file

@ -0,0 +1,7 @@
// +build !linux
package config
func setViperAdditionalConfigPaths() {
}

View file

@ -16,6 +16,7 @@ class SFTPGoApiRequests:
self.userPath = urlparse.urljoin(baseUrl, "/api/v1/user") self.userPath = urlparse.urljoin(baseUrl, "/api/v1/user")
self.quotaScanPath = urlparse.urljoin(baseUrl, "/api/v1/quota_scan") self.quotaScanPath = urlparse.urljoin(baseUrl, "/api/v1/quota_scan")
self.activeConnectionsPath = urlparse.urljoin(baseUrl, "/api/v1/sftp_connection") self.activeConnectionsPath = urlparse.urljoin(baseUrl, "/api/v1/sftp_connection")
self.versionPath = urlparse.urljoin(baseUrl, "/api/v1/version")
self.debug = debug self.debug = debug
if authType == "basic": if authType == "basic":
self.auth = requests.auth.HTTPBasicAuth(authUser, authPassword) self.auth = requests.auth.HTTPBasicAuth(authUser, authPassword)
@ -98,6 +99,10 @@ class SFTPGoApiRequests:
r = requests.post(self.quotaScanPath, json=u, auth=self.auth, verify=self.verify) r = requests.post(self.quotaScanPath, json=u, auth=self.auth, verify=self.verify)
self.printResponse(r) self.printResponse(r)
def getVersion(self):
r = requests.get(self.versionPath, auth=self.auth, verify=self.verify)
self.printResponse(r)
def addCommonUserArguments(parser): def addCommonUserArguments(parser):
parser.add_argument('username', type=str) parser.add_argument('username', type=str)
@ -165,6 +170,8 @@ if __name__ == '__main__':
parserStartQuotaScans = subparsers.add_parser("start_quota_scan", help="Start a new quota scan") parserStartQuotaScans = subparsers.add_parser("start_quota_scan", help="Start a new quota scan")
addCommonUserArguments(parserStartQuotaScans) addCommonUserArguments(parserStartQuotaScans)
parserGetVersion = subparsers.add_parser("get_version", help="Get version details")
args = parser.parse_args() args = parser.parse_args()
api = SFTPGoApiRequests(args.debug, args.base_url, args.auth_type, args.auth_user, args.auth_password, args.verify) api = SFTPGoApiRequests(args.debug, args.base_url, args.auth_type, args.auth_user, args.auth_password, args.verify)
@ -191,4 +198,6 @@ if __name__ == '__main__':
api.getQuotaScans() api.getQuotaScans()
elif args.command == "start_quota_scan": elif args.command == "start_quota_scan":
api.startQuotaScan(args.username) api.startQuotaScan(args.username)
elif args.command == "get_version":
api.getVersion()

View file

@ -193,7 +193,7 @@ func GetConnectionsStats() []ConnectionStatus {
} }
for _, t := range activeTransfers { for _, t := range activeTransfers {
if t.connectionID == c.ID { if t.connectionID == c.ID {
if utils.GetTimeAsMsSinceEpoch(t.lastActivity) > conn.LastActivity { if t.lastActivity.UnixNano() > c.lastActivity.UnixNano() {
conn.LastActivity = utils.GetTimeAsMsSinceEpoch(t.lastActivity) conn.LastActivity = utils.GetTimeAsMsSinceEpoch(t.lastActivity)
} }
var operationType string var operationType string

View file

@ -96,6 +96,7 @@ func TestMain(m *testing.M) {
sftpdConf := config.GetSFTPDConfig() sftpdConf := config.GetSFTPDConfig()
httpdConf := config.GetHTTPDConfig() httpdConf := config.GetHTTPDConfig()
router := api.GetHTTPRouter() router := api.GetHTTPRouter()
sftpdConf.BindPort = 2022
// we run the test cases with UploadMode atomic. The non atomic code path // we run the test cases with UploadMode atomic. The non atomic code path
// simply does not execute some code so if it works in atomic mode will // simply does not execute some code so if it works in atomic mode will
// work in non atomic mode too // work in non atomic mode too
@ -105,7 +106,7 @@ func TestMain(m *testing.M) {
} else { } else {
homeBasePath = "/tmp" homeBasePath = "/tmp"
sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete"} sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete"}
sftpdConf.Actions.Command = "/bin/true" sftpdConf.Actions.Command = "/usr/bin/true"
sftpdConf.Actions.HTTPNotificationURL = "http://127.0.0.1:8080/" sftpdConf.Actions.HTTPNotificationURL = "http://127.0.0.1:8080/"
} }
@ -145,6 +146,7 @@ func TestInitialization(t *testing.T) {
config.LoadConfig(configDir, "") config.LoadConfig(configDir, "")
sftpdConf := config.GetSFTPDConfig() sftpdConf := config.GetSFTPDConfig()
sftpdConf.Umask = "invalid umask" sftpdConf.Umask = "invalid umask"
sftpdConf.BindPort = 2022
err := sftpdConf.Initialize(configDir) err := sftpdConf.Initialize(configDir)
if err == nil { if err == nil {
t.Errorf("Inizialize must fail, a SFTP server should be already running") t.Errorf("Inizialize must fail, a SFTP server should be already running")

View file

@ -12,12 +12,6 @@ import (
const logSender = "utils" const logSender = "utils"
var (
version = "dev"
commit = ""
date = ""
)
// IsStringInSlice searches a string in a slice and returns true if the string is found // IsStringInSlice searches a string in a slice and returns true if the string is found
func IsStringInSlice(obj string, list []string) bool { func IsStringInSlice(obj string, list []string) bool {
for _, v := range list { for _, v := range list {
@ -76,14 +70,7 @@ func SetPathPermissions(path string, uid int, gid int) {
} }
} }
// GetAppVersion returns the app version // GetAppVersion returns VersionInfo struct
func GetAppVersion() string { func GetAppVersion() VersionInfo {
v := version return versionInfo
if len(commit) > 0 {
v += "-" + commit
}
if len(date) > 0 {
v += "-" + date
}
return v
} }

35
utils/version.go Normal file
View file

@ -0,0 +1,35 @@
package utils
var (
version = "0.9.0-dev"
commit = ""
date = ""
versionInfo VersionInfo
)
// VersionInfo defines version details
type VersionInfo struct {
Version string `json:"version"`
BuildDate string `json:"build_date"`
CommitHash string `json:"commit_hash"`
}
// GetVersionAsString returns the string representation of the VersionInfo struct
func (v *VersionInfo) GetVersionAsString() string {
versionString := v.Version
if len(v.CommitHash) > 0 {
versionString += "-" + v.CommitHash
}
if len(v.BuildDate) > 0 {
versionString += "-" + v.BuildDate
}
return versionString
}
func init() {
versionInfo = VersionInfo{
Version: version,
CommitHash: commit,
BuildDate: date,
}
}