Merge branch 'master' into alerting_reminder
* master: (30 commits) changelog: add notes about closing #11882 renamed variable in tests added comment, variableChange -> variableValueChange added a test added if to check if new variable has been added Gravatar fallback does not respect 'AppSubUrl'-setting (#12149) change admin password after first login changelog: adds note about closing #11958 revert: reverted singlestat panel position change PR #12004 Revert "provisioning: turn relative symlinked path into absolut paths" provisioning: turn relative symlinked path into absolut paths changelog: adds note about closing #11670 elasticsearch: sort bucket keys to fix issue wth response parser tests docs: what's new in v5.2 changelog: add notes about closing #11167 docs: docker secrets support. (#12141) alerting: show alerts for user with Viewer role datasource: added option no-direct-access to ds-http-settings diretive, closes #12138 provisioning: adds fallback if evalsymlink/abs fails tests: uses different paths depending on os ...
This commit is contained in:
+1
-1
@@ -79,7 +79,7 @@ func GetAlerts(c *m.ReqContext) Response {
|
||||
DashboardIds: dashboardIDs,
|
||||
Type: string(search.DashHitDB),
|
||||
FolderIds: folderIDs,
|
||||
Permission: m.PERMISSION_EDIT,
|
||||
Permission: m.PERMISSION_VIEW,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&searchQuery)
|
||||
|
||||
@@ -77,6 +77,9 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
|
||||
r.Get("/explore/", reqEditorRole, Index)
|
||||
r.Get("/explore/*", reqEditorRole, Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, Index)
|
||||
r.Get("/playlists/*", reqSignedIn, Index)
|
||||
r.Get("/alerting/", reqSignedIn, Index)
|
||||
|
||||
@@ -52,7 +52,7 @@ type UserStars struct {
|
||||
|
||||
func GetGravatarUrl(text string) string {
|
||||
if setting.DisableGravatar {
|
||||
return "/public/img/user_profile.png"
|
||||
return setting.AppSubUrl + "/public/img/user_profile.png"
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
Children: dashboardChildNavs,
|
||||
})
|
||||
|
||||
if setting.ExploreEnabled {
|
||||
if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Explore",
|
||||
Id: "explore",
|
||||
|
||||
@@ -254,6 +254,7 @@ type DashboardProvisioning struct {
|
||||
DashboardId int64
|
||||
Name string
|
||||
ExternalId string
|
||||
CheckSum string
|
||||
Updated int64
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,10 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
if dashboards[i].OrgId == 0 {
|
||||
dashboards[i].OrgId = 1
|
||||
}
|
||||
|
||||
if dashboards[i].UpdateIntervalSeconds == 0 {
|
||||
dashboards[i].UpdateIntervalSeconds = 3
|
||||
}
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestDashboardsAsConfig(t *testing.T) {
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
validateDashboardAsConfig(cfg)
|
||||
validateDashboardAsConfig(t, cfg)
|
||||
})
|
||||
|
||||
Convey("Can read config file in version 0 format", func() {
|
||||
@@ -30,7 +30,7 @@ func TestDashboardsAsConfig(t *testing.T) {
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
validateDashboardAsConfig(cfg)
|
||||
validateDashboardAsConfig(t, cfg)
|
||||
})
|
||||
|
||||
Convey("Should skip invalid path", func() {
|
||||
@@ -56,7 +56,9 @@ func TestDashboardsAsConfig(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
|
||||
t.Helper()
|
||||
|
||||
So(len(cfg), ShouldEqual, 2)
|
||||
|
||||
ds := cfg[0]
|
||||
@@ -68,6 +70,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
So(len(ds.Options), ShouldEqual, 1)
|
||||
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds.DisableDeletion, ShouldBeTrue)
|
||||
So(ds.UpdateIntervalSeconds, ShouldEqual, 10)
|
||||
|
||||
ds2 := cfg[1]
|
||||
So(ds2.Name, ShouldEqual, "default")
|
||||
@@ -78,4 +81,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
So(len(ds2.Options), ShouldEqual, 1)
|
||||
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds2.DisableDeletion, ShouldBeFalse)
|
||||
So(ds2.UpdateIntervalSeconds, ShouldEqual, 3)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
@@ -19,8 +21,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
checkDiskForChangesInterval = time.Second * 3
|
||||
|
||||
ErrFolderNameMissing = errors.New("Folder name missing")
|
||||
)
|
||||
|
||||
@@ -47,15 +47,25 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
||||
log.Error("Cannot read directory", "error", err)
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
copy := path
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error("Could not create absolute path ", "path", path)
|
||||
absPath = path //if .Abs return an error we fallback to path
|
||||
}
|
||||
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
log.Error("Failed to read content of symlinked path: %s", path)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = copy
|
||||
log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
||||
}
|
||||
|
||||
return &fileReader{
|
||||
Cfg: cfg,
|
||||
Path: absPath,
|
||||
Path: path,
|
||||
log: log,
|
||||
dashboardService: dashboards.NewProvisioningService(),
|
||||
}, nil
|
||||
@@ -66,7 +76,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
||||
fr.log.Error("failed to search for dashboards", "error", err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(checkDiskForChangesInterval)
|
||||
ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds))
|
||||
|
||||
running := false
|
||||
|
||||
@@ -159,15 +169,20 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
||||
}
|
||||
|
||||
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
|
||||
upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
|
||||
upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix()
|
||||
|
||||
dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
||||
jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
|
||||
return provisioningMetadata, nil
|
||||
}
|
||||
|
||||
if provisionedData != nil && jsonFile.checkSum == provisionedData.CheckSum {
|
||||
upToDate = true
|
||||
}
|
||||
|
||||
// keeps track of what uid's and title's we have already provisioned
|
||||
dash := jsonFile.dashboard
|
||||
provisioningMetadata.uid = dash.Dashboard.Uid
|
||||
provisioningMetadata.title = dash.Dashboard.Title
|
||||
|
||||
@@ -185,7 +200,13 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
||||
}
|
||||
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
|
||||
dp := &models.DashboardProvisioning{
|
||||
ExternalId: path,
|
||||
Name: fr.Cfg.Name,
|
||||
Updated: resolvedFileInfo.ModTime().Unix(),
|
||||
CheckSum: jsonFile.checkSum,
|
||||
}
|
||||
|
||||
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
|
||||
return provisioningMetadata, err
|
||||
}
|
||||
@@ -283,14 +304,30 @@ func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
type dashboardJsonFile struct {
|
||||
dashboard *dashboards.SaveDashboardDTO
|
||||
checkSum string
|
||||
lastModified time.Time
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboardJsonFile, error) {
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := simplejson.NewFromReader(reader)
|
||||
all, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkSum, err := util.Md5SumString(string(all))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := simplejson.NewJson(all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -300,7 +337,11 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
return &dashboardJsonFile{
|
||||
dashboard: dash,
|
||||
checkSum: checkSum,
|
||||
lastModified: lastModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type provisioningMetadata struct {
|
||||
@@ -328,7 +369,6 @@ func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
|
||||
if len(pm.title) > 0 {
|
||||
checker.titleUsage[pm.title] += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
||||
@@ -343,5 +383,4 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
||||
log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// +build linux
|
||||
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
var (
|
||||
symlinkedFolder = "testdata/test-dashboards/symlink"
|
||||
)
|
||||
|
||||
func TestProvsionedSymlinkedFolder(t *testing.T) {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{"path": symlinkedFolder},
|
||||
}
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil")
|
||||
}
|
||||
|
||||
want, err := filepath.Abs(containingId)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected err to be nill")
|
||||
}
|
||||
|
||||
if reader.Path != want {
|
||||
t.Errorf("got %s want %s", reader.Path, want)
|
||||
}
|
||||
}
|
||||
@@ -49,13 +49,16 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("using full path", func() {
|
||||
cfg.Options["folder"] = "/var/lib/grafana/dashboards"
|
||||
fullPath := "/var/lib/grafana/dashboards"
|
||||
if runtime.GOOS == "windows" {
|
||||
fullPath = `c:\var\lib\grafana`
|
||||
}
|
||||
|
||||
cfg.Options["folder"] = fullPath
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
}
|
||||
So(reader.Path, ShouldEqual, fullPath)
|
||||
So(filepath.IsAbs(reader.Path), ShouldBeTrue)
|
||||
})
|
||||
|
||||
|
||||
Vendored
+1
@@ -6,6 +6,7 @@ providers:
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
containing-id/
|
||||
@@ -10,23 +10,25 @@ import (
|
||||
)
|
||||
|
||||
type DashboardsAsConfig struct {
|
||||
Name string
|
||||
Type string
|
||||
OrgId int64
|
||||
Folder string
|
||||
Editable bool
|
||||
Options map[string]interface{}
|
||||
DisableDeletion bool
|
||||
Name string
|
||||
Type string
|
||||
OrgId int64
|
||||
Folder string
|
||||
Editable bool
|
||||
Options map[string]interface{}
|
||||
DisableDeletion bool
|
||||
UpdateIntervalSeconds int64
|
||||
}
|
||||
|
||||
type DashboardsAsConfigV0 struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
|
||||
}
|
||||
|
||||
type ConfigVersion struct {
|
||||
@@ -38,13 +40,14 @@ type DashboardAsConfigV1 struct {
|
||||
}
|
||||
|
||||
type DashboardProviderConfigs struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
|
||||
}
|
||||
|
||||
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
@@ -68,13 +71,14 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig
|
||||
|
||||
for _, v := range v0 {
|
||||
r = append(r, &DashboardsAsConfig{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
UpdateIntervalSeconds: v.UpdateIntervalSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,13 +90,14 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
|
||||
|
||||
for _, v := range dc.Providers {
|
||||
r = append(r, &DashboardsAsConfig{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
UpdateIntervalSeconds: v.UpdateIntervalSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
}
|
||||
|
||||
if query.User.OrgRole != m.ROLE_ADMIN {
|
||||
builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT)
|
||||
builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_VIEW)
|
||||
}
|
||||
|
||||
builder.Write(" ORDER BY name ASC")
|
||||
|
||||
@@ -2,7 +2,6 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -110,11 +109,12 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Viewer cannot read alerts", func() {
|
||||
alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}}
|
||||
viewerUser := &m.SignedInUser{OrgRole: m.ROLE_VIEWER, OrgId: 1}
|
||||
alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser}
|
||||
err2 := HandleAlertsQuery(&alertQuery)
|
||||
|
||||
So(err2, ShouldBeNil)
|
||||
So(alertQuery.Result, ShouldHaveLength, 0)
|
||||
So(alertQuery.Result, ShouldHaveLength, 1)
|
||||
})
|
||||
|
||||
Convey("Alerts with same dashboard id and panel id should update", func() {
|
||||
|
||||
@@ -211,4 +211,8 @@ func addDashboardMigration(mg *Migrator) {
|
||||
"name": "name",
|
||||
"external_id": "external_id",
|
||||
})
|
||||
|
||||
mg.AddMigration("Add check_sum column", NewAddColumnMigration(dashboardExtrasTableV2, &Column{
|
||||
Name: "check_sum", Type: DB_NVarchar, Length: 32, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -113,15 +113,22 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range esAgg.Get("buckets").MustMap() {
|
||||
bucket := simplejson.NewFromAny(v)
|
||||
buckets := esAgg.Get("buckets").MustMap()
|
||||
bucketKeys := make([]string, 0)
|
||||
for k := range buckets {
|
||||
bucketKeys = append(bucketKeys, k)
|
||||
}
|
||||
sort.Strings(bucketKeys)
|
||||
|
||||
for _, bucketKey := range bucketKeys {
|
||||
bucket := simplejson.NewFromAny(buckets[bucketKey])
|
||||
newProps := make(map[string]string, 0)
|
||||
|
||||
for k, v := range props {
|
||||
newProps[k] = v
|
||||
}
|
||||
|
||||
newProps["filter"] = k
|
||||
newProps["filter"] = bucketKey
|
||||
|
||||
err = rp.processBuckets(bucket.MustMap(), target, series, table, newProps, depth+1)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Md5Sum calculates the md5sum of a stream
|
||||
func Md5Sum(reader io.Reader) (string, error) {
|
||||
var returnMD5String string
|
||||
hash := md5.New()
|
||||
if _, err := io.Copy(hash, reader); err != nil {
|
||||
return returnMD5String, err
|
||||
}
|
||||
hashInBytes := hash.Sum(nil)[:16]
|
||||
returnMD5String = hex.EncodeToString(hashInBytes)
|
||||
return returnMD5String, nil
|
||||
}
|
||||
|
||||
// Md5Sum calculates the md5sum of a string
|
||||
func Md5SumString(input string) (string, error) {
|
||||
buffer := strings.NewReader(input)
|
||||
return Md5Sum(buffer)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMd5Sum(t *testing.T) {
|
||||
input := "dont hash passwords with md5"
|
||||
|
||||
have, err := Md5SumString(input)
|
||||
if err != nil {
|
||||
t.Fatal("expected err to be nil")
|
||||
}
|
||||
|
||||
want := "2d6a56c82d09d374643b926d3417afba"
|
||||
if have != want {
|
||||
t.Fatalf("expected: %s got: %s", want, have)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user