diff --git a/.editorconfig b/.editorconfig index 831bb8696cc..3701d80b453 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ root = true [*.go] -indent_style = tabs +indent_style = tab indent_size = 2 charset = utf-8 trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index 515c19e7e0b..721a2a71ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ public/css/*.min.css *.swp .idea/ *.iml +.vscode/ /data/* /bin/* @@ -37,4 +38,4 @@ profile.cov .notouch /pkg/cmd/grafana-cli/grafana-cli /pkg/cmd/grafana-server/grafana-server -/examples/*/dist \ No newline at end of file +/examples/*/dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 0352a2a3f8f..487a97cf3b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,17 @@ * **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609) * **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456) * **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613) +* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827) + +### Breaking changes +* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971) # 3.1.2 (unreleased) * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790) * **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767) * **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739) * **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806) +* **Grafana-CLI**: Improve error message for upgrade-all command, fixes [#5885](https://github.com/grafana/grafana/issues/5885) # 3.1.1 (2016-08-01) * **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7d0d7f15009..bed9f878753 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,6 @@ { "ImportPath": "github.com/grafana/grafana", - "GoVersion": "go1.5.1", + "GoVersion": "go1.6.2", "GodepVersion": "v60", "Packages": [ "./pkg/..." @@ -368,8 +368,8 @@ }, { "ImportPath": "gopkg.in/ini.v1", - "Comment": "v0-16-g1772191", - "Rev": "177219109c97e7920c933e21c9b25f874357b237" + "Comment": "v1.21.1", + "Rev": "6e4869b434bd001f6983749881c7ead3545887d8" }, { "ImportPath": "gopkg.in/macaron.v1", diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore b/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore index 53b64215a3b..c5203bf6e73 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore @@ -1,3 +1,5 @@ testdata/conf_out.ini ini.sublime-project ini.sublime-workspace +testdata/conf_reflect.ini +.idea diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml b/Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml new file mode 100644 index 00000000000..0064ba1d7cd --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml @@ -0,0 +1,16 @@ +sudo: false +language: go + +go: + - 1.4 + - 1.5 + - 1.6 + - tip + +script: + - go get -v github.com/smartystreets/goconvey + - go test -v -cover -race + +notifications: + email: + - u@gogs.io diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/Makefile b/Godeps/_workspace/src/gopkg.in/ini.v1/Makefile new file mode 100644 index 00000000000..ac034e5258f --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/Makefile @@ -0,0 +1,12 @@ +.PHONY: build test bench vet + +build: vet bench + +test: + go test -v -cover -race + +bench: + go test -v -cover -race -test.bench=. -test.benchmem + +vet: + go vet diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/README.md b/Godeps/_workspace/src/gopkg.in/ini.v1/README.md index 6d771819656..a939d75e9f2 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/README.md +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/README.md @@ -1,6 +1,8 @@ -ini [![Build Status](https://drone.io/github.com/go-ini/ini/status.png)](https://drone.io/github.com/go-ini/ini/latest) [![](http://gocover.io/_badge/github.com/go-ini/ini)](http://gocover.io/github.com/go-ini/ini) +INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini) === +![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200) + Package ini provides INI file read and write functionality in Go. [简体中文](README_ZH.md) @@ -20,13 +22,29 @@ Package ini provides INI file read and write functionality in Go. ## Installation +To use a tagged revision: + go get gopkg.in/ini.v1 +To use with latest changes: + + go get github.com/go-ini/ini + +Please add `-u` flag to update in the future. + +### Testing + +If you want to test on your machine, please apply `-t` flag: + + go get -t gopkg.in/ini.v1 + +Please add `-u` flag to update in the future. + ## Getting Started ### Loading from data sources -A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many as** data sources you want. Passing other types will simply return an error. +A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many data sources as you want**. Passing other types will simply return an error. ```go cfg, err := ini.Load([]byte("raw data"), "filename") @@ -38,12 +56,56 @@ Or start with an empty object: cfg := ini.Empty() ``` -When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later. +When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later. ```go err := cfg.Append("other file", []byte("other raw data")) ``` +If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error. + +```go +cfg, err := ini.LooseLoad("filename", "filename_404") +``` + +The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual. + +#### Ignore cases of key name + +When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing. + +```go +cfg, err := ini.InsensitiveLoad("filename") +//... + +// sec1 and sec2 are the exactly same section object +sec1, err := cfg.GetSection("Section") +sec2, err := cfg.GetSection("SecTIOn") + +// key1 and key2 are the exactly same key object +key1, err := cfg.GetKey("Key") +key2, err := cfg.GetKey("KeY") +``` + +#### MySQL-like boolean key + +MySQL's configuration allows a key without value as follows: + +```ini +[mysqld] +... +skip-host-cache +skip-name-resolve +``` + +By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options: + +```go +cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) +``` + +The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read. + ### Working with sections To get a section, you would need to: @@ -93,6 +155,12 @@ Same rule applies to key operations: key := cfg.Section("").Key("key name") ``` +To check if a key exists: + +```go +yes := cfg.Section("").HasKey("key name") +``` + To create a new key: ```go @@ -102,14 +170,14 @@ err := cfg.Section("").NewKey("name", "value") To get a list of keys or key names: ```go -keys := cfg.Section().Keys() -names := cfg.Section().KeyStrings() +keys := cfg.Section("").Keys() +names := cfg.Section("").KeyStrings() ``` To get a clone hash of keys and corresponding values: ```go -hash := cfg.GetSection("").KeysHash() +hash := cfg.Section("").KeysHash() ``` ### Working with values @@ -120,16 +188,41 @@ To get a string value: val := cfg.Section("").Key("key name").String() ``` +To validate key value on the fly: + +```go +val := cfg.Section("").Key("key name").Validate(func(in string) string { + if len(in) == 0 { + return "default" + } + return in +}) +``` + +If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance): + +```go +val := cfg.Section("").Key("key name").Value() +``` + +To check if raw value exists: + +```go +yes := cfg.Section("").HasValue("test value") +``` + To get value with types: ```go // For boolean values: -// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On -// false when value is: 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off +// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On +// false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() v, err = cfg.Section("").Key("INT64").Int64() +v, err = cfg.Section("").Key("UINT").Uint() +v, err = cfg.Section("").Key("UINT64").Uint64() v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) v, err = cfg.Section("").Key("TIME").Time() // RFC3339 @@ -137,6 +230,8 @@ v = cfg.Section("").Key("BOOL").MustBool() v = cfg.Section("").Key("FLOAT64").MustFloat64() v = cfg.Section("").Key("INT").MustInt() v = cfg.Section("").Key("INT64").MustInt64() +v = cfg.Section("").Key("UINT").MustUint() +v = cfg.Section("").Key("UINT64").MustUint64() v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) v = cfg.Section("").Key("TIME").MustTime() // RFC3339 @@ -144,11 +239,13 @@ v = cfg.Section("").Key("TIME").MustTime() // RFC3339 // when key not found or fail to parse value to given type. // Except method MustString, which you have to pass a default value. -v = cfg.Seciont("").Key("String").MustString("default") +v = cfg.Section("").Key("String").MustString("default") v = cfg.Section("").Key("BOOL").MustBool(true) v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) v = cfg.Section("").Key("INT").MustInt(10) v = cfg.Section("").Key("INT64").MustInt64(99) +v = cfg.Section("").Key("UINT").MustUint(3) +v = cfg.Section("").Key("UINT64").MustUint64(6) v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 ``` @@ -174,6 +271,42 @@ Earth ------ end --- */ ``` +That's cool, how about continuation lines? + +```ini +[advance] +two_lines = how about \ + continuation lines? +lots_of_lines = 1 \ + 2 \ + 3 \ + 4 +``` + +Piece of cake! + +```go +cfg.Section("advance").Key("two_lines").String() // how about continuation lines? +cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 +``` + +Well, I hate continuation lines, how do I disable that? + +```go +cfg, err := ini.LoadSources(ini.LoadOptions{ + IgnoreContinuation: true, +}, "filename") +``` + +Holy crap! + +Note that single quotes around values will be stripped: + +```ini +foo = "some value" // foo: some value +bar = 'some value' // bar: some value +``` + That's all? Hmm, no. #### Helper methods of working with values @@ -185,6 +318,8 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) +v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) +v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 ``` @@ -197,20 +332,74 @@ To validate value in a given range: vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) +vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) +vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 ``` -To auto-split value into slice: +##### Auto-split values into a slice + +To use zero value of type for invalid inputs: ```go +// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] +// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] vals = cfg.Section("").Key("STRINGS").Strings(",") vals = cfg.Section("").Key("FLOAT64S").Float64s(",") vals = cfg.Section("").Key("INTS").Ints(",") vals = cfg.Section("").Key("INT64S").Int64s(",") +vals = cfg.Section("").Key("UINTS").Uints(",") +vals = cfg.Section("").Key("UINT64S").Uint64s(",") vals = cfg.Section("").Key("TIMES").Times(",") ``` +To exclude invalid values out of result slice: + +```go +// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] +// Input: how, 2.2, are, you -> [2.2] +vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") +vals = cfg.Section("").Key("INTS").ValidInts(",") +vals = cfg.Section("").Key("INT64S").ValidInt64s(",") +vals = cfg.Section("").Key("UINTS").ValidUints(",") +vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") +vals = cfg.Section("").Key("TIMES").ValidTimes(",") +``` + +Or to return nothing but error when have invalid inputs: + +```go +// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] +// Input: how, 2.2, are, you -> error +vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") +vals = cfg.Section("").Key("INTS").StrictInts(",") +vals = cfg.Section("").Key("INT64S").StrictInt64s(",") +vals = cfg.Section("").Key("UINTS").StrictUints(",") +vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") +vals = cfg.Section("").Key("TIMES").StrictTimes(",") +``` + +### Save your configuration + +Finally, it's time to save your configuration to somewhere. + +A typical way to save configuration is writing it to a file: + +```go +// ... +err = cfg.SaveTo("my.ini") +err = cfg.SaveToIndent("my.ini", "\t") +``` + +Another way to save is writing to a `io.Writer` interface: + +```go +// ... +cfg.WriteTo(writer) +cfg.WriteToIndent(writer, "\t") +``` + ## Advanced Usage ### Recursive Values @@ -252,6 +441,12 @@ CLONE_URL = https://%(IMPORT_PATH)s cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 ``` +#### Retrieve parent keys available to a child section + +```go +cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] +``` + ### Auto-increment Key Names If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter. @@ -327,9 +522,57 @@ p := &Person{ // ... ``` +It's really cool, but what's the point if you can't give me my file back from struct? + +### Reflect From Struct + +Why not? + +```go +type Embeded struct { + Dates []time.Time `delim:"|"` + Places []string `ini:"places,omitempty"` + None []int `ini:",omitempty"` +} + +type Author struct { + Name string `ini:"NAME"` + Male bool + Age int + GPA float64 + NeverMind string `ini:"-"` + *Embeded +} + +func main() { + a := &Author{"Unknwon", true, 21, 2.8, "", + &Embeded{ + []time.Time{time.Now(), time.Now()}, + []string{"HangZhou", "Boston"}, + []int{}, + }} + cfg := ini.Empty() + err = ini.ReflectFrom(cfg, a) + // ... +} +``` + +So, what do I get? + +```ini +NAME = Unknwon +Male = true +Age = 21 +GPA = 2.8 + +[Embeded] +Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 +places = HangZhou,Boston +``` + #### Name Mapper -To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name. +To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name. There are 2 built-in name mappers: @@ -339,15 +582,15 @@ There are 2 built-in name mappers: To use them: ```go -type Info struct{ +type Info struct { PackageName string } func main() { - err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("packag_name=ini")) + err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) // ... - cfg, err := ini.Load("PACKAGE_NAME=ini") + cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) // ... info := new(Info) cfg.NameMapper = ini.AllCapsUnderscore @@ -356,6 +599,88 @@ func main() { } ``` +Same rules of name mapper apply to `ini.ReflectFromWithMapper` function. + +#### Value Mapper + +To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values: + +```go +type Env struct { + Foo string `ini:"foo"` +} + +func main() { + cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") + cfg.ValueMapper = os.ExpandEnv + // ... + env := &Env{} + err = cfg.Section("env").MapTo(env) +} +``` + +This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`. + +#### Other Notes On Map/Reflect + +Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature: + +```go +type Child struct { + Age string +} + +type Parent struct { + Name string + Child +} + +type Config struct { + City string + Parent +} +``` + +Example configuration: + +```ini +City = Boston + +[Parent] +Name = Unknwon + +[Child] +Age = 21 +``` + +What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome. + +```go +type Child struct { + Age string +} + +type Parent struct { + Name string + Child `ini:"Parent"` +} + +type Config struct { + City string + Parent +} +``` + +Example configuration: + +```ini +City = Boston + +[Parent] +Name = Unknwon +Age = 21 +``` + ## Getting Help - [API Documentation](https://gowalker.org/gopkg.in/ini.v1) diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md b/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md index c455cb67297..2178e47895d 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md @@ -15,8 +15,24 @@ ## 下载安装 +使用一个特定版本: + go get gopkg.in/ini.v1 +使用最新版: + + go get github.com/go-ini/ini + +如需更新请添加 `-u` 选项。 + +### 测试安装 + +如果您想要在自己的机器上运行测试,请使用 `-t` 标记: + + go get -t gopkg.in/ini.v1 + +如需更新请添加 `-u` 选项。 + ## 开始使用 ### 从数据源加载 @@ -39,6 +55,50 @@ cfg := ini.Empty() err := cfg.Append("other file", []byte("other raw data")) ``` +当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误): + +```go +cfg, err := ini.LooseLoad("filename", "filename_404") +``` + +更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。 + +#### 忽略键名的大小写 + +有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写: + +```go +cfg, err := ini.InsensitiveLoad("filename") +//... + +// sec1 和 sec2 指向同一个分区对象 +sec1, err := cfg.GetSection("Section") +sec2, err := cfg.GetSection("SecTIOn") + +// key1 和 key2 指向同一个键对象 +key1, err := cfg.GetKey("Key") +key2, err := cfg.GetKey("KeY") +``` + +#### 类似 MySQL 配置中的布尔值键 + +MySQL 的配置文件中会出现没有具体值的布尔类型的键: + +```ini +[mysqld] +... +skip-host-cache +skip-name-resolve +``` + +默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理: + +```go +cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) +``` + +这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 + ### 操作分区(Section) 获取指定分区: @@ -88,6 +148,12 @@ key, err := cfg.Section("").GetKey("key name") key := cfg.Section("").Key("key name") ``` +判断某个键是否存在: + +```go +yes := cfg.Section("").HasKey("key name") +``` + 创建一个新的键: ```go @@ -97,14 +163,14 @@ err := cfg.Section("").NewKey("name", "value") 获取分区下的所有键或键名: ```go -keys := cfg.Section().Keys() -names := cfg.Section().KeyStrings() +keys := cfg.Section("").Keys() +names := cfg.Section("").KeyStrings() ``` 获取分区下的所有键值对的克隆: ```go -hash := cfg.GetSection("").KeysHash() +hash := cfg.Section("").KeysHash() ``` ### 操作键值(Value) @@ -115,16 +181,41 @@ hash := cfg.GetSection("").KeysHash() val := cfg.Section("").Key("key name").String() ``` +获取值的同时通过自定义函数进行处理验证: + +```go +val := cfg.Section("").Key("key name").Validate(func(in string) string { + if len(in) == 0 { + return "default" + } + return in +}) +``` + +如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳): + +```go +val := cfg.Section("").Key("key name").Value() +``` + +判断某个原值是否存在: + +```go +yes := cfg.Section("").HasValue("test value") +``` + 获取其它类型的值: ```go // 布尔值的规则: -// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On -// false 当值为:0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off +// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On +// false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() v, err = cfg.Section("").Key("INT64").Int64() +v, err = cfg.Section("").Key("UINT").Uint() +v, err = cfg.Section("").Key("UINT64").Uint64() v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) v, err = cfg.Section("").Key("TIME").Time() // RFC3339 @@ -132,6 +223,8 @@ v = cfg.Section("").Key("BOOL").MustBool() v = cfg.Section("").Key("FLOAT64").MustFloat64() v = cfg.Section("").Key("INT").MustInt() v = cfg.Section("").Key("INT64").MustInt64() +v = cfg.Section("").Key("UINT").MustUint() +v = cfg.Section("").Key("UINT64").MustUint64() v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) v = cfg.Section("").Key("TIME").MustTime() // RFC3339 @@ -144,6 +237,8 @@ v = cfg.Section("").Key("BOOL").MustBool(true) v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) v = cfg.Section("").Key("INT").MustInt(10) v = cfg.Section("").Key("INT64").MustInt64(99) +v = cfg.Section("").Key("UINT").MustUint(3) +v = cfg.Section("").Key("UINT64").MustUint64(6) v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 ``` @@ -169,6 +264,42 @@ Earth ------ end --- */ ``` +赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办? + +```ini +[advance] +two_lines = how about \ + continuation lines? +lots_of_lines = 1 \ + 2 \ + 3 \ + 4 +``` + +简直是小菜一碟! + +```go +cfg.Section("advance").Key("two_lines").String() // how about continuation lines? +cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 +``` + +可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢? + +```go +cfg, err := ini.LoadSources(ini.LoadOptions{ + IgnoreContinuation: true, +}, "filename") +``` + +哇靠给力啊! + +需要注意的是,值两侧的单引号会被自动剔除: + +```ini +foo = "some value" // foo: some value +bar = 'some value' // bar: some value +``` + 这就是全部了?哈哈,当然不是。 #### 操作键值的辅助方法 @@ -180,6 +311,8 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) +v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) +v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 ``` @@ -192,20 +325,74 @@ v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, tim vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) +vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) +vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 ``` -自动分割键值为切片(slice): +##### 自动分割键值到切片(slice) + +当存在无效输入时,使用零值代替: ```go +// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] +// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] vals = cfg.Section("").Key("STRINGS").Strings(",") vals = cfg.Section("").Key("FLOAT64S").Float64s(",") vals = cfg.Section("").Key("INTS").Ints(",") vals = cfg.Section("").Key("INT64S").Int64s(",") +vals = cfg.Section("").Key("UINTS").Uints(",") +vals = cfg.Section("").Key("UINT64S").Uint64s(",") vals = cfg.Section("").Key("TIMES").Times(",") ``` +从结果切片中剔除无效输入: + +```go +// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] +// Input: how, 2.2, are, you -> [2.2] +vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") +vals = cfg.Section("").Key("INTS").ValidInts(",") +vals = cfg.Section("").Key("INT64S").ValidInt64s(",") +vals = cfg.Section("").Key("UINTS").ValidUints(",") +vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") +vals = cfg.Section("").Key("TIMES").ValidTimes(",") +``` + +当存在无效输入时,直接返回错误: + +```go +// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] +// Input: how, 2.2, are, you -> error +vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") +vals = cfg.Section("").Key("INTS").StrictInts(",") +vals = cfg.Section("").Key("INT64S").StrictInt64s(",") +vals = cfg.Section("").Key("UINTS").StrictUints(",") +vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") +vals = cfg.Section("").Key("TIMES").StrictTimes(",") +``` + +### 保存配置 + +终于到了这个时刻,是时候保存一下配置了。 + +比较原始的做法是输出配置到某个文件: + +```go +// ... +err = cfg.SaveTo("my.ini") +err = cfg.SaveToIndent("my.ini", "\t") +``` + +另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中: + +```go +// ... +cfg.WriteTo(writer) +cfg.WriteToIndent(writer, "\t") +``` + ### 高级用法 #### 递归读取键值 @@ -247,6 +434,12 @@ CLONE_URL = https://%(IMPORT_PATH)s cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 ``` +#### 获取上级父分区下的所有键名 + +```go +cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] +``` + #### 读取自增键名 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 @@ -320,6 +513,54 @@ p := &Person{ // ... ``` +这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用? + +### 从结构反射 + +可是,我有说不能吗? + +```go +type Embeded struct { + Dates []time.Time `delim:"|"` + Places []string `ini:"places,omitempty"` + None []int `ini:",omitempty"` +} + +type Author struct { + Name string `ini:"NAME"` + Male bool + Age int + GPA float64 + NeverMind string `ini:"-"` + *Embeded +} + +func main() { + a := &Author{"Unknwon", true, 21, 2.8, "", + &Embeded{ + []time.Time{time.Now(), time.Now()}, + []string{"HangZhou", "Boston"}, + []int{}, + }} + cfg := ini.Empty() + err = ini.ReflectFrom(cfg, a) + // ... +} +``` + +瞧瞧,奇迹发生了。 + +```ini +NAME = Unknwon +Male = true +Age = 21 +GPA = 2.8 + +[Embeded] +Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 +places = HangZhou,Boston +``` + #### 名称映射器(Name Mapper) 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 @@ -337,10 +578,10 @@ type Info struct{ } func main() { - err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("packag_name=ini")) + err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) // ... - cfg, err := ini.Load("PACKAGE_NAME=ini") + cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) // ... info := new(Info) cfg.NameMapper = ini.AllCapsUnderscore @@ -349,6 +590,88 @@ func main() { } ``` +使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。 + +#### 值映射器(Value Mapper) + +值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量: + +```go +type Env struct { + Foo string `ini:"foo"` +} + +func main() { + cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") + cfg.ValueMapper = os.ExpandEnv + // ... + env := &Env{} + err = cfg.Section("env").MapTo(env) +} +``` + +本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。 + +#### 映射/反射的其它说明 + +任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联: + +```go +type Child struct { + Age string +} + +type Parent struct { + Name string + Child +} + +type Config struct { + City string + Parent +} +``` + +示例配置文件: + +```ini +City = Boston + +[Parent] +Name = Unknwon + +[Child] +Age = 21 +``` + +很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚! + +```go +type Child struct { + Age string +} + +type Parent struct { + Name string + Child `ini:"Parent"` +} + +type Config struct { + City string + Parent +} +``` + +示例配置文件: + +```ini +City = Boston + +[Parent] +Name = Unknwon +Age = 21 +``` + ## 获取帮助 - [API 文档](https://gowalker.org/gopkg.in/ini.v1) diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/error.go b/Godeps/_workspace/src/gopkg.in/ini.v1/error.go new file mode 100644 index 00000000000..80afe743158 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/error.go @@ -0,0 +1,32 @@ +// Copyright 2016 Unknwon +// +// 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 ini + +import ( + "fmt" +) + +type ErrDelimiterNotFound struct { + Line string +} + +func IsErrDelimiterNotFound(err error) bool { + _, ok := err.(ErrDelimiterNotFound) + return ok +} + +func (err ErrDelimiterNotFound) Error() string { + return fmt.Sprintf("key-value delimiter not found: %s", err.Line) +} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go b/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go index 6674baf0b00..cd065e7822b 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go @@ -16,7 +16,6 @@ package ini import ( - "bufio" "bytes" "errors" "fmt" @@ -31,25 +30,35 @@ import ( ) const ( + // Name for default section. You can use this constant or the string literal. + // In most of cases, an empty string is all you need to access the section. DEFAULT_SECTION = "DEFAULT" + // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 - - _VERSION = "1.2.6" + _VERSION = "1.21.1" ) +// Version returns current package version literal. func Version() string { return _VERSION } var ( + // Delimiter to determine or compose a new line. + // This variable will be changed to "\r\n" automatically on Windows + // at package init time. LineBreak = "\n" // Variable regexp pattern: %(variable)s varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) - // Write spaces around "=" to look better. + // Indicate whether to align "=" sign with spaces to produce pretty output + // or reduce all possible spaces for compact format. PrettyFormat = true + + // Explicitly write DEFAULT section header + DefaultHeader = false ) func init() { @@ -67,501 +76,41 @@ func inSlice(str string, s []string) bool { return false } -// dataSource is a interface that returns file content. +// dataSource is an interface that returns object which can be read and closed. type dataSource interface { - Reader() (io.Reader, error) + ReadCloser() (io.ReadCloser, error) } +// sourceFile represents an object that contains content on the local file system. type sourceFile struct { name string } -func (s sourceFile) Reader() (io.Reader, error) { +func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { return os.Open(s.name) } +type bytesReadCloser struct { + reader io.Reader +} + +func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { + return rc.reader.Read(p) +} + +func (rc *bytesReadCloser) Close() error { + return nil +} + +// sourceData represents an object that contains content in memory. type sourceData struct { data []byte } -func (s *sourceData) Reader() (io.Reader, error) { - return bytes.NewReader(s.data), nil +func (s *sourceData) ReadCloser() (io.ReadCloser, error) { + return &bytesReadCloser{bytes.NewReader(s.data)}, nil } -// ____ __. -// | |/ _|____ ___.__. -// | <_/ __ < | | -// | | \ ___/\___ | -// |____|__ \___ > ____| -// \/ \/\/ - -// Key represents a key under a section. -type Key struct { - s *Section - Comment string - name string - value string - isAutoIncr bool -} - -// Name returns name of key. -func (k *Key) Name() string { - return k.name -} - -// Value returns raw value of key for performance purpose. -func (k *Key) Value() string { - return k.value -} - -// String returns string representation of value. -func (k *Key) String() string { - val := k.value - if strings.Index(val, "%") == -1 { - return val - } - - for i := 0; i < _DEPTH_VALUES; i++ { - vr := varPattern.FindString(val) - if len(vr) == 0 { - break - } - - // Take off leading '%(' and trailing ')s'. - noption := strings.TrimLeft(vr, "%(") - noption = strings.TrimRight(noption, ")s") - - // Search in the same section. - nk, err := k.s.GetKey(noption) - if err != nil { - // Search again in default section. - nk, _ = k.s.f.Section("").GetKey(noption) - } - - // Substitute by new value and take off leading '%(' and trailing ')s'. - val = strings.Replace(val, vr, nk.value, -1) - } - return val -} - -// parseBool returns the boolean value represented by the string. -// -// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On, -// 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off. -// Any other value returns an error. -func parseBool(str string) (value bool, err error) { - switch str { - case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "ON", "on", "On": - return true, nil - case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "OFF", "off", "Off": - return false, nil - } - return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) -} - -// Bool returns bool type value. -func (k *Key) Bool() (bool, error) { - return parseBool(k.String()) -} - -// Float64 returns float64 type value. -func (k *Key) Float64() (float64, error) { - return strconv.ParseFloat(k.String(), 64) -} - -// Int returns int type value. -func (k *Key) Int() (int, error) { - return strconv.Atoi(k.String()) -} - -// Int64 returns int64 type value. -func (k *Key) Int64() (int64, error) { - return strconv.ParseInt(k.String(), 10, 64) -} - -// TimeFormat parses with given format and returns time.Time type value. -func (k *Key) TimeFormat(format string) (time.Time, error) { - return time.Parse(format, k.String()) -} - -// Time parses with RFC3339 format and returns time.Time type value. -func (k *Key) Time() (time.Time, error) { - return k.TimeFormat(time.RFC3339) -} - -// MustString returns default value if key value is empty. -func (k *Key) MustString(defaultVal string) string { - val := k.String() - if len(val) == 0 { - return defaultVal - } - return val -} - -// MustBool always returns value without error, -// it returns false if error occurs. -func (k *Key) MustBool(defaultVal ...bool) bool { - val, err := k.Bool() - if len(defaultVal) > 0 && err != nil { - return defaultVal[0] - } - return val -} - -// MustFloat64 always returns value without error, -// it returns 0.0 if error occurs. -func (k *Key) MustFloat64(defaultVal ...float64) float64 { - val, err := k.Float64() - if len(defaultVal) > 0 && err != nil { - return defaultVal[0] - } - return val -} - -// MustInt always returns value without error, -// it returns 0 if error occurs. -func (k *Key) MustInt(defaultVal ...int) int { - val, err := k.Int() - if len(defaultVal) > 0 && err != nil { - return defaultVal[0] - } - return val -} - -// MustInt64 always returns value without error, -// it returns 0 if error occurs. -func (k *Key) MustInt64(defaultVal ...int64) int64 { - val, err := k.Int64() - if len(defaultVal) > 0 && err != nil { - return defaultVal[0] - } - return val -} - -// MustTimeFormat always parses with given format and returns value without error, -// it returns zero value if error occurs. -func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { - val, err := k.TimeFormat(format) - if len(defaultVal) > 0 && err != nil { - return defaultVal[0] - } - return val -} - -// MustTime always parses with RFC3339 format and returns value without error, -// it returns zero value if error occurs. -func (k *Key) MustTime(defaultVal ...time.Time) time.Time { - return k.MustTimeFormat(time.RFC3339, defaultVal...) -} - -// In always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) In(defaultVal string, candidates []string) string { - val := k.String() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InFloat64 always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { - val := k.MustFloat64() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InInt always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InInt(defaultVal int, candidates []int) int { - val := k.MustInt() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InInt64 always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { - val := k.MustInt64() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InTimeFormat always parses with given format and returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { - val := k.MustTimeFormat(format) - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InTime always parses with RFC3339 format and returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { - return k.InTimeFormat(time.RFC3339, defaultVal, candidates) -} - -// RangeFloat64 checks if value is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { - val := k.MustFloat64() - if val < min || val > max { - return defaultVal - } - return val -} - -// RangeInt checks if value is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeInt(defaultVal, min, max int) int { - val := k.MustInt() - if val < min || val > max { - return defaultVal - } - return val -} - -// RangeInt64 checks if value is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { - val := k.MustInt64() - if val < min || val > max { - return defaultVal - } - return val -} - -// RangeTimeFormat checks if value with given format is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { - val := k.MustTimeFormat(format) - if val.Unix() < min.Unix() || val.Unix() > max.Unix() { - return defaultVal - } - return val -} - -// RangeTime checks if value with RFC3339 format is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { - return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) -} - -// Strings returns list of string devide by given delimiter. -func (k *Key) Strings(delim string) []string { - str := k.String() - if len(str) == 0 { - return []string{} - } - - vals := strings.Split(str, delim) - for i := range vals { - vals[i] = strings.TrimSpace(vals[i]) - } - return vals -} - -// Float64s returns list of float64 devide by given delimiter. -func (k *Key) Float64s(delim string) []float64 { - strs := k.Strings(delim) - vals := make([]float64, len(strs)) - for i := range strs { - vals[i], _ = strconv.ParseFloat(strs[i], 64) - } - return vals -} - -// Ints returns list of int devide by given delimiter. -func (k *Key) Ints(delim string) []int { - strs := k.Strings(delim) - vals := make([]int, len(strs)) - for i := range strs { - vals[i], _ = strconv.Atoi(strs[i]) - } - return vals -} - -// Int64s returns list of int64 devide by given delimiter. -func (k *Key) Int64s(delim string) []int64 { - strs := k.Strings(delim) - vals := make([]int64, len(strs)) - for i := range strs { - vals[i], _ = strconv.ParseInt(strs[i], 10, 64) - } - return vals -} - -// TimesFormat parses with given format and returns list of time.Time devide by given delimiter. -func (k *Key) TimesFormat(format, delim string) []time.Time { - strs := k.Strings(delim) - vals := make([]time.Time, len(strs)) - for i := range strs { - vals[i], _ = time.Parse(format, strs[i]) - } - return vals -} - -// Times parses with RFC3339 format and returns list of time.Time devide by given delimiter. -func (k *Key) Times(delim string) []time.Time { - return k.TimesFormat(time.RFC3339, delim) -} - -// SetValue changes key value. -func (k *Key) SetValue(v string) { - k.value = v -} - -// _________ __ .__ -// / _____/ ____ _____/ |_|__| ____ ____ -// \_____ \_/ __ \_/ ___\ __\ |/ _ \ / \ -// / \ ___/\ \___| | | ( <_> ) | \ -// /_______ /\___ >\___ >__| |__|\____/|___| / -// \/ \/ \/ \/ - -// Section represents a config section. -type Section struct { - f *File - Comment string - name string - keys map[string]*Key - keyList []string - keysHash map[string]string -} - -func newSection(f *File, name string) *Section { - return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)} -} - -// Name returns name of Section. -func (s *Section) Name() string { - return s.name -} - -// NewKey creates a new key to given section. -func (s *Section) NewKey(name, val string) (*Key, error) { - if len(name) == 0 { - return nil, errors.New("error creating new key: empty key name") - } - - if s.f.BlockMode { - s.f.lock.Lock() - defer s.f.lock.Unlock() - } - - if inSlice(name, s.keyList) { - s.keys[name].value = val - return s.keys[name], nil - } - - s.keyList = append(s.keyList, name) - s.keys[name] = &Key{s, "", name, val, false} - s.keysHash[name] = val - return s.keys[name], nil -} - -// GetKey returns key in section by given name. -func (s *Section) GetKey(name string) (*Key, error) { - // FIXME: change to section level lock? - if s.f.BlockMode { - s.f.lock.RLock() - defer s.f.lock.RUnlock() - } - - key := s.keys[name] - if key == nil { - // Check if it is a child-section. - if i := strings.LastIndex(s.name, "."); i > -1 { - return s.f.Section(s.name[:i]).GetKey(name) - } - return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) - } - return key, nil -} - -// Key assumes named Key exists in section and returns a zero-value when not. -func (s *Section) Key(name string) *Key { - key, err := s.GetKey(name) - if err != nil { - // It's OK here because the only possible error is empty key name, - // but if it's empty, this piece of code won't be executed. - key, _ = s.NewKey(name, "") - return key - } - return key -} - -// Keys returns list of keys of section. -func (s *Section) Keys() []*Key { - keys := make([]*Key, len(s.keyList)) - for i := range s.keyList { - keys[i] = s.Key(s.keyList[i]) - } - return keys -} - -// KeyStrings returns list of key names of section. -func (s *Section) KeyStrings() []string { - list := make([]string, len(s.keyList)) - copy(list, s.keyList) - return list -} - -// KeysHash returns keys hash consisting of names and values. -func (s *Section) KeysHash() map[string]string { - if s.f.BlockMode { - s.f.lock.RLock() - defer s.f.lock.RUnlock() - } - - hash := map[string]string{} - for key, value := range s.keysHash { - hash[key] = value - } - return hash -} - -// DeleteKey deletes a key from section. -func (s *Section) DeleteKey(name string) { - if s.f.BlockMode { - s.f.lock.Lock() - defer s.f.lock.Unlock() - } - - for i, k := range s.keyList { - if k == name { - s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) - delete(s.keys, name) - return - } - } -} - -// ___________.__.__ -// \_ _____/|__| | ____ -// | __) | | | _/ __ \ -// | \ | | |_\ ___/ -// \___ / |__|____/\___ > -// \/ \/ - // File represents a combination of a or more INI file(s) in memory. type File struct { // Should make things safe, but sometimes doesn't matter. @@ -577,16 +126,20 @@ type File struct { // To keep data in order. sectionList []string + options LoadOptions + NameMapper + ValueMapper } // newFile initializes File object with given data sources. -func newFile(dataSources []dataSource) *File { +func newFile(dataSources []dataSource, opts LoadOptions) *File { return &File{ BlockMode: true, dataSources: dataSources, sections: make(map[string]*Section), sectionList: make([]string, 0, 10), + options: opts, } } @@ -601,9 +154,19 @@ func parseDataSource(source interface{}) (dataSource, error) { } } -// Load loads and parses from INI data sources. -// Arguments can be mixed of file name with string type, or raw data in []byte. -func Load(source interface{}, others ...interface{}) (_ *File, err error) { +type LoadOptions struct { + // Loose indicates whether the parser should ignore nonexistent files or return error. + Loose bool + // Insensitive indicates whether the parser forces all section and key names to lowercase. + Insensitive bool + // IgnoreContinuation indicates whether to ignore continuation lines while parsing. + IgnoreContinuation bool + // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. + // This type of keys are mostly used in my.cnf. + AllowBooleanKeys bool +} + +func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { sources := make([]dataSource, len(others)+1) sources[0], err = parseDataSource(source) if err != nil { @@ -615,8 +178,30 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) { return nil, err } } - f := newFile(sources) - return f, f.Reload() + f := newFile(sources, opts) + if err = f.Reload(); err != nil { + return nil, err + } + return f, nil +} + +// Load loads and parses from INI data sources. +// Arguments can be mixed of file name with string type, or raw data in []byte. +// It will return error if list contains nonexistent files. +func Load(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{}, source, others...) +} + +// LooseLoad has exactly same functionality as Load function +// except it ignores nonexistent files instead of returning error. +func LooseLoad(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{Loose: true}, source, others...) +} + +// InsensitiveLoad has exactly same functionality as Load function +// except it forces all section and key names to be lowercased. +func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{Insensitive: true}, source, others...) } // Empty returns an empty file object. @@ -630,6 +215,8 @@ func Empty() *File { func (f *File) NewSection(name string) (*Section, error) { if len(name) == 0 { return nil, errors.New("error creating new section: empty section name") + } else if f.options.Insensitive && name != DEFAULT_SECTION { + name = strings.ToLower(name) } if f.BlockMode { @@ -660,6 +247,8 @@ func (f *File) NewSections(names ...string) (err error) { func (f *File) GetSection(name string) (*Section, error) { if len(name) == 0 { name = DEFAULT_SECTION + } else if f.options.Insensitive { + name = strings.ToLower(name) } if f.BlockMode { @@ -669,7 +258,7 @@ func (f *File) GetSection(name string) (*Section, error) { sec := f.sections[name] if sec == nil { - return nil, fmt.Errorf("error when getting section: section '%s' not exists", name) + return nil, fmt.Errorf("section '%s' does not exist", name) } return sec, nil } @@ -678,7 +267,7 @@ func (f *File) GetSection(name string) (*Section, error) { func (f *File) Section(name string) *Section { sec, err := f.GetSection(name) if err != nil { - // It's OK here because the only possible error is empty section name, + // Note: It's OK here because the only possible error is empty section name, // but if it's empty, this piece of code won't be executed. sec, _ = f.NewSection(name) return sec @@ -722,200 +311,25 @@ func (f *File) DeleteSection(name string) { } } -func cutComment(str string) string { - i := strings.Index(str, "#") - if i == -1 { - return str - } - return str[:i] -} - -// parse parses data through an io.Reader. -func (f *File) parse(reader io.Reader) error { - buf := bufio.NewReader(reader) - - // Handle BOM-UTF8. - // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding - mask, err := buf.Peek(3) - if err == nil && len(mask) >= 3 && mask[0] == 239 && mask[1] == 187 && mask[2] == 191 { - buf.Read(mask) - } - - count := 1 - comments := "" - isEnd := false - - section, err := f.NewSection(DEFAULT_SECTION) +func (f *File) reload(s dataSource) error { + r, err := s.ReadCloser() if err != nil { return err } + defer r.Close() - for { - line, err := buf.ReadString('\n') - line = strings.TrimSpace(line) - length := len(line) - - // Check error and ignore io.EOF just for a moment. - if err != nil { - if err != io.EOF { - return fmt.Errorf("error reading next line: %v", err) - } - // The last line of file could be an empty line. - if length == 0 { - break - } - isEnd = true - } - - // Skip empty lines. - if length == 0 { - continue - } - - switch { - case line[0] == '#' || line[0] == ';': // Comments. - if len(comments) == 0 { - comments = line - } else { - comments += LineBreak + line - } - continue - case line[0] == '[' && line[length-1] == ']': // New sction. - name := strings.TrimSpace(line[1 : length-1]) - section, err = f.NewSection(name) - if err != nil { - return err - } - - if len(comments) > 0 { - section.Comment = comments - comments = "" - } - // Reset counter. - count = 1 - continue - } - - // Other possibilities. - var ( - i int - keyQuote string - kname string - valQuote string - val string - ) - - // Key name surrounded by quotes. - if line[0] == '"' { - if length > 6 && line[0:3] == `"""` { - keyQuote = `"""` - } else { - keyQuote = `"` - } - } else if line[0] == '`' { - keyQuote = "`" - } - if len(keyQuote) > 0 { - qLen := len(keyQuote) - pos := strings.Index(line[qLen:], keyQuote) - if pos == -1 { - return fmt.Errorf("error parsing line: missing closing key quote: %s", line) - } - pos = pos + qLen - i = strings.IndexAny(line[pos:], "=:") - if i < 0 { - return fmt.Errorf("error parsing line: key-value delimiter not found: %s", line) - } else if i == pos { - return fmt.Errorf("error parsing line: key is empty: %s", line) - } - i = i + pos - kname = line[qLen:pos] // Just keep spaces inside quotes. - } else { - i = strings.IndexAny(line, "=:") - if i < 0 { - return fmt.Errorf("error parsing line: key-value delimiter not found: %s", line) - } else if i == 0 { - return fmt.Errorf("error parsing line: key is empty: %s", line) - } - kname = strings.TrimSpace(line[0:i]) - } - - isAutoIncr := false - // Auto increment. - if kname == "-" { - isAutoIncr = true - kname = "#" + fmt.Sprint(count) - count++ - } - - lineRight := strings.TrimSpace(line[i+1:]) - lineRightLength := len(lineRight) - firstChar := "" - if lineRightLength >= 2 { - firstChar = lineRight[0:1] - } - if firstChar == "`" { - valQuote = "`" - } else if lineRightLength >= 6 && lineRight[0:3] == `"""` { - valQuote = `"""` - } - if len(valQuote) > 0 { - qLen := len(valQuote) - pos := strings.LastIndex(lineRight[qLen:], valQuote) - // For multiple lines value. - if pos == -1 { - isEnd := false - val = lineRight[qLen:] + "\n" - for { - next, err := buf.ReadString('\n') - if err != nil { - if err != io.EOF { - return err - } - isEnd = true - } - pos = strings.LastIndex(next, valQuote) - if pos > -1 { - val += next[:pos] - break - } - val += next - if isEnd { - return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next) - } - } - } else { - val = lineRight[qLen : pos+qLen] - } - } else { - val = strings.TrimSpace(cutComment(lineRight[0:])) - } - - k, err := section.NewKey(kname, val) - if err != nil { - return err - } - k.isAutoIncr = isAutoIncr - if len(comments) > 0 { - k.Comment = comments - comments = "" - } - - if isEnd { - break - } - } - return nil + return f.parse(r) } // Reload reloads and parses all data sources. -func (f *File) Reload() error { +func (f *File) Reload() (err error) { for _, s := range f.dataSources { - r, err := s.Reader() - if err != nil { - return err - } - if err = f.parse(r); err != nil { + if err = f.reload(s); err != nil { + // In loose mode, we create an empty default section for nonexistent files. + if os.IsNotExist(err) && f.options.Loose { + f.parse(bytes.NewBuffer(nil)) + continue + } return err } } @@ -939,8 +353,10 @@ func (f *File) Append(source interface{}, others ...interface{}) error { return f.Reload() } -// SaveTo writes content to filesystem. -func (f *File) SaveTo(filename string) (err error) { +// WriteToIndent writes content into io.Writer with given indention. +// If PrettyFormat has been set to be true, +// it will align "=" sign with spaces under each section. +func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { equalSign := "=" if PrettyFormat { equalSign = " = " @@ -955,63 +371,131 @@ func (f *File) SaveTo(filename string) (err error) { sec.Comment = "; " + sec.Comment } if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { - return err + return 0, err } } - if i > 0 { + if i > 0 || DefaultHeader { if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { - return err + return 0, err } } else { - // Write nothing if default section is empty. + // Write nothing if default section is empty if len(sec.keyList) == 0 { continue } } + // Count and generate alignment length and buffer spaces using the + // longest key. Keys may be modifed if they contain certain characters so + // we need to take that into account in our calculation. + alignLength := 0 + if PrettyFormat { + for _, kname := range sec.keyList { + keyLength := len(kname) + // First case will surround key by ` and second by """ + if strings.ContainsAny(kname, "\"=:") { + keyLength += 2 + } else if strings.Contains(kname, "`") { + keyLength += 6 + } + + if keyLength > alignLength { + alignLength = keyLength + } + } + } + alignSpaces := bytes.Repeat([]byte(" "), alignLength) + for _, kname := range sec.keyList { key := sec.Key(kname) if len(key.Comment) > 0 { + if len(indent) > 0 && sname != DEFAULT_SECTION { + buf.WriteString(indent) + } if key.Comment[0] != '#' && key.Comment[0] != ';' { key.Comment = "; " + key.Comment } if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { - return err + return 0, err } } + if len(indent) > 0 && sname != DEFAULT_SECTION { + buf.WriteString(indent) + } + switch { - case key.isAutoIncr: + case key.isAutoIncrement: kname = "-" - case strings.Contains(kname, "`") || strings.Contains(kname, `"`): - kname = `"""` + kname + `"""` - case strings.Contains(kname, `=`) || strings.Contains(kname, `:`): + case strings.ContainsAny(kname, "\"=:"): kname = "`" + kname + "`" + case strings.Contains(kname, "`"): + kname = `"""` + kname + `"""` + } + if _, err = buf.WriteString(kname); err != nil { + return 0, err + } + + if key.isBooleanType { + continue + } + + // Write out alignment spaces before "=" sign + if PrettyFormat { + buf.Write(alignSpaces[:alignLength-len(kname)]) } val := key.value - // In case key value contains "\n", "`" or "\"". - if strings.Contains(val, "\n") || strings.Contains(val, "`") || strings.Contains(val, `"`) { + // In case key value contains "\n", "`", "\"", "#" or ";" + if strings.ContainsAny(val, "\n`") { val = `"""` + val + `"""` + } else if strings.ContainsAny(val, "#;") { + val = "`" + val + "`" } - if _, err = buf.WriteString(kname + equalSign + val + LineBreak); err != nil { - return err + if _, err = buf.WriteString(equalSign + val + LineBreak); err != nil { + return 0, err } } - // Put a line between sections. + // Put a line between sections if _, err = buf.WriteString(LineBreak); err != nil { - return err + return 0, err } } - fw, err := os.Create(filename) + return buf.WriteTo(w) +} + +// WriteTo writes file content into io.Writer. +func (f *File) WriteTo(w io.Writer) (int64, error) { + return f.WriteToIndent(w, "") +} + +// SaveToIndent writes content to file system with given value indention. +func (f *File) SaveToIndent(filename, indent string) error { + // Note: Because we are truncating with os.Create, + // so it's safer to save to a temporary file location and rename afte done. + tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp" + defer os.Remove(tmpPath) + + fw, err := os.Create(tmpPath) if err != nil { return err } - if _, err = buf.WriteTo(fw); err != nil { + + if _, err = f.WriteToIndent(fw, indent); err != nil { + fw.Close() return err } - return fw.Close() + fw.Close() + + // Remove old file and rename the new one. + os.Remove(filename) + return os.Rename(tmpPath, filename) +} + +// SaveTo writes content to file system. +func (f *File) SaveTo(filename string) error { + return f.SaveToIndent(filename, "") } diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go b/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go deleted file mode 100644 index c6daf81ca52..00000000000 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright 2014 Unknwon -// -// 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 ini - -import ( - "fmt" - "strings" - "testing" - "time" - - . "github.com/smartystreets/goconvey/convey" -) - -func Test_Version(t *testing.T) { - Convey("Get version", t, func() { - So(Version(), ShouldEqual, _VERSION) - }) -} - -const _CONF_DATA = ` -; Package name -NAME = ini -; Package version -VERSION = v1 -; Package import path -IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s - -# Information about package author -# Bio can be written in multiple lines. -[author] -NAME = Unknwon # Succeeding comment -E-MAIL = fake@localhost -GITHUB = https://github.com/%(NAME)s -BIO = """Gopher. -Coding addict. -Good man. -""" # Succeeding comment - -[package] -CLONE_URL = https://%(IMPORT_PATH)s - -[package.sub] -UNUSED_KEY = should be deleted - -[features] --: Support read/write comments of keys and sections --: Support auto-increment of key names --: Support load multiple files to overwrite key values - -[types] -STRING = str -BOOL = true -BOOL_FALSE = false -FLOAT64 = 1.25 -INT = 10 -TIME = 2015-01-01T20:17:05Z - -[array] -STRINGS = en, zh, de -FLOAT64S = 1.1, 2.2, 3.3 -INTS = 1, 2, 3 -TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z - -[note] - -[advance] -true = """"2+3=5"""" -"1+1=2" = true -"""6+1=7""" = true -"""` + "`" + `5+5` + "`" + `""" = 10 -""""6+6"""" = 12 -` + "`" + `7-2=4` + "`" + ` = false -ADDRESS = ` + "`" + `404 road, -NotFound, State, 50000` + "`" - -func Test_Load(t *testing.T) { - Convey("Load from data sources", t, func() { - - Convey("Load with empty data", func() { - So(Empty(), ShouldNotBeNil) - }) - - Convey("Load with multiple data sources", func() { - cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - }) - }) - - Convey("Bad load process", t, func() { - - Convey("Load from invalid data sources", func() { - _, err := Load(_CONF_DATA) - So(err, ShouldNotBeNil) - - _, err = Load("testdata/404.ini") - So(err, ShouldNotBeNil) - - _, err = Load(1) - So(err, ShouldNotBeNil) - - _, err = Load([]byte(""), 1) - So(err, ShouldNotBeNil) - }) - - Convey("Load with empty section name", func() { - _, err := Load([]byte("[]")) - So(err, ShouldNotBeNil) - }) - - Convey("Load with bad keys", func() { - _, err := Load([]byte(`"""name`)) - So(err, ShouldNotBeNil) - - _, err = Load([]byte(`"""name"""`)) - So(err, ShouldNotBeNil) - - _, err = Load([]byte(`""=1`)) - So(err, ShouldNotBeNil) - - _, err = Load([]byte(`=`)) - So(err, ShouldNotBeNil) - - _, err = Load([]byte(`name`)) - So(err, ShouldNotBeNil) - }) - - Convey("Load with bad values", func() { - _, err := Load([]byte(`name="""Unknwon`)) - So(err, ShouldNotBeNil) - }) - }) -} - -func Test_Values(t *testing.T) { - Convey("Test getting and setting values", t, func() { - cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - Convey("Get values in default section", func() { - sec := cfg.Section("") - So(sec, ShouldNotBeNil) - So(sec.Key("NAME").Value(), ShouldEqual, "ini") - So(sec.Key("NAME").String(), ShouldEqual, "ini") - So(sec.Key("NAME").Comment, ShouldEqual, "; Package name") - So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1") - }) - - Convey("Get values in non-default section", func() { - sec := cfg.Section("author") - So(sec, ShouldNotBeNil) - So(sec.Key("NAME").String(), ShouldEqual, "Unknwon") - So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon") - - sec = cfg.Section("package") - So(sec, ShouldNotBeNil) - So(sec.Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") - }) - - Convey("Get auto-increment key names", func() { - keys := cfg.Section("features").Keys() - for i, k := range keys { - So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1)) - } - }) - - Convey("Get overwrite value", func() { - So(cfg.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") - }) - - Convey("Get sections", func() { - sections := cfg.Sections() - for i, name := range []string{DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "advance"} { - So(sections[i].Name(), ShouldEqual, name) - } - }) - - Convey("Get parent section value", func() { - So(cfg.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") - }) - - Convey("Get multiple line value", func() { - So(cfg.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n") - }) - - Convey("Get values with type", func() { - sec := cfg.Section("types") - v1, err := sec.Key("BOOL").Bool() - So(err, ShouldBeNil) - So(v1, ShouldBeTrue) - - v1, err = sec.Key("BOOL_FALSE").Bool() - So(err, ShouldBeNil) - So(v1, ShouldBeFalse) - - v2, err := sec.Key("FLOAT64").Float64() - So(err, ShouldBeNil) - So(v2, ShouldEqual, 1.25) - - v3, err := sec.Key("INT").Int() - So(err, ShouldBeNil) - So(v3, ShouldEqual, 10) - - v4, err := sec.Key("INT").Int64() - So(err, ShouldBeNil) - So(v4, ShouldEqual, 10) - - t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") - So(err, ShouldBeNil) - v5, err := sec.Key("TIME").Time() - So(err, ShouldBeNil) - So(v5.String(), ShouldEqual, t.String()) - - Convey("Must get values with type", func() { - So(sec.Key("STRING").MustString("404"), ShouldEqual, "str") - So(sec.Key("BOOL").MustBool(), ShouldBeTrue) - So(sec.Key("FLOAT64").MustFloat64(), ShouldEqual, 1.25) - So(sec.Key("INT").MustInt(), ShouldEqual, 10) - So(sec.Key("INT").MustInt64(), ShouldEqual, 10) - So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String()) - - Convey("Must get values with default value", func() { - So(sec.Key("STRING_404").MustString("404"), ShouldEqual, "404") - So(sec.Key("BOOL_404").MustBool(true), ShouldBeTrue) - So(sec.Key("FLOAT64_404").MustFloat64(2.5), ShouldEqual, 2.5) - So(sec.Key("INT_404").MustInt(15), ShouldEqual, 15) - So(sec.Key("INT_404").MustInt64(15), ShouldEqual, 15) - - t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z") - So(err, ShouldBeNil) - So(sec.Key("TIME_404").MustTime(t).String(), ShouldEqual, t.String()) - }) - }) - }) - - Convey("Get value with candidates", func() { - sec := cfg.Section("types") - So(sec.Key("STRING").In("", []string{"str", "arr", "types"}), ShouldEqual, "str") - So(sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25) - So(sec.Key("INT").InInt(0, []int{10, 20, 30}), ShouldEqual, 10) - So(sec.Key("INT").InInt64(0, []int64{10, 20, 30}), ShouldEqual, 10) - - zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") - So(err, ShouldBeNil) - t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") - So(err, ShouldBeNil) - So(sec.Key("TIME").InTime(zt, []time.Time{t, time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String()) - - Convey("Get value with candidates and default value", func() { - So(sec.Key("STRING_404").In("str", []string{"str", "arr", "types"}), ShouldEqual, "str") - So(sec.Key("FLOAT64_404").InFloat64(1.25, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25) - So(sec.Key("INT_404").InInt(10, []int{10, 20, 30}), ShouldEqual, 10) - So(sec.Key("INT64_404").InInt64(10, []int64{10, 20, 30}), ShouldEqual, 10) - So(sec.Key("TIME_404").InTime(t, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String()) - }) - }) - - Convey("Get values in range", func() { - sec := cfg.Section("types") - So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25) - So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10) - So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10) - - minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") - So(err, ShouldBeNil) - midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z") - So(err, ShouldBeNil) - maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z") - So(err, ShouldBeNil) - t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") - So(err, ShouldBeNil) - So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String()) - - Convey("Get value in range with default value", func() { - So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5) - So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7) - So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7) - So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String()) - }) - }) - - Convey("Get values into slice", func() { - sec := cfg.Section("array") - So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de") - So(len(sec.Key("STRINGS_404").Strings(",")), ShouldEqual, 0) - - vals1 := sec.Key("FLOAT64S").Float64s(",") - for i, v := range []float64{1.1, 2.2, 3.3} { - So(vals1[i], ShouldEqual, v) - } - - vals2 := sec.Key("INTS").Ints(",") - for i, v := range []int{1, 2, 3} { - So(vals2[i], ShouldEqual, v) - } - - vals3 := sec.Key("INTS").Int64s(",") - for i, v := range []int64{1, 2, 3} { - So(vals3[i], ShouldEqual, v) - } - - t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") - So(err, ShouldBeNil) - vals4 := sec.Key("TIMES").Times(",") - for i, v := range []time.Time{t, t, t} { - So(vals4[i].String(), ShouldEqual, v.String()) - } - }) - - Convey("Get key hash", func() { - cfg.Section("").KeysHash() - }) - - Convey("Set key value", func() { - k := cfg.Section("author").Key("NAME") - k.SetValue("无闻") - So(k.String(), ShouldEqual, "无闻") - }) - - Convey("Get key strings", func() { - So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME") - }) - - Convey("Delete a key", func() { - cfg.Section("package.sub").DeleteKey("UNUSED_KEY") - _, err := cfg.Section("package.sub").GetKey("UNUSED_KEY") - So(err, ShouldNotBeNil) - }) - - Convey("Get section strings", func() { - So(strings.Join(cfg.SectionStrings(), ","), ShouldEqual, "DEFAULT,author,package,package.sub,features,types,array,note,advance") - }) - - Convey("Delete a section", func() { - cfg.DeleteSection("") - So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION) - }) - - Convey("Create new sections", func() { - cfg.NewSections("test", "test2") - _, err := cfg.GetSection("test") - So(err, ShouldBeNil) - _, err = cfg.GetSection("test2") - So(err, ShouldBeNil) - }) - }) - - Convey("Test getting and setting bad values", t, func() { - cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - Convey("Create new key with empty name", func() { - k, err := cfg.Section("").NewKey("", "") - So(err, ShouldNotBeNil) - So(k, ShouldBeNil) - }) - - Convey("Create new section with empty name", func() { - s, err := cfg.NewSection("") - So(err, ShouldNotBeNil) - So(s, ShouldBeNil) - }) - - Convey("Create new sections with empty name", func() { - So(cfg.NewSections(""), ShouldNotBeNil) - }) - - Convey("Get section that not exists", func() { - s, err := cfg.GetSection("404") - So(err, ShouldNotBeNil) - So(s, ShouldBeNil) - - s = cfg.Section("404") - So(s, ShouldNotBeNil) - }) - }) -} - -func Test_File_Append(t *testing.T) { - Convey("Append data sources", t, func() { - cfg, err := Load([]byte("")) - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - So(cfg.Append([]byte(""), []byte("")), ShouldBeNil) - - Convey("Append bad data sources", func() { - So(cfg.Append(1), ShouldNotBeNil) - So(cfg.Append([]byte(""), 1), ShouldNotBeNil) - }) - }) -} - -func Test_File_SaveTo(t *testing.T) { - Convey("Save file", t, func() { - cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - cfg.Section("").Key("NAME").Comment = "Package name" - cfg.Section("author").Comment = `Information about package author -# Bio can be written in multiple lines.` - So(cfg.SaveTo("testdata/conf_out.ini"), ShouldBeNil) - }) -} - -func Benchmark_Key_Value(b *testing.B) { - c, _ := Load([]byte(_CONF_DATA)) - for i := 0; i < b.N; i++ { - c.Section("").Key("NAME").Value() - } -} - -func Benchmark_Key_String(b *testing.B) { - c, _ := Load([]byte(_CONF_DATA)) - for i := 0; i < b.N; i++ { - c.Section("").Key("NAME").String() - } -} - -func Benchmark_Key_Value_NonBlock(b *testing.B) { - c, _ := Load([]byte(_CONF_DATA)) - c.BlockMode = false - for i := 0; i < b.N; i++ { - c.Section("").Key("NAME").Value() - } -} - -func Benchmark_Key_String_NonBlock(b *testing.B) { - c, _ := Load([]byte(_CONF_DATA)) - c.BlockMode = false - for i := 0; i < b.N; i++ { - c.Section("").Key("NAME").String() - } -} - -func Benchmark_Key_SetValue(b *testing.B) { - c, _ := Load([]byte(_CONF_DATA)) - for i := 0; i < b.N; i++ { - c.Section("").Key("NAME").SetValue("10") - } -} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/key.go b/Godeps/_workspace/src/gopkg.in/ini.v1/key.go new file mode 100644 index 00000000000..9738c55a21b --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/key.go @@ -0,0 +1,633 @@ +// Copyright 2014 Unknwon +// +// 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 ini + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// Key represents a key under a section. +type Key struct { + s *Section + name string + value string + isAutoIncrement bool + isBooleanType bool + + Comment string +} + +// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv +type ValueMapper func(string) string + +// Name returns name of key. +func (k *Key) Name() string { + return k.name +} + +// Value returns raw value of key for performance purpose. +func (k *Key) Value() string { + return k.value +} + +// String returns string representation of value. +func (k *Key) String() string { + val := k.value + if k.s.f.ValueMapper != nil { + val = k.s.f.ValueMapper(val) + } + if strings.Index(val, "%") == -1 { + return val + } + + for i := 0; i < _DEPTH_VALUES; i++ { + vr := varPattern.FindString(val) + if len(vr) == 0 { + break + } + + // Take off leading '%(' and trailing ')s'. + noption := strings.TrimLeft(vr, "%(") + noption = strings.TrimRight(noption, ")s") + + // Search in the same section. + nk, err := k.s.GetKey(noption) + if err != nil { + // Search again in default section. + nk, _ = k.s.f.Section("").GetKey(noption) + } + + // Substitute by new value and take off leading '%(' and trailing ')s'. + val = strings.Replace(val, vr, nk.value, -1) + } + return val +} + +// Validate accepts a validate function which can +// return modifed result as key value. +func (k *Key) Validate(fn func(string) string) string { + return fn(k.String()) +} + +// parseBool returns the boolean value represented by the string. +// +// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On, +// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off. +// Any other value returns an error. +func parseBool(str string) (value bool, err error) { + switch str { + case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On": + return true, nil + case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off": + return false, nil + } + return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) +} + +// Bool returns bool type value. +func (k *Key) Bool() (bool, error) { + return parseBool(k.String()) +} + +// Float64 returns float64 type value. +func (k *Key) Float64() (float64, error) { + return strconv.ParseFloat(k.String(), 64) +} + +// Int returns int type value. +func (k *Key) Int() (int, error) { + return strconv.Atoi(k.String()) +} + +// Int64 returns int64 type value. +func (k *Key) Int64() (int64, error) { + return strconv.ParseInt(k.String(), 10, 64) +} + +// Uint returns uint type valued. +func (k *Key) Uint() (uint, error) { + u, e := strconv.ParseUint(k.String(), 10, 64) + return uint(u), e +} + +// Uint64 returns uint64 type value. +func (k *Key) Uint64() (uint64, error) { + return strconv.ParseUint(k.String(), 10, 64) +} + +// Duration returns time.Duration type value. +func (k *Key) Duration() (time.Duration, error) { + return time.ParseDuration(k.String()) +} + +// TimeFormat parses with given format and returns time.Time type value. +func (k *Key) TimeFormat(format string) (time.Time, error) { + return time.Parse(format, k.String()) +} + +// Time parses with RFC3339 format and returns time.Time type value. +func (k *Key) Time() (time.Time, error) { + return k.TimeFormat(time.RFC3339) +} + +// MustString returns default value if key value is empty. +func (k *Key) MustString(defaultVal string) string { + val := k.String() + if len(val) == 0 { + k.value = defaultVal + return defaultVal + } + return val +} + +// MustBool always returns value without error, +// it returns false if error occurs. +func (k *Key) MustBool(defaultVal ...bool) bool { + val, err := k.Bool() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatBool(defaultVal[0]) + return defaultVal[0] + } + return val +} + +// MustFloat64 always returns value without error, +// it returns 0.0 if error occurs. +func (k *Key) MustFloat64(defaultVal ...float64) float64 { + val, err := k.Float64() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64) + return defaultVal[0] + } + return val +} + +// MustInt always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustInt(defaultVal ...int) int { + val, err := k.Int() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatInt(int64(defaultVal[0]), 10) + return defaultVal[0] + } + return val +} + +// MustInt64 always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustInt64(defaultVal ...int64) int64 { + val, err := k.Int64() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatInt(defaultVal[0], 10) + return defaultVal[0] + } + return val +} + +// MustUint always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustUint(defaultVal ...uint) uint { + val, err := k.Uint() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatUint(uint64(defaultVal[0]), 10) + return defaultVal[0] + } + return val +} + +// MustUint64 always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustUint64(defaultVal ...uint64) uint64 { + val, err := k.Uint64() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatUint(defaultVal[0], 10) + return defaultVal[0] + } + return val +} + +// MustDuration always returns value without error, +// it returns zero value if error occurs. +func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration { + val, err := k.Duration() + if len(defaultVal) > 0 && err != nil { + k.value = defaultVal[0].String() + return defaultVal[0] + } + return val +} + +// MustTimeFormat always parses with given format and returns value without error, +// it returns zero value if error occurs. +func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { + val, err := k.TimeFormat(format) + if len(defaultVal) > 0 && err != nil { + k.value = defaultVal[0].Format(format) + return defaultVal[0] + } + return val +} + +// MustTime always parses with RFC3339 format and returns value without error, +// it returns zero value if error occurs. +func (k *Key) MustTime(defaultVal ...time.Time) time.Time { + return k.MustTimeFormat(time.RFC3339, defaultVal...) +} + +// In always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) In(defaultVal string, candidates []string) string { + val := k.String() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InFloat64 always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { + val := k.MustFloat64() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InInt always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InInt(defaultVal int, candidates []int) int { + val := k.MustInt() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InInt64 always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { + val := k.MustInt64() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InUint always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InUint(defaultVal uint, candidates []uint) uint { + val := k.MustUint() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InUint64 always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { + val := k.MustUint64() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InTimeFormat always parses with given format and returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { + val := k.MustTimeFormat(format) + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InTime always parses with RFC3339 format and returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { + return k.InTimeFormat(time.RFC3339, defaultVal, candidates) +} + +// RangeFloat64 checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { + val := k.MustFloat64() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeInt checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeInt(defaultVal, min, max int) int { + val := k.MustInt() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeInt64 checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { + val := k.MustInt64() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeTimeFormat checks if value with given format is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { + val := k.MustTimeFormat(format) + if val.Unix() < min.Unix() || val.Unix() > max.Unix() { + return defaultVal + } + return val +} + +// RangeTime checks if value with RFC3339 format is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { + return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) +} + +// Strings returns list of string divided by given delimiter. +func (k *Key) Strings(delim string) []string { + str := k.String() + if len(str) == 0 { + return []string{} + } + + vals := strings.Split(str, delim) + for i := range vals { + vals[i] = strings.TrimSpace(vals[i]) + } + return vals +} + +// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Float64s(delim string) []float64 { + vals, _ := k.getFloat64s(delim, true, false) + return vals +} + +// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Ints(delim string) []int { + vals, _ := k.getInts(delim, true, false) + return vals +} + +// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Int64s(delim string) []int64 { + vals, _ := k.getInt64s(delim, true, false) + return vals +} + +// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Uints(delim string) []uint { + vals, _ := k.getUints(delim, true, false) + return vals +} + +// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Uint64s(delim string) []uint64 { + vals, _ := k.getUint64s(delim, true, false) + return vals +} + +// TimesFormat parses with given format and returns list of time.Time divided by given delimiter. +// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). +func (k *Key) TimesFormat(format, delim string) []time.Time { + vals, _ := k.getTimesFormat(format, delim, true, false) + return vals +} + +// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter. +// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). +func (k *Key) Times(delim string) []time.Time { + return k.TimesFormat(time.RFC3339, delim) +} + +// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then +// it will not be included to result list. +func (k *Key) ValidFloat64s(delim string) []float64 { + vals, _ := k.getFloat64s(delim, false, false) + return vals +} + +// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will +// not be included to result list. +func (k *Key) ValidInts(delim string) []int { + vals, _ := k.getInts(delim, false, false) + return vals +} + +// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, +// then it will not be included to result list. +func (k *Key) ValidInt64s(delim string) []int64 { + vals, _ := k.getInt64s(delim, false, false) + return vals +} + +// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, +// then it will not be included to result list. +func (k *Key) ValidUints(delim string) []uint { + vals, _ := k.getUints(delim, false, false) + return vals +} + +// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned +// integer, then it will not be included to result list. +func (k *Key) ValidUint64s(delim string) []uint64 { + vals, _ := k.getUint64s(delim, false, false) + return vals +} + +// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. +func (k *Key) ValidTimesFormat(format, delim string) []time.Time { + vals, _ := k.getTimesFormat(format, delim, false, false) + return vals +} + +// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter. +func (k *Key) ValidTimes(delim string) []time.Time { + return k.ValidTimesFormat(time.RFC3339, delim) +} + +// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. +func (k *Key) StrictFloat64s(delim string) ([]float64, error) { + return k.getFloat64s(delim, false, true) +} + +// StrictInts returns list of int divided by given delimiter or error on first invalid input. +func (k *Key) StrictInts(delim string) ([]int, error) { + return k.getInts(delim, false, true) +} + +// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. +func (k *Key) StrictInt64s(delim string) ([]int64, error) { + return k.getInt64s(delim, false, true) +} + +// StrictUints returns list of uint divided by given delimiter or error on first invalid input. +func (k *Key) StrictUints(delim string) ([]uint, error) { + return k.getUints(delim, false, true) +} + +// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. +func (k *Key) StrictUint64s(delim string) ([]uint64, error) { + return k.getUint64s(delim, false, true) +} + +// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter +// or error on first invalid input. +func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { + return k.getTimesFormat(format, delim, false, true) +} + +// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter +// or error on first invalid input. +func (k *Key) StrictTimes(delim string) ([]time.Time, error) { + return k.StrictTimesFormat(time.RFC3339, delim) +} + +// getFloat64s returns list of float64 divided by given delimiter. +func (k *Key) getFloat64s(delim string, addInvalid, returnOnInvalid bool) ([]float64, error) { + strs := k.Strings(delim) + vals := make([]float64, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseFloat(str, 64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// getInts returns list of int divided by given delimiter. +func (k *Key) getInts(delim string, addInvalid, returnOnInvalid bool) ([]int, error) { + strs := k.Strings(delim) + vals := make([]int, 0, len(strs)) + for _, str := range strs { + val, err := strconv.Atoi(str) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// getInt64s returns list of int64 divided by given delimiter. +func (k *Key) getInt64s(delim string, addInvalid, returnOnInvalid bool) ([]int64, error) { + strs := k.Strings(delim) + vals := make([]int64, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseInt(str, 10, 64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// getUints returns list of uint divided by given delimiter. +func (k *Key) getUints(delim string, addInvalid, returnOnInvalid bool) ([]uint, error) { + strs := k.Strings(delim) + vals := make([]uint, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseUint(str, 10, 0) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, uint(val)) + } + } + return vals, nil +} + +// getUint64s returns list of uint64 divided by given delimiter. +func (k *Key) getUint64s(delim string, addInvalid, returnOnInvalid bool) ([]uint64, error) { + strs := k.Strings(delim) + vals := make([]uint64, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseUint(str, 10, 64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// getTimesFormat parses with given format and returns list of time.Time divided by given delimiter. +func (k *Key) getTimesFormat(format, delim string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { + strs := k.Strings(delim) + vals := make([]time.Time, 0, len(strs)) + for _, str := range strs { + val, err := time.Parse(format, str) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// SetValue changes key value. +func (k *Key) SetValue(v string) { + if k.s.f.BlockMode { + k.s.f.lock.Lock() + defer k.s.f.lock.Unlock() + } + + k.value = v + k.s.keysHash[k.name] = v +} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/parser.go b/Godeps/_workspace/src/gopkg.in/ini.v1/parser.go new file mode 100644 index 00000000000..dc6df87a6c3 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/parser.go @@ -0,0 +1,325 @@ +// Copyright 2015 Unknwon +// +// 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 ini + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + "strings" + "unicode" +) + +type tokenType int + +const ( + _TOKEN_INVALID tokenType = iota + _TOKEN_COMMENT + _TOKEN_SECTION + _TOKEN_KEY +) + +type parser struct { + buf *bufio.Reader + isEOF bool + count int + comment *bytes.Buffer +} + +func newParser(r io.Reader) *parser { + return &parser{ + buf: bufio.NewReader(r), + count: 1, + comment: &bytes.Buffer{}, + } +} + +// BOM handles header of BOM-UTF8 format. +// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding +func (p *parser) BOM() error { + mask, err := p.buf.Peek(3) + if err != nil && err != io.EOF { + return err + } else if len(mask) < 3 { + return nil + } else if mask[0] == 239 && mask[1] == 187 && mask[2] == 191 { + p.buf.Read(mask) + } + return nil +} + +func (p *parser) readUntil(delim byte) ([]byte, error) { + data, err := p.buf.ReadBytes(delim) + if err != nil { + if err == io.EOF { + p.isEOF = true + } else { + return nil, err + } + } + return data, nil +} + +func cleanComment(in []byte) ([]byte, bool) { + i := bytes.IndexAny(in, "#;") + if i == -1 { + return nil, false + } + return in[i:], true +} + +func readKeyName(in []byte) (string, int, error) { + line := string(in) + + // Check if key name surrounded by quotes. + var keyQuote string + if line[0] == '"' { + if len(line) > 6 && string(line[0:3]) == `"""` { + keyQuote = `"""` + } else { + keyQuote = `"` + } + } else if line[0] == '`' { + keyQuote = "`" + } + + // Get out key name + endIdx := -1 + if len(keyQuote) > 0 { + startIdx := len(keyQuote) + // FIXME: fail case -> """"""name"""=value + pos := strings.Index(line[startIdx:], keyQuote) + if pos == -1 { + return "", -1, fmt.Errorf("missing closing key quote: %s", line) + } + pos += startIdx + + // Find key-value delimiter + i := strings.IndexAny(line[pos+startIdx:], "=:") + if i < 0 { + return "", -1, ErrDelimiterNotFound{line} + } + endIdx = pos + i + return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil + } + + endIdx = strings.IndexAny(line, "=:") + if endIdx < 0 { + return "", -1, ErrDelimiterNotFound{line} + } + return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil +} + +func (p *parser) readMultilines(line, val, valQuote string) (string, error) { + for { + data, err := p.readUntil('\n') + if err != nil { + return "", err + } + next := string(data) + + pos := strings.LastIndex(next, valQuote) + if pos > -1 { + val += next[:pos] + + comment, has := cleanComment([]byte(next[pos:])) + if has { + p.comment.Write(bytes.TrimSpace(comment)) + } + break + } + val += next + if p.isEOF { + return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) + } + } + return val, nil +} + +func (p *parser) readContinuationLines(val string) (string, error) { + for { + data, err := p.readUntil('\n') + if err != nil { + return "", err + } + next := strings.TrimSpace(string(data)) + + if len(next) == 0 { + break + } + val += next + if val[len(val)-1] != '\\' { + break + } + val = val[:len(val)-1] + } + return val, nil +} + +// hasSurroundedQuote check if and only if the first and last characters +// are quotes \" or \'. +// It returns false if any other parts also contain same kind of quotes. +func hasSurroundedQuote(in string, quote byte) bool { + return len(in) > 2 && in[0] == quote && in[len(in)-1] == quote && + strings.IndexByte(in[1:], quote) == len(in)-2 +} + +func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) { + line := strings.TrimLeftFunc(string(in), unicode.IsSpace) + if len(line) == 0 { + return "", nil + } + + var valQuote string + if len(line) > 3 && string(line[0:3]) == `"""` { + valQuote = `"""` + } else if line[0] == '`' { + valQuote = "`" + } + + if len(valQuote) > 0 { + startIdx := len(valQuote) + pos := strings.LastIndex(line[startIdx:], valQuote) + // Check for multi-line value + if pos == -1 { + return p.readMultilines(line, line[startIdx:], valQuote) + } + + return line[startIdx : pos+startIdx], nil + } + + // Won't be able to reach here if value only contains whitespace. + line = strings.TrimSpace(line) + + // Check continuation lines when desired. + if !ignoreContinuation && line[len(line)-1] == '\\' { + return p.readContinuationLines(line[:len(line)-1]) + } + + i := strings.IndexAny(line, "#;") + if i > -1 { + p.comment.WriteString(line[i:]) + line = strings.TrimSpace(line[:i]) + } + + // Trim single quotes + if hasSurroundedQuote(line, '\'') || + hasSurroundedQuote(line, '"') { + line = line[1 : len(line)-1] + } + return line, nil +} + +// parse parses data through an io.Reader. +func (f *File) parse(reader io.Reader) (err error) { + p := newParser(reader) + if err = p.BOM(); err != nil { + return fmt.Errorf("BOM: %v", err) + } + + // Ignore error because default section name is never empty string. + section, _ := f.NewSection(DEFAULT_SECTION) + + var line []byte + for !p.isEOF { + line, err = p.readUntil('\n') + if err != nil { + return err + } + + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + if len(line) == 0 { + continue + } + + // Comments + if line[0] == '#' || line[0] == ';' { + // Note: we do not care ending line break, + // it is needed for adding second line, + // so just clean it once at the end when set to value. + p.comment.Write(line) + continue + } + + // Section + if line[0] == '[' { + // Read to the next ']' (TODO: support quoted strings) + // TODO(unknwon): use LastIndexByte when stop supporting Go1.4 + closeIdx := bytes.LastIndex(line, []byte("]")) + if closeIdx == -1 { + return fmt.Errorf("unclosed section: %s", line) + } + + name := string(line[1:closeIdx]) + section, err = f.NewSection(name) + if err != nil { + return err + } + + comment, has := cleanComment(line[closeIdx+1:]) + if has { + p.comment.Write(comment) + } + + section.Comment = strings.TrimSpace(p.comment.String()) + + // Reset aotu-counter and comments + p.comment.Reset() + p.count = 1 + continue + } + + kname, offset, err := readKeyName(line) + if err != nil { + // Treat as boolean key when desired, and whole line is key name. + if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { + key, err := section.NewKey(string(line), "true") + if err != nil { + return err + } + key.isBooleanType = true + key.Comment = strings.TrimSpace(p.comment.String()) + p.comment.Reset() + continue + } + return err + } + + // Auto increment. + isAutoIncr := false + if kname == "-" { + isAutoIncr = true + kname = "#" + strconv.Itoa(p.count) + p.count++ + } + + key, err := section.NewKey(kname, "") + if err != nil { + return err + } + key.isAutoIncrement = isAutoIncr + + value, err := p.readValue(line[offset:], f.options.IgnoreContinuation) + if err != nil { + return err + } + key.SetValue(value) + key.Comment = strings.TrimSpace(p.comment.String()) + p.comment.Reset() + } + return nil +} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/section.go b/Godeps/_workspace/src/gopkg.in/ini.v1/section.go new file mode 100644 index 00000000000..bbb73caf8cc --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/section.go @@ -0,0 +1,206 @@ +// Copyright 2014 Unknwon +// +// 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 ini + +import ( + "errors" + "fmt" + "strings" +) + +// Section represents a config section. +type Section struct { + f *File + Comment string + name string + keys map[string]*Key + keyList []string + keysHash map[string]string +} + +func newSection(f *File, name string) *Section { + return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)} +} + +// Name returns name of Section. +func (s *Section) Name() string { + return s.name +} + +// NewKey creates a new key to given section. +func (s *Section) NewKey(name, val string) (*Key, error) { + if len(name) == 0 { + return nil, errors.New("error creating new key: empty key name") + } else if s.f.options.Insensitive { + name = strings.ToLower(name) + } + + if s.f.BlockMode { + s.f.lock.Lock() + defer s.f.lock.Unlock() + } + + if inSlice(name, s.keyList) { + s.keys[name].value = val + return s.keys[name], nil + } + + s.keyList = append(s.keyList, name) + s.keys[name] = &Key{ + s: s, + name: name, + value: val, + } + s.keysHash[name] = val + return s.keys[name], nil +} + +// GetKey returns key in section by given name. +func (s *Section) GetKey(name string) (*Key, error) { + // FIXME: change to section level lock? + if s.f.BlockMode { + s.f.lock.RLock() + } + if s.f.options.Insensitive { + name = strings.ToLower(name) + } + key := s.keys[name] + if s.f.BlockMode { + s.f.lock.RUnlock() + } + + if key == nil { + // Check if it is a child-section. + sname := s.name + for { + if i := strings.LastIndex(sname, "."); i > -1 { + sname = sname[:i] + sec, err := s.f.GetSection(sname) + if err != nil { + continue + } + return sec.GetKey(name) + } else { + break + } + } + return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) + } + return key, nil +} + +// HasKey returns true if section contains a key with given name. +func (s *Section) HasKey(name string) bool { + key, _ := s.GetKey(name) + return key != nil +} + +// Haskey is a backwards-compatible name for HasKey. +func (s *Section) Haskey(name string) bool { + return s.HasKey(name) +} + +// HasValue returns true if section contains given raw value. +func (s *Section) HasValue(value string) bool { + if s.f.BlockMode { + s.f.lock.RLock() + defer s.f.lock.RUnlock() + } + + for _, k := range s.keys { + if value == k.value { + return true + } + } + return false +} + +// Key assumes named Key exists in section and returns a zero-value when not. +func (s *Section) Key(name string) *Key { + key, err := s.GetKey(name) + if err != nil { + // It's OK here because the only possible error is empty key name, + // but if it's empty, this piece of code won't be executed. + key, _ = s.NewKey(name, "") + return key + } + return key +} + +// Keys returns list of keys of section. +func (s *Section) Keys() []*Key { + keys := make([]*Key, len(s.keyList)) + for i := range s.keyList { + keys[i] = s.Key(s.keyList[i]) + } + return keys +} + +// ParentKeys returns list of keys of parent section. +func (s *Section) ParentKeys() []*Key { + var parentKeys []*Key + sname := s.name + for { + if i := strings.LastIndex(sname, "."); i > -1 { + sname = sname[:i] + sec, err := s.f.GetSection(sname) + if err != nil { + continue + } + parentKeys = append(parentKeys, sec.Keys()...) + } else { + break + } + + } + return parentKeys +} + +// KeyStrings returns list of key names of section. +func (s *Section) KeyStrings() []string { + list := make([]string, len(s.keyList)) + copy(list, s.keyList) + return list +} + +// KeysHash returns keys hash consisting of names and values. +func (s *Section) KeysHash() map[string]string { + if s.f.BlockMode { + s.f.lock.RLock() + defer s.f.lock.RUnlock() + } + + hash := map[string]string{} + for key, value := range s.keysHash { + hash[key] = value + } + return hash +} + +// DeleteKey deletes a key from section. +func (s *Section) DeleteKey(name string) { + if s.f.BlockMode { + s.f.lock.Lock() + defer s.f.lock.Unlock() + } + + for i, k := range s.keyList { + if k == name { + s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) + delete(s.keys, name) + return + } + } +} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go b/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go index 09ea816442b..d00fb4b8373 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go @@ -15,9 +15,11 @@ package ini import ( + "bytes" "errors" "fmt" "reflect" + "strings" "time" "unicode" ) @@ -75,11 +77,64 @@ func parseDelim(actual string) string { var reflectTime = reflect.TypeOf(time.Now()).Kind() +// setSliceWithProperType sets proper values to slice based on its type. +func setSliceWithProperType(key *Key, field reflect.Value, delim string) error { + strs := key.Strings(delim) + numVals := len(strs) + if numVals == 0 { + return nil + } + + var vals interface{} + + sliceOf := field.Type().Elem().Kind() + switch sliceOf { + case reflect.String: + vals = strs + case reflect.Int: + vals = key.Ints(delim) + case reflect.Int64: + vals = key.Int64s(delim) + case reflect.Uint: + vals = key.Uints(delim) + case reflect.Uint64: + vals = key.Uint64s(delim) + case reflect.Float64: + vals = key.Float64s(delim) + case reflectTime: + vals = key.Times(delim) + default: + return fmt.Errorf("unsupported type '[]%s'", sliceOf) + } + + slice := reflect.MakeSlice(field.Type(), numVals, numVals) + for i := 0; i < numVals; i++ { + switch sliceOf { + case reflect.String: + slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i])) + case reflect.Int: + slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i])) + case reflect.Int64: + slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i])) + case reflect.Uint: + slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i])) + case reflect.Uint64: + slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) + case reflect.Float64: + slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) + case reflectTime: + slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) + } + } + field.Set(slice) + return nil +} + // setWithProperType sets proper value to field based on its type, // but it does not return error for failing parsing, // because we want to use default value that is already assigned to strcut. -func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error { - switch kind { +func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error { + switch t.Kind() { case reflect.String: if len(key.String()) == 0 { return nil @@ -92,11 +147,33 @@ func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim s } field.SetBool(boolVal) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + durationVal, err := key.Duration() + // Skip zero value + if err == nil && int(durationVal) > 0 { + field.Set(reflect.ValueOf(durationVal)) + return nil + } + intVal, err := key.Int64() - if err != nil { + if err != nil || intVal == 0 { return nil } field.SetInt(intVal) + // byte is an alias for uint8, so supporting uint8 breaks support for byte + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + durationVal, err := key.Duration() + // Skip zero value + if err == nil && int(durationVal) > 0 { + field.Set(reflect.ValueOf(durationVal)) + return nil + } + + uintVal, err := key.Uint64() + if err != nil { + return nil + } + field.SetUint(uintVal) + case reflect.Float64: floatVal, err := key.Float64() if err != nil { @@ -110,31 +187,9 @@ func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim s } field.Set(reflect.ValueOf(timeVal)) case reflect.Slice: - vals := key.Strings(delim) - numVals := len(vals) - if numVals == 0 { - return nil - } - - sliceOf := field.Type().Elem().Kind() - - var times []time.Time - if sliceOf == reflectTime { - times = key.Times(delim) - } - - slice := reflect.MakeSlice(field.Type(), numVals, numVals) - for i := 0; i < numVals; i++ { - switch sliceOf { - case reflectTime: - slice.Index(i).Set(reflect.ValueOf(times[i])) - default: - slice.Index(i).Set(reflect.ValueOf(vals[i])) - } - } - field.Set(slice) + return setSliceWithProperType(key, field, delim) default: - return fmt.Errorf("unsupported type '%s'", kind) + return fmt.Errorf("unsupported type '%s'", t) } return nil } @@ -154,20 +209,19 @@ func (s *Section) mapTo(val reflect.Value) error { continue } - fieldName := s.parseFieldName(tpField.Name, tag) + opts := strings.SplitN(tag, ",", 2) // strip off possible omitempty + fieldName := s.parseFieldName(tpField.Name, opts[0]) if len(fieldName) == 0 || !field.CanSet() { continue } - if tpField.Type.Kind() == reflect.Struct { - if sec, err := s.f.GetSection(fieldName); err == nil { - if err = sec.mapTo(field); err != nil { - return fmt.Errorf("error mapping field(%s): %v", fieldName, err) - } - continue - } - } else if tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous { + isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous + isStruct := tpField.Type.Kind() == reflect.Struct + if isAnonymous { field.Set(reflect.New(tpField.Type.Elem())) + } + + if isAnonymous || isStruct { if sec, err := s.f.GetSection(fieldName); err == nil { if err = sec.mapTo(field); err != nil { return fmt.Errorf("error mapping field(%s): %v", fieldName, err) @@ -177,7 +231,7 @@ func (s *Section) mapTo(val reflect.Value) error { } if key, err := s.GetKey(fieldName); err == nil { - if err = setWithProperType(tpField.Type.Kind(), key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { + if err = setWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { return fmt.Errorf("error mapping field(%s): %v", fieldName, err) } } @@ -218,3 +272,160 @@ func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, other func MapTo(v, source interface{}, others ...interface{}) error { return MapToWithMapper(v, nil, source, others...) } + +// reflectSliceWithProperType does the opposite thing as setSliceWithProperType. +func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { + slice := field.Slice(0, field.Len()) + if field.Len() == 0 { + return nil + } + + var buf bytes.Buffer + sliceOf := field.Type().Elem().Kind() + for i := 0; i < field.Len(); i++ { + switch sliceOf { + case reflect.String: + buf.WriteString(slice.Index(i).String()) + case reflect.Int, reflect.Int64: + buf.WriteString(fmt.Sprint(slice.Index(i).Int())) + case reflect.Uint, reflect.Uint64: + buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) + case reflect.Float64: + buf.WriteString(fmt.Sprint(slice.Index(i).Float())) + case reflectTime: + buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) + default: + return fmt.Errorf("unsupported type '[]%s'", sliceOf) + } + buf.WriteString(delim) + } + key.SetValue(buf.String()[:buf.Len()-1]) + return nil +} + +// reflectWithProperType does the opposite thing as setWithProperType. +func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error { + switch t.Kind() { + case reflect.String: + key.SetValue(field.String()) + case reflect.Bool: + key.SetValue(fmt.Sprint(field.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + key.SetValue(fmt.Sprint(field.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + key.SetValue(fmt.Sprint(field.Uint())) + case reflect.Float32, reflect.Float64: + key.SetValue(fmt.Sprint(field.Float())) + case reflectTime: + key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) + case reflect.Slice: + return reflectSliceWithProperType(key, field, delim) + default: + return fmt.Errorf("unsupported type '%s'", t) + } + return nil +} + +// CR: copied from encoding/json/encode.go with modifications of time.Time support. +// TODO: add more test coverage. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflectTime: + return v.Interface().(time.Time).IsZero() + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func (s *Section) reflectFrom(val reflect.Value) error { + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + typ := val.Type() + + for i := 0; i < typ.NumField(); i++ { + field := val.Field(i) + tpField := typ.Field(i) + + tag := tpField.Tag.Get("ini") + if tag == "-" { + continue + } + + opts := strings.SplitN(tag, ",", 2) + if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) { + continue + } + + fieldName := s.parseFieldName(tpField.Name, opts[0]) + if len(fieldName) == 0 || !field.CanSet() { + continue + } + + if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || + (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { + // Note: The only error here is section doesn't exist. + sec, err := s.f.GetSection(fieldName) + if err != nil { + // Note: fieldName can never be empty here, ignore error. + sec, _ = s.f.NewSection(fieldName) + } + if err = sec.reflectFrom(field); err != nil { + return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) + } + continue + } + + // Note: Same reason as secion. + key, err := s.GetKey(fieldName) + if err != nil { + key, _ = s.NewKey(fieldName, "") + } + if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { + return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) + } + + } + return nil +} + +// ReflectFrom reflects secion from given struct. +func (s *Section) ReflectFrom(v interface{}) error { + typ := reflect.TypeOf(v) + val := reflect.ValueOf(v) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } else { + return errors.New("cannot reflect from non-pointer struct") + } + + return s.reflectFrom(val) +} + +// ReflectFrom reflects file from given struct. +func (f *File) ReflectFrom(v interface{}) error { + return f.Section("").ReflectFrom(v) +} + +// ReflectFrom reflects data sources from given struct with name mapper. +func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error { + cfg.NameMapper = mapper + return cfg.ReflectFrom(v) +} + +// ReflectFrom reflects data sources from given struct. +func ReflectFrom(cfg *File, v interface{}) error { + return ReflectFromWithMapper(cfg, v, nil) +} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go b/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go deleted file mode 100644 index f6fad196ae0..00000000000 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2014 Unknwon -// -// 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 ini - -import ( - "strings" - "testing" - "time" - - . "github.com/smartystreets/goconvey/convey" -) - -type testNested struct { - Cities []string `delim:"|"` - Visits []time.Time - Note string - Unused int `ini:"-"` -} - -type testEmbeded struct { - GPA float64 -} - -type testStruct struct { - Name string `ini:"NAME"` - Age int - Male bool - Money float64 - Born time.Time - Others testNested - *testEmbeded `ini:"grade"` - Unused int `ini:"-"` -} - -const _CONF_DATA_STRUCT = ` -NAME = Unknwon -Age = 21 -Male = true -Money = 1.25 -Born = 1993-10-07T20:17:05Z - -[Others] -Cities = HangZhou|Boston -Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z -Note = Hello world! - -[grade] -GPA = 2.8 -` - -type unsupport struct { - Byte byte -} - -type unsupport2 struct { - Others struct { - Cities byte - } -} - -type unsupport3 struct { - Cities byte -} - -type unsupport4 struct { - *unsupport3 `ini:"Others"` -} - -type defaultValue struct { - Name string - Age int - Male bool - Money float64 - Born time.Time - Cities []string -} - -const _INVALID_DATA_CONF_STRUCT = ` -Name = -Age = age -Male = 123 -Money = money -Born = nil -Cities = -` - -func Test_Struct(t *testing.T) { - Convey("Map file to struct", t, func() { - ts := new(testStruct) - So(MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil) - - So(ts.Name, ShouldEqual, "Unknwon") - So(ts.Age, ShouldEqual, 21) - So(ts.Male, ShouldBeTrue) - So(ts.Money, ShouldEqual, 1.25) - - t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") - So(err, ShouldBeNil) - So(ts.Born.String(), ShouldEqual, t.String()) - - So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston") - So(ts.Others.Visits[0].String(), ShouldEqual, t.String()) - So(ts.Others.Note, ShouldEqual, "Hello world!") - So(ts.testEmbeded.GPA, ShouldEqual, 2.8) - }) - - Convey("Map to non-pointer struct", t, func() { - cfg, err := Load([]byte(_CONF_DATA_STRUCT)) - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - So(cfg.MapTo(testStruct{}), ShouldNotBeNil) - }) - - Convey("Map to unsupported type", t, func() { - cfg, err := Load([]byte(_CONF_DATA_STRUCT)) - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - cfg.NameMapper = func(raw string) string { - if raw == "Byte" { - return "NAME" - } - return raw - } - So(cfg.MapTo(&unsupport{}), ShouldNotBeNil) - So(cfg.MapTo(&unsupport2{}), ShouldNotBeNil) - So(cfg.MapTo(&unsupport4{}), ShouldNotBeNil) - }) - - Convey("Map from invalid data source", t, func() { - So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil) - }) - - Convey("Map to wrong types and gain default values", t, func() { - cfg, err := Load([]byte(_INVALID_DATA_CONF_STRUCT)) - So(err, ShouldBeNil) - - t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") - So(err, ShouldBeNil) - dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}} - So(cfg.MapTo(dv), ShouldBeNil) - So(dv.Name, ShouldEqual, "Joe") - So(dv.Age, ShouldEqual, 10) - So(dv.Male, ShouldBeTrue) - So(dv.Money, ShouldEqual, 1.25) - So(dv.Born.String(), ShouldEqual, t.String()) - So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston") - }) -} - -type testMapper struct { - PackageName string -} - -func Test_NameGetter(t *testing.T) { - Convey("Test name mappers", t, func() { - So(MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil) - - cfg, err := Load([]byte("PACKAGE_NAME=ini")) - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) - - cfg.NameMapper = AllCapsUnderscore - tg := new(testMapper) - So(cfg.MapTo(tg), ShouldBeNil) - So(tg.PackageName, ShouldEqual, "ini") - }) -} diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini b/Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini deleted file mode 100644 index 2ed0ac1d3ac..00000000000 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini +++ /dev/null @@ -1,2 +0,0 @@ -[author] -E-MAIL = u@gogs.io \ No newline at end of file diff --git a/README.md b/README.md index 6dbfc5388c2..f8d1db6cd07 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ the latest master builds [here](http://grafana.org/download/builds) ### Dependencies -- Go 1.5 +- Go 1.6 - NodeJS v4+ - [Godep](https://github.com/tools/godep) diff --git a/build.go b/build.go index 4347c486063..9b8aae6a68a 100644 --- a/build.go +++ b/build.go @@ -34,7 +34,7 @@ var ( binaries []string = []string{"grafana-server", "grafana-cli"} ) -const minGoVersion = 1.3 +const minGoVersion = 1.6 func main() { log.SetOutput(os.Stdout) diff --git a/circle.yml b/circle.yml index ef11d6b038e..5640980b702 100644 --- a/circle.yml +++ b/circle.yml @@ -31,4 +31,4 @@ deployment: branch: master owner: grafana commands: - - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} + - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} \ No newline at end of file diff --git a/conf/defaults.ini b/conf/defaults.ini index 6f3fede698a..53a034bfeaa 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -59,7 +59,7 @@ cert_key = #################################### Database #################################### [database] -# You can configure the database connection by specifying type, host, name, user and password +# You can configure the database connection by specifying type, host, name, user and password # as seperate properties or as on string using the url propertie. # Either "mysql", "postgres" or "sqlite3", it's your choice @@ -223,6 +223,19 @@ token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = +#################################### Generic OAuth ########################## +[auth.generic_oauth] +enabled = false +allow_sign_up = false +client_id = some_id +client_secret = some_secret +scopes = user:email +auth_url = +token_url = +api_url = +team_ids = +allowed_organizations = + #################################### Basic Auth ########################## [auth.basic] enabled = true diff --git a/conf/sample.ini b/conf/sample.ini index 4fcbe5d1157..2c428ea775f 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -61,7 +61,7 @@ #################################### Database #################################### [database] -# You can configure the database connection by specifying type, host, name, user and password +# You can configure the database connection by specifying type, host, name, user and password # as seperate properties or as on string using the url propertie. # Either "mysql", "postgres" or "sqlite3", it's your choice @@ -205,6 +205,19 @@ check_for_updates = true ;api_url = https://www.googleapis.com/oauth2/v1/userinfo ;allowed_domains = +#################################### Generic OAuth ########################## +[auth.generic_oauth] +;enabled = false +;allow_sign_up = false +;client_id = some_id +;client_secret = some_secret +;scopes = user:email,read:org +;auth_url = https://foo.bar/login/oauth/authorize +;token_url = https://foo.bar/login/oauth/access_token +;api_url = https://foo.bar/user +;team_ids = +;allowed_organizations = + #################################### Auth Proxy ########################## [auth.proxy] ;enabled = false @@ -318,7 +331,7 @@ check_for_updates = true # \______(_______;;;)__;;;) [alerting] -enabled = false +;enabled = false #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP API Url /api/metrics diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 92f4367d9ae..69338eaba40 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -77,7 +77,7 @@ Example dimension queries which will return list of resources for individual AWS Service | Query ------- | ----- -EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)` +ELB | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)` ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)` RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)` RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index af3bb796e2d..509695a273a 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -88,6 +88,8 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have `http` or `https` +> **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination. + ### domain This setting is only used in as a part of the `root_url` setting (see below). Important if you @@ -339,6 +341,23 @@ You may allow users to sign-up via Google authentication by setting the user successfully authenticating via Google authentication will be automatically signed up. +## [auth.generic_oauth] + +This option could be used if have your own oauth service. + +This callback URL must match the full HTTP address that you use in your +browser to access Grafana, but with the prefix path of `/login/generic_oauth`. + + [auth.generic_oauth] + enabled = true + client_id = YOUR_APP_CLIENT_ID + client_secret = YOUR_APP_CLIENT_SECRET + scopes = + auth_url = + token_url = + allowed_domains = mycompany.com mycompany.org + allow_sign_up = false +
## [auth.basic] diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service index e87635bb3de..ea7193c55a6 100644 --- a/packaging/deb/systemd/grafana-server.service +++ b/packaging/deb/systemd/grafana-server.service @@ -1,5 +1,5 @@ [Unit] -Description=Starts and stops a single grafana instance on this system +Description=Grafana instance Documentation=http://docs.grafana.org Wants=network-online.target After=network-online.target diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index d79c5987576..ec0ef7e804e 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -1,5 +1,5 @@ [Unit] -Description=Starts and stops a single grafana instance on this system +Description=Grafana instance Documentation=http://docs.grafana.org Wants=network-online.target After=network-online.target diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index f20e08a5cad..37d247a7c8f 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/annotations" ) func ValidateOrgAlert(c *middleware.Context) { @@ -146,7 +147,7 @@ func DelAlert(c *middleware.Context) Response { } func GetAlertNotifications(c *middleware.Context) Response { - query := &models.GetAlertNotificationsQuery{OrgId: c.OrgId} + query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId} if err := bus.Dispatch(query); err != nil { return ApiError(500, "Failed to get alert notifications", err) @@ -156,11 +157,12 @@ func GetAlertNotifications(c *middleware.Context) Response { for _, notification := range query.Result { result = append(result, dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - Created: notification.Created, - Updated: notification.Updated, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, }) } @@ -177,7 +179,7 @@ func GetAlertNotificationById(c *middleware.Context) Response { return ApiError(500, "Failed to get alert notifications", err) } - return Json(200, query.Result[0]) + return Json(200, query.Result) } func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response { @@ -212,3 +214,86 @@ func DeleteAlertNotification(c *middleware.Context) Response { return ApiSuccess("Notification deleted") } + +//POST /api/alert-notifications/test +func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) Response { + cmd := &alerting.NotificationTestCommand{ + Name: dto.Name, + Type: dto.Type, + Severity: dto.Severity, + Settings: dto.Settings, + } + + if err := bus.Dispatch(cmd); err != nil { + return ApiError(500, "Failed to send alert notifications", err) + } + + return ApiSuccess("Test notification sent") +} + +func GetAlertHistory(c *middleware.Context) Response { + alertId, err := getAlertIdForRequest(c) + if err != nil { + return ApiError(400, "Invalid request", err) + } + + query := &annotations.ItemQuery{ + AlertId: alertId, + Type: annotations.AlertType, + OrgId: c.OrgId, + Limit: c.QueryInt64("limit"), + } + + repo := annotations.GetRepository() + + items, err := repo.Find(query) + if err != nil { + return ApiError(500, "Failed to get history for alert", err) + } + + var result []dtos.AlertHistory + for _, item := range items { + result = append(result, dtos.AlertHistory{ + AlertId: item.AlertId, + Timestamp: item.Timestamp, + Data: item.Data, + NewState: item.NewState, + Text: item.Text, + Metric: item.Metric, + Title: item.Title, + }) + } + + return Json(200, result) +} + +func getAlertIdForRequest(c *middleware.Context) (int64, error) { + alertId := c.QueryInt64("alertId") + panelId := c.QueryInt64("panelId") + dashboardId := c.QueryInt64("dashboardId") + + if alertId == 0 && dashboardId == 0 && panelId == 0 { + return 0, fmt.Errorf("Missing alertId or dashboardId and panelId") + } + + if alertId == 0 { + //fetch alertId + query := models.GetAlertsQuery{ + OrgId: c.OrgId, + DashboardId: dashboardId, + PanelId: panelId, + } + + if err := bus.Dispatch(&query); err != nil { + return 0, err + } + + if len(query.Result) != 1 { + return 0, fmt.Errorf("PanelId is not unique on dashboard") + } + + alertId = query.Result[0].Id + } + + return alertId, nil +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 31184a5a095..de3a1de8a65 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -19,6 +19,9 @@ func Register(r *macaron.Macaron) { quota := middleware.Quota bind := binding.Bind + // automatically set HEAD for every GET + r.SetAutoHead(true) + // not logged in views r.Get("/", reqSignedIn, Index) r.Get("/logout", Logout) @@ -247,14 +250,16 @@ func Register(r *macaron.Macaron) { r.Group("/alerts", func() { r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) - //r.Get("/:alertId/states", wrap(GetAlertStates)) r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert)) r.Get("/", wrap(GetAlerts)) }) + r.Get("/alert-history", wrap(GetAlertHistory)) + r.Get("/alert-notifications", wrap(GetAlertNotifications)) r.Group("/alert-notifications", func() { + r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest)) r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification)) r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification)) r.Get("/:notificationId", wrap(GetAlertNotificationById)) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 62a83bdaa0b..18b48cd8e29 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -22,7 +22,6 @@ func GetDataSources(c *middleware.Context) { result := make(dtos.DataSourceList, 0) for _, ds := range query.Result { - dsItem := dtos.DataSource{ Id: ds.Id, OrgId: ds.OrgId, @@ -35,6 +34,7 @@ func GetDataSources(c *middleware.Context) { User: ds.User, BasicAuth: ds.BasicAuth, IsDefault: ds.IsDefault, + JsonData: ds.JsonData, } if plugin, exists := plugins.DataSources[ds.Type]; exists { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 0ec0174a949..83b21757d32 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -22,11 +22,12 @@ type AlertRule struct { } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type AlertTestCommand struct { @@ -52,3 +53,21 @@ type EvalMatch struct { Metric string `json:"metric"` Value float64 `json:"value"` } + +type AlertHistory struct { + AlertId int64 `json:"alertId"` + NewState string `json:"newState"` + Timestamp time.Time `json:"timestamp"` + Title string `json:"title"` + Text string `json:"text"` + Metric string `json:"metric"` + + Data *simplejson.Json `json:"data"` +} + +type NotificationTestCommand struct { + Name string `json:"name"` + Type string `json:"type"` + Settings *simplejson.Json `json:"settings"` + Severity string `json:"severity"` +} diff --git a/pkg/api/login.go b/pkg/api/login.go index 4f976f753a2..789765ee01e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -27,6 +27,8 @@ func LoginView(c *middleware.Context) { viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub + viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic + viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp viewData.Settings["loginHint"] = setting.LoginHint viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 2099a576647..b301924d956 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -14,7 +14,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C cmd := &contextCommandLine{context} if err := command(cmd); err != nil { logger.Errorf("\n%s: ", color.RedString("Error")) - logger.Errorf("%s\n\n", err) + logger.Errorf("%s %s\n\n", color.RedString("✗"), err) cmd.ShowHelp() os.Exit(1) diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index 1a6df719053..636292cce11 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -53,8 +53,16 @@ func upgradeAllCommand(c CommandLine) error { for _, p := range pluginsToUpgrade { logger.Infof("Updating %v \n", p.Id) - s.RemoveInstalledPlugin(pluginsDir, p.Id) - InstallPlugin(p.Id, "", c) + var err error + err = s.RemoveInstalledPlugin(pluginsDir, p.Id) + if err != nil { + return err + } + + err = InstallPlugin(p.Id, "", c) + if err != nil { + return err + } } return nil diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 5b383e69f34..1de46e7fd1d 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -19,9 +19,9 @@ func NewImageUploader() (ImageUploader, error) { return nil, err } - bucket := s3sec.Key("secret_key").String() - accessKey := s3sec.Key("access_key").String() - secretKey := s3sec.Key("secret_key").String() + bucket := s3sec.Key("bucket_url").MustString("") + accessKey := s3sec.Key("access_key").MustString("") + secretKey := s3sec.Key("secret_key").MustString("") if bucket == "" { return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3") diff --git a/pkg/components/imguploader/imguploader_test.go b/pkg/components/imguploader/imguploader_test.go index d12464dae69..4a18f22c173 100644 --- a/pkg/components/imguploader/imguploader_test.go +++ b/pkg/components/imguploader/imguploader_test.go @@ -1,7 +1,6 @@ package imguploader import ( - "reflect" "testing" "github.com/grafana/grafana/pkg/setting" @@ -27,7 +26,12 @@ func TestImageUploaderFactory(t *testing.T) { uploader, err := NewImageUploader() So(err, ShouldBeNil) - So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&S3Uploader{})) + original, ok := uploader.(*S3Uploader) + + So(ok, ShouldBeTrue) + So(original.accessKey, ShouldEqual, "access_key") + So(original.secretKey, ShouldEqual, "secret_key") + So(original.bucket, ShouldEqual, "bucket_url") }) Convey("Webdav uploader", func() { @@ -47,7 +51,12 @@ func TestImageUploaderFactory(t *testing.T) { uploader, err := NewImageUploader() So(err, ShouldBeNil) - So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&WebdavUploader{})) + original, ok := uploader.(*WebdavUploader) + + So(ok, ShouldBeTrue) + So(original.url, ShouldEqual, "webdavUrl") + So(original.username, ShouldEqual, "username") + So(original.password, ShouldEqual, "password") }) }) } diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index af995d566c1..59ec598412b 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -3,7 +3,10 @@ package imguploader import ( "io/ioutil" "net/http" + "net/url" + "path" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/util" "github.com/kr/s3/s3util" ) @@ -12,6 +15,7 @@ type S3Uploader struct { bucket string secretKey string accessKey string + log log.Logger } func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader { @@ -19,10 +23,11 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader { bucket: bucket, accessKey: accessKey, secretKey: secretKey, + log: log.New("s3uploader"), } } -func (u *S3Uploader) Upload(path string) (string, error) { +func (u *S3Uploader) Upload(imageDiskPath string) (string, error) { s3util.DefaultConfig.AccessKey = u.accessKey s3util.DefaultConfig.SecretKey = u.secretKey @@ -31,15 +36,26 @@ func (u *S3Uploader) Upload(path string) (string, error) { header.Add("x-amz-acl", "public-read") header.Add("Content-Type", "image/png") - fullUrl := u.bucket + util.GetRandomString(20) + ".png" - writer, err := s3util.Create(fullUrl, header, nil) + var imageUrl *url.URL + var err error + + if imageUrl, err = url.Parse(u.bucket); err != nil { + return "", err + } + + // add image to url + imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png") + imageUrlString := imageUrl.String() + log.Debug("Uploading image to s3", "url", imageUrlString) + + writer, err := s3util.Create(imageUrlString, header, nil) if err != nil { return "", err } defer writer.Close() - imgData, err := ioutil.ReadFile(path) + imgData, err := ioutil.ReadFile(imageDiskPath) if err != nil { return "", err } @@ -49,5 +65,5 @@ func (u *S3Uploader) Upload(path string) (string, error) { return "", err } - return fullUrl, nil + return imageUrlString, nil } diff --git a/pkg/components/imguploader/s3uploader_test.go b/pkg/components/imguploader/s3uploader_test.go new file mode 100644 index 00000000000..1204a2db4e2 --- /dev/null +++ b/pkg/components/imguploader/s3uploader_test.go @@ -0,0 +1,23 @@ +package imguploader + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUploadToS3(t *testing.T) { + SkipConvey("[Integration test] for external_image_store.webdav", t, func() { + setting.NewConfigContext(&setting.CommandLineArgs{ + HomePath: "../../../", + }) + + s3Uploader, _ := NewImageUploader() + + path, err := s3Uploader.Upload("../../../public/img/logo_transparent_400x.png") + + So(err, ShouldBeNil) + So(path, ShouldNotEqual, "") + }) +} diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go index 74444b1b123..3b59e1690fd 100644 --- a/pkg/components/imguploader/webdavuploader.go +++ b/pkg/components/imguploader/webdavuploader.go @@ -9,7 +9,6 @@ import ( "path" "time" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/util" ) @@ -20,7 +19,6 @@ type WebdavUploader struct { } func (u *WebdavUploader) Upload(pa string) (string, error) { - log.Error2("Hej") client := http.Client{Timeout: time.Duration(10 * time.Second)} url, _ := url.Parse(u.url) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 9477f2738b5..31dd6f8b201 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -9,34 +9,36 @@ func init() { } var ( - M_Instance_Start Counter - M_Page_Status_200 Counter - M_Page_Status_500 Counter - M_Page_Status_404 Counter - M_Api_Status_500 Counter - M_Api_Status_404 Counter - M_Api_User_SignUpStarted Counter - M_Api_User_SignUpCompleted Counter - M_Api_User_SignUpInvite Counter - M_Api_Dashboard_Save Timer - M_Api_Dashboard_Get Timer - M_Api_Dashboard_Search Timer - M_Api_Admin_User_Create Counter - M_Api_Login_Post Counter - M_Api_Login_OAuth Counter - M_Api_Org_Create Counter - M_Api_Dashboard_Snapshot_Create Counter - M_Api_Dashboard_Snapshot_External Counter - M_Api_Dashboard_Snapshot_Get Counter - M_Models_Dashboard_Insert Counter - M_Alerting_Result_Critical Counter - M_Alerting_Result_Warning Counter - M_Alerting_Result_Info Counter - M_Alerting_Result_Ok Counter - M_Alerting_Active_Alerts Counter - M_Alerting_Notification_Sent_Slack Counter - M_Alerting_Notification_Sent_Email Counter - M_Alerting_Notification_Sent_Webhook Counter + M_Instance_Start Counter + M_Page_Status_200 Counter + M_Page_Status_500 Counter + M_Page_Status_404 Counter + M_Api_Status_500 Counter + M_Api_Status_404 Counter + M_Api_User_SignUpStarted Counter + M_Api_User_SignUpCompleted Counter + M_Api_User_SignUpInvite Counter + M_Api_Dashboard_Save Timer + M_Api_Dashboard_Get Timer + M_Api_Dashboard_Search Timer + M_Api_Admin_User_Create Counter + M_Api_Login_Post Counter + M_Api_Login_OAuth Counter + M_Api_Org_Create Counter + M_Api_Dashboard_Snapshot_Create Counter + M_Api_Dashboard_Snapshot_External Counter + M_Api_Dashboard_Snapshot_Get Counter + M_Models_Dashboard_Insert Counter + M_Alerting_Result_State_Critical Counter + M_Alerting_Result_State_Warning Counter + M_Alerting_Result_State_Ok Counter + M_Alerting_Result_State_Paused Counter + M_Alerting_Result_State_Unknown Counter + M_Alerting_Result_State_ExecutionError Counter + M_Alerting_Active_Alerts Counter + M_Alerting_Notification_Sent_Slack Counter + M_Alerting_Notification_Sent_Email Counter + M_Alerting_Notification_Sent_Webhook Counter // Timers M_DataSource_ProxyReq_Timer Timer @@ -75,10 +77,13 @@ func initMetricVars(settings *MetricSettings) { M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert") - M_Alerting_Result_Critical = RegCounter("alerting.result", "severity", "critical") - M_Alerting_Result_Warning = RegCounter("alerting.result", "severity", "warning") - M_Alerting_Result_Info = RegCounter("alerting.result", "severity", "info") - M_Alerting_Result_Ok = RegCounter("alerting.result", "severity", "ok") + M_Alerting_Result_State_Critical = RegCounter("alerting.result", "state", "critical") + M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning") + M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok") + M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused") + M_Alerting_Result_State_Unknown = RegCounter("alerting.result", "state", "unknown") + M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error") + M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts") M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack") M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email") diff --git a/pkg/models/alert.go b/pkg/models/alert.go index b7d0c3e0c1c..d7dcd5b39a8 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -10,7 +10,7 @@ type AlertStateType string type AlertSeverityType string const ( - AlertStatePending AlertStateType = "pending" + AlertStateUnknown AlertStateType = "unknown" AlertStateExeuctionError AlertStateType = "execution_error" AlertStatePaused AlertStateType = "paused" AlertStateCritical AlertStateType = "critical" @@ -19,7 +19,7 @@ const ( ) func (s AlertStateType) IsValid() bool { - return s == AlertStateOK || s == AlertStatePending || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning + return s == AlertStateOK || s == AlertStateUnknown || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning } const ( diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 464d6dc88da..87b515f370c 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -7,29 +7,32 @@ import ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification @@ -43,8 +46,20 @@ type DeleteAlertNotificationCommand struct { type GetAlertNotificationsQuery struct { Name string Id int64 + OrgId int64 + + Result *AlertNotification +} + +type GetAlertNotificationsToSendQuery struct { Ids []int64 OrgId int64 Result []*AlertNotification } + +type GetAllAlertNotificationsQuery struct { + OrgId int64 + + Result []*AlertNotification +} diff --git a/pkg/models/models.go b/pkg/models/models.go index 189e594576b..5a53cfdabb3 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -6,4 +6,5 @@ const ( GITHUB OAuthType = iota + 1 GOOGLE TWITTER + GENERIC ) diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go index 8c28a278d9f..18a2bf35262 100644 --- a/pkg/services/alerting/conditions/evaluator.go +++ b/pkg/services/alerting/conditions/evaluator.go @@ -5,25 +5,21 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/alerting" - "github.com/grafana/grafana/pkg/tsdb" ) var ( - defaultTypes []string = []string{"gt", "lt"} - rangedTypes []string = []string{"within_range", "outside_range"} - paramlessTypes []string = []string{"no_value"} + defaultTypes []string = []string{"gt", "lt"} + rangedTypes []string = []string{"within_range", "outside_range"} ) type AlertEvaluator interface { - Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool + Eval(reducedValue *float64) bool } -type ParameterlessEvaluator struct { - Type string -} +type NoDataEvaluator struct{} -func (e *ParameterlessEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { - return len(series.Points) == 0 +func (e *NoDataEvaluator) Eval(reducedValue *float64) bool { + return reducedValue == nil } type ThresholdEvaluator struct { @@ -47,14 +43,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu return defaultEval, nil } -func (e *ThresholdEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { +func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool { + if reducedValue == nil { + return false + } + switch e.Type { case "gt": - return reducedValue > e.Threshold + return *reducedValue > e.Threshold case "lt": - return reducedValue < e.Threshold - case "no_value": - return len(series.Points) == 0 + return *reducedValue < e.Threshold } return false @@ -88,12 +86,16 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e return rangedEval, nil } -func (e *RangedEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { +func (e *RangedEvaluator) Eval(reducedValue *float64) bool { + if reducedValue == nil { + return false + } + switch e.Type { case "within_range": - return (e.Lower < reducedValue && e.Upper > reducedValue) || (e.Upper < reducedValue && e.Lower > reducedValue) + return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue) case "outside_range": - return (e.Upper < reducedValue && e.Lower < reducedValue) || (e.Upper > reducedValue && e.Lower > reducedValue) + return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue) } return false @@ -113,8 +115,8 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) { return newRangedEvaluator(typ, model) } - if inSlice(typ, paramlessTypes) { - return &ParameterlessEvaluator{Type: typ}, nil + if typ == "no_data" { + return &NoDataEvaluator{}, nil } return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"} diff --git a/pkg/services/alerting/conditions/evaluator_test.go b/pkg/services/alerting/conditions/evaluator_test.go index 8cc6899ff81..585dca2b878 100644 --- a/pkg/services/alerting/conditions/evaluator_test.go +++ b/pkg/services/alerting/conditions/evaluator_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) @@ -15,19 +14,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64) evaluator, err := NewAlertEvaluator(jsonModel) So(err, ShouldBeNil) - var timeserie [][2]float64 - dummieTimestamp := float64(521452145) - - for _, v := range datapoints { - timeserie = append(timeserie, [2]float64{v, dummieTimestamp}) - } - - tsdb := &tsdb.TimeSeries{ - Name: "test time serie", - Points: timeserie, - } - - return evaluator.Eval(tsdb, reducedValue) + return evaluator.Eval(reducedValue) } func TestEvalutors(t *testing.T) { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index d94c2d468f2..ef51e09685c 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -40,22 +40,27 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { for _, series := range seriesList { reducedValue := c.Reducer.Reduce(series) - evalMatch := c.Evaluator.Eval(series, reducedValue) + evalMatch := c.Evaluator.Eval(reducedValue) if context.IsTestRun { context.Logs = append(context.Logs, &alerting.ResultLogEntry{ - Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue), + Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue), }) } if evalMatch { context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{ Metric: series.Name, - Value: reducedValue, + Value: *reducedValue, }) } context.Firing = evalMatch + + // handle no data scenario + if reducedValue == nil { + context.NoDataFound = true + } } } @@ -106,10 +111,16 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb. RefId: "A", Query: c.Query.Model.Get("target").MustString(), DataSource: &tsdb.DataSourceInfo{ - Id: datasource.Id, - Name: datasource.Name, - PluginId: datasource.Type, - Url: datasource.Url, + Id: datasource.Id, + Name: datasource.Name, + PluginId: datasource.Type, + Url: datasource.Url, + User: datasource.User, + Password: datasource.Password, + Database: datasource.Database, + BasicAuth: datasource.BasicAuth, + BasicAuthUser: datasource.BasicAuthUser, + BasicAuthPassword: datasource.BasicAuthPassword, }, }, }, diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index f97ff6a56d7..50a36b716d2 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -1,52 +1,72 @@ package conditions -import "github.com/grafana/grafana/pkg/tsdb" +import ( + "math" + + "github.com/grafana/grafana/pkg/tsdb" +) type QueryReducer interface { - Reduce(timeSeries *tsdb.TimeSeries) float64 + Reduce(timeSeries *tsdb.TimeSeries) *float64 } type SimpleReducer struct { Type string } -func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 { - var value float64 = 0 +func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 { + if len(series.Points) == 0 { + return nil + } + + value := float64(0) + allNull := true switch s.Type { case "avg": for _, point := range series.Points { - value += point[0] + if point[0] != nil { + value += *point[0] + allNull = false + } } value = value / float64(len(series.Points)) case "sum": for _, point := range series.Points { - value += point[0] + if point[0] != nil { + value += *point[0] + allNull = false + } } case "min": - for i, point := range series.Points { - if i == 0 { - value = point[0] - } - - if value > point[0] { - value = point[0] + value = math.MaxFloat64 + for _, point := range series.Points { + if point[0] != nil { + allNull = false + if value > *point[0] { + value = *point[0] + } } } case "max": + value = -math.MaxFloat64 for _, point := range series.Points { - if value < point[0] { - value = point[0] + if point[0] != nil { + allNull = false + if value < *point[0] { + value = *point[0] + } } } - case "mean": - meanPosition := int64(len(series.Points) / 2) - value = series.Points[meanPosition][0] case "count": value = float64(len(series.Points)) } - return value + if allNull { + return nil + } + + return &value } func NewSimpleReducer(typ string) *SimpleReducer { diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 70dc6b17a2f..d1ac851daf1 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -26,38 +26,55 @@ type EvalContext struct { dashboardSlug string ImagePublicUrl string ImageOnDiskPath string + NoDataFound bool + RetryCount int +} + +type StateDescription struct { + Color string + Text string + Data string +} + +func (c *EvalContext) GetStateModel() *StateDescription { + switch c.Rule.State { + case m.AlertStateOK: + return &StateDescription{ + Color: "#36a64f", + Text: "OK", + } + case m.AlertStateUnknown: + return &StateDescription{ + Color: "#888888", + Text: "UNKNOWN", + } + case m.AlertStateExeuctionError: + return &StateDescription{ + Color: "#000", + Text: "EXECUTION_ERROR", + } + case m.AlertStateWarning: + return &StateDescription{ + Color: "#fd821b", + Text: "WARNING", + } + case m.AlertStateCritical: + return &StateDescription{ + Color: "#D63232", + Text: "CRITICAL", + } + default: + panic("Unknown rule state " + c.Rule.State) + } + } func (a *EvalContext) GetDurationMs() float64 { return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) } -func (c *EvalContext) GetColor() string { - if !c.Firing { - return "#36a64f" - } - - if c.Rule.Severity == m.AlertSeverityWarning { - return "#fd821b" - } else { - return "#D63232" - } -} - -func (c *EvalContext) GetStateText() string { - if !c.Firing { - return "OK" - } - - if c.Rule.Severity == m.AlertSeverityWarning { - return "WARNING" - } else { - return "CRITICAL" - } -} - func (c *EvalContext) GetNotificationTitle() string { - return "[" + c.GetStateText() + "] " + c.Rule.Name + return "[" + c.GetStateModel().Text + "] " + c.Rule.Name } func (c *EvalContext) getDashboardSlug() (string, error) { @@ -101,5 +118,6 @@ func NewEvalContext(rule *Rule) *EvalContext { DoneChan: make(chan bool, 1), CancelChan: make(chan bool, 1), log: log.New("alerting.evalContext"), + RetryCount: 0, } } diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 3abcd978064..2818f5ae8a2 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -8,6 +8,10 @@ import ( "github.com/grafana/grafana/pkg/metrics" ) +var ( + MaxRetries int = 1 +) + type DefaultEvalHandler struct { log log.Logger alertJobTimeout time.Duration @@ -21,7 +25,6 @@ func NewEvalHandler() *DefaultEvalHandler { } func (e *DefaultEvalHandler) Eval(context *EvalContext) { - go e.eval(context) select { @@ -29,13 +32,36 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { context.Error = fmt.Errorf("Timeout") context.EndTime = time.Now() e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id) + e.retry(context) case <-context.DoneChan: e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing) - } + if context.Error != nil { + e.retry(context) + } + } +} + +func (e *DefaultEvalHandler) retry(context *EvalContext) { + e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id) + + context.RetryCount++ + if context.RetryCount > MaxRetries { + context.DoneChan = make(chan bool, 1) + context.CancelChan = make(chan bool, 1) + e.Eval(context) + } } func (e *DefaultEvalHandler) eval(context *EvalContext) { + defer func() { + if err := recover(); err != nil { + e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1)) + if panicErr, ok := err.(error); ok { + context.Error = panicErr + } + } + }() for _, condition := range context.Rule.Conditions { condition.Eval(context) diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 039e9be9a30..ae5b4e4501d 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -40,6 +40,5 @@ func TestAlertingExecutor(t *testing.T) { handler.eval(context) So(context.Firing, ShouldEqual, false) }) - }) } diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 9688aba153a..e2549da2365 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,6 +1,10 @@ package alerting -import "time" +import ( + "time" + + "github.com/grafana/grafana/pkg/models" +) type EvalHandler interface { Eval(context *EvalContext) @@ -15,6 +19,7 @@ type Notifier interface { Notify(alertResult *EvalContext) GetType() string NeedsImage() bool + MatchSeverity(result models.AlertSeverityType) bool } type Condition interface { diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index e11e1e1aaaf..39e7eb83166 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -1,10 +1,11 @@ package alerting type Job struct { - Offset int64 - Delay bool - Running bool - Rule *Rule + Offset int64 + OffsetWait bool + Delay bool + Running bool + Rule *Rule } type ResultLogEntry struct { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 826aede2d11..9a81501833d 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -28,10 +28,14 @@ func (n *RootNotifier) NeedsImage() bool { return false } +func (n *RootNotifier) MatchSeverity(result m.AlertSeverityType) bool { + return false +} + func (n *RootNotifier) Notify(context *EvalContext) { n.log.Info("Sending notifications for", "ruleId", context.Rule.Id) - notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications) + notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) if err != nil { n.log.Error("Failed to read notifications", "error", err) return @@ -46,15 +50,17 @@ func (n *RootNotifier) Notify(context *EvalContext) { n.log.Error("Failed to upload alert panel image", "error", err) } + n.sendNotifications(notifiers, context) +} + +func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) { for _, notifier := range notifiers { n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType()) - go notifier.Notify(context) } } func (n *RootNotifier) uploadImage(context *EvalContext) error { - uploader, _ := imguploader.NewImageUploader() imageUrl, err := context.GetImageUrl() @@ -85,29 +91,28 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error { return nil } -func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) { - if len(notificationIds) == 0 { - return []Notifier{}, nil - } +func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) { + query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} - query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { return nil, err } var result []Notifier for _, notification := range query.Result { - if not, err := n.getNotifierFor(notification); err != nil { + if not, err := n.createNotifierFor(notification); err != nil { return nil, err } else { - result = append(result, not) + if shouldUseNotification(not, context) { + result = append(result, not) + } } } return result, nil } -func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, error) { +func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) { factory, found := notifierFactories[model.Type] if !found { return nil, errors.New("Unsupported notification type") @@ -116,6 +121,18 @@ func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, err return factory(model) } +func shouldUseNotification(notifier Notifier, context *EvalContext) bool { + if !context.Firing { + return true + } + + if context.Error != nil { + return true + } + + return notifier.MatchSeverity(context.Rule.Severity) +} + type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory) diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index ebe305c6174..1354177f9b3 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -1,114 +1,82 @@ package alerting -// func TestAlertNotificationExtraction(t *testing.T) { -// Convey("Notifier tests", t, func() { -// Convey("rules for sending notifications", func() { -// dummieNotifier := NotifierImpl{} -// -// result := &AlertResult{ -// State: alertstates.Critical, -// } -// -// notifier := &Notification{ -// Name: "Test Notifier", -// Type: "TestType", -// SendCritical: true, -// SendWarning: true, -// } -// -// Convey("Should send notification", func() { -// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue) -// }) -// -// Convey("warn:false and state:warn should not send", func() { -// result.State = alertstates.Warn -// notifier.SendWarning = false -// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse) -// }) -// }) -// -// Convey("Parsing alert notification from settings", func() { -// Convey("Parsing email", func() { -// Convey("empty settings should return error", func() { -// json := `{ }` -// -// settingsJSON, _ := simplejson.NewJson([]byte(json)) -// model := &m.AlertNotification{ -// Name: "ops", -// Type: "email", -// Settings: settingsJSON, -// } -// -// _, err := NewNotificationFromDBModel(model) -// So(err, ShouldNotBeNil) -// }) -// -// Convey("from settings", func() { -// json := ` -// { -// "to": "ops@grafana.org" -// }` -// -// settingsJSON, _ := simplejson.NewJson([]byte(json)) -// model := &m.AlertNotification{ -// Name: "ops", -// Type: "email", -// Settings: settingsJSON, -// } -// -// not, err := NewNotificationFromDBModel(model) -// -// So(err, ShouldBeNil) -// So(not.Name, ShouldEqual, "ops") -// So(not.Type, ShouldEqual, "email") -// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier") -// -// email := not.Notifierr.(*EmailNotifier) -// So(email.To, ShouldEqual, "ops@grafana.org") -// }) -// }) -// -// Convey("Parsing webhook", func() { -// Convey("empty settings should return error", func() { -// json := `{ }` -// -// settingsJSON, _ := simplejson.NewJson([]byte(json)) -// model := &m.AlertNotification{ -// Name: "ops", -// Type: "webhook", -// Settings: settingsJSON, -// } -// -// _, err := NewNotificationFromDBModel(model) -// So(err, ShouldNotBeNil) -// }) -// -// Convey("from settings", func() { -// json := ` -// { -// "url": "http://localhost:3000", -// "username": "username", -// "password": "password" -// }` -// -// settingsJSON, _ := simplejson.NewJson([]byte(json)) -// model := &m.AlertNotification{ -// Name: "slack", -// Type: "webhook", -// Settings: settingsJSON, -// } -// -// not, err := NewNotificationFromDBModel(model) -// -// So(err, ShouldBeNil) -// So(not.Name, ShouldEqual, "slack") -// So(not.Type, ShouldEqual, "webhook") -// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier") -// -// webhook := not.Notifierr.(*WebhookNotifier) -// So(webhook.Url, ShouldEqual, "http://localhost:3000") -// }) -// }) -// }) -// }) -// } +import ( + "testing" + + "fmt" + + "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +type FakeNotifier struct { + FakeMatchResult bool +} + +func (fn *FakeNotifier) GetType() string { + return "FakeNotifier" +} + +func (fn *FakeNotifier) NeedsImage() bool { + return true +} + +func (fn *FakeNotifier) Notify(alertResult *EvalContext) {} + +func (fn *FakeNotifier) MatchSeverity(result models.AlertSeverityType) bool { + return fn.FakeMatchResult +} + +func TestAlertNotificationExtraction(t *testing.T) { + + Convey("Notifier tests", t, func() { + Convey("none firing alerts", func() { + ctx := &EvalContext{ + Firing: false, + Rule: &Rule{ + Severity: models.AlertSeverityCritical, + }, + } + notifier := &FakeNotifier{FakeMatchResult: false} + + So(shouldUseNotification(notifier, ctx), ShouldBeTrue) + }) + + Convey("exeuction error cannot be ignored", func() { + ctx := &EvalContext{ + Firing: true, + Error: fmt.Errorf("I used to be a programmer just like you"), + Rule: &Rule{ + Severity: models.AlertSeverityCritical, + }, + } + notifier := &FakeNotifier{FakeMatchResult: false} + + So(shouldUseNotification(notifier, ctx), ShouldBeTrue) + }) + + Convey("firing alert that match", func() { + ctx := &EvalContext{ + Firing: true, + Rule: &Rule{ + Severity: models.AlertSeverityCritical, + }, + } + notifier := &FakeNotifier{FakeMatchResult: true} + + So(shouldUseNotification(notifier, ctx), ShouldBeTrue) + }) + + Convey("firing alert that dont match", func() { + ctx := &EvalContext{ + Firing: true, + Rule: &Rule{ + Severity: models.AlertSeverityCritical, + }, + } + notifier := &FakeNotifier{FakeMatchResult: false} + + So(shouldUseNotification(notifier, ctx), ShouldBeFalse) + }) + }) +} diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 48fe6c4eaa5..acfafd59ac4 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,8 +1,34 @@ package notifiers +import ( + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" +) + type NotifierBase struct { - Name string - Type string + Name string + Type string + SeverityFilter models.AlertSeverityType +} + +func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase { + base := NotifierBase{Name: name, Type: notifierType} + + severityFilter := models.AlertSeverityType(model.Get("severityFilter").MustString("")) + + if severityFilter == models.AlertSeverityCritical || severityFilter == models.AlertSeverityWarning { + base.SeverityFilter = severityFilter + } + + return base +} + +func (n *NotifierBase) MatchSeverity(result models.AlertSeverityType) bool { + if !n.SeverityFilter.IsValid() { + return true + } + + return n.SeverityFilter == result } func (n *NotifierBase) GetType() string { diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go new file mode 100644 index 00000000000..af5dd902c87 --- /dev/null +++ b/pkg/services/alerting/notifiers/base_test.go @@ -0,0 +1,36 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestBaseNotifier(t *testing.T) { + Convey("Parsing base notification severity", t, func() { + + Convey("matches", func() { + json := ` + { + "severityFilter": "critical" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + not := NewNotifierBase("ops", "email", settingsJSON) + So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue) + }) + + Convey("does not match", func() { + json := ` + { + "severityFilter": "critical" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + not := NewNotifierBase("ops", "email", settingsJSON) + So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse) + }) + }) +} diff --git a/pkg/services/alerting/notifiers/common.go b/pkg/services/alerting/notifiers/common.go deleted file mode 100644 index 48b634c44d7..00000000000 --- a/pkg/services/alerting/notifiers/common.go +++ /dev/null @@ -1 +0,0 @@ -package notifiers diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 20b0bc06463..c754dd7985e 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -29,12 +29,9 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &EmailNotifier{ - NotifierBase: NotifierBase{ - Name: model.Name, - Type: model.Type, - }, - Addresses: strings.Split(addressesString, "\n"), - log: log.New("alerting.notifier.email"), + NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings), + Addresses: strings.Split(addressesString, "\n"), + log: log.New("alerting.notifier.email"), }, nil } @@ -54,7 +51,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) { "State": context.Rule.State, "Name": context.Rule.Name, "Severity": context.Rule.Severity, - "SeverityColor": context.GetColor(), + "SeverityColor": context.GetStateModel().Color, "Message": context.Rule.Message, "RuleUrl": ruleUrl, "ImageLink": context.ImagePublicUrl, diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index e099c7da690..d0d67ca5a88 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -23,12 +23,9 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SlackNotifier{ - NotifierBase: NotifierBase{ - Name: model.Name, - Type: model.Type, - }, - Url: url, - log: log.New("alerting.notifier.slack"), + NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings), + Url: url, + log: log.New("alerting.notifier.slack"), }, nil } @@ -61,13 +58,26 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) { } } + if context.Error != nil { + fields = append(fields, map[string]interface{}{ + "title": "Error message", + "value": context.Error.Error(), + "short": false, + }) + } + + message := "" + if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. + message = context.Rule.Message + } + body := map[string]interface{}{ "attachments": []map[string]interface{}{ { - "color": context.GetColor(), + "color": context.GetStateModel().Color, "title": context.GetNotificationTitle(), "title_link": ruleUrl, - "text": context.Rule.Message, + "text": message, "fields": fields, "image_url": context.ImagePublicUrl, "footer": "Grafana v" + setting.BuildVersion, diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index e725752778f..9a66d18c73c 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -20,14 +20,11 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NotifierBase{ - Name: model.Name, - Type: model.Type, - }, - Url: url, - User: model.Settings.Get("user").MustString(), - Password: model.Settings.Get("password").MustString(), - log: log.New("alerting.notifier.webhook"), + NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings), + Url: url, + User: model.Settings.Get("user").MustString(), + Password: model.Settings.Get("password").MustString(), + log: log.New("alerting.notifier.webhook"), }, nil } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 09c77c8edd7..7932aead4aa 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -4,6 +4,7 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" @@ -30,17 +31,25 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { oldState := ctx.Rule.State exeuctionError := "" + annotationData := simplejson.New() if ctx.Error != nil { handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error) ctx.Rule.State = m.AlertStateExeuctionError exeuctionError = ctx.Error.Error() + annotationData.Set("errorMessage", exeuctionError) } else if ctx.Firing { ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity) + annotationData = simplejson.NewFromAny(ctx.EvalMatches) } else { - ctx.Rule.State = m.AlertStateOK + // handle no data case + if ctx.NoDataFound { + ctx.Rule.State = ctx.Rule.NoDataState + } else { + ctx.Rule.State = m.AlertStateOK + } } - countSeverity(ctx.Rule.Severity) + countStateResult(ctx.Rule.State) if ctx.Rule.State != oldState { handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState) @@ -61,10 +70,11 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { Type: annotations.AlertType, AlertId: ctx.Rule.Id, Title: ctx.Rule.Name, - Text: ctx.GetStateText(), + Text: ctx.GetStateModel().Text, NewState: string(ctx.Rule.State), PrevState: string(oldState), Timestamp: time.Now(), + Data: annotationData, } annotationRepo := annotations.GetRepository() @@ -76,15 +86,19 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { } } -func countSeverity(state m.AlertSeverityType) { +func countStateResult(state m.AlertStateType) { switch state { - case m.AlertSeverityOK: - metrics.M_Alerting_Result_Ok.Inc(1) - case m.AlertSeverityInfo: - metrics.M_Alerting_Result_Info.Inc(1) - case m.AlertSeverityWarning: - metrics.M_Alerting_Result_Warning.Inc(1) - case m.AlertSeverityCritical: - metrics.M_Alerting_Result_Critical.Inc(1) + case m.AlertStateCritical: + metrics.M_Alerting_Result_State_Critical.Inc(1) + case m.AlertStateWarning: + metrics.M_Alerting_Result_State_Warning.Inc(1) + case m.AlertStateOK: + metrics.M_Alerting_Result_State_Ok.Inc(1) + case m.AlertStatePaused: + metrics.M_Alerting_Result_State_Paused.Inc(1) + case m.AlertStateUnknown: + metrics.M_Alerting_Result_State_Unknown.Inc(1) + case m.AlertStateExeuctionError: + metrics.M_Alerting_Result_State_ExecutionError.Inc(1) } } diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 8292041e04c..a967e2c40fb 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -18,6 +18,7 @@ type Rule struct { Frequency int64 Name string Message string + NoDataState m.AlertStateType State m.AlertStateType Severity m.AlertSeverityType Conditions []Condition @@ -67,6 +68,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.Severity = ruleDef.Severity model.State = ruleDef.State + model.NoDataState = m.AlertStateType(ruleDef.Settings.Get("noDataState").MustString("unknown")) for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 91b31d6d07f..2d0677b2d70 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -45,6 +45,7 @@ func TestAlertRuleModel(t *testing.T) { "name": "name2", "description": "desc2", "handler": 0, + "noDataMode": "critical", "enabled": true, "frequency": "60s", "conditions": [ @@ -63,7 +64,7 @@ func TestAlertRuleModel(t *testing.T) { alertJSON, jsonErr := simplejson.NewJson([]byte(json)) So(jsonErr, ShouldBeNil) - alert := &models.Alert{ + alert := &m.Alert{ Id: 1, OrgId: 1, DashboardId: 1, @@ -80,6 +81,10 @@ func TestAlertRuleModel(t *testing.T) { Convey("Can read notifications", func() { So(len(alertRule.Notifications), ShouldEqual, 2) }) + + Convey("Can read noDataMode", func() { + So(len(alertRule.NoDataMode), ShouldEqual, m.AlertStateCritical) + }) }) }) } diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index ffac7ddb659..6701ad6eb6c 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -1,6 +1,7 @@ package alerting import ( + "math" "time" "github.com/grafana/grafana/pkg/log" @@ -34,8 +35,8 @@ func (s *SchedulerImpl) Update(rules []*Rule) { } job.Rule = rule - job.Offset = int64(i) - + job.Offset = ((rule.Frequency * 1000) / int64(len(rules))) * int64(i) + job.Offset = int64(math.Floor(float64(job.Offset) / 1000)) jobs[rule.Id] = job } @@ -46,9 +47,27 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) { now := tickTime.Unix() for _, job := range s.jobs { - if now%job.Rule.Frequency == 0 && job.Running == false { - s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name) - execQueue <- job + if job.Running { + continue + } + + if job.OffsetWait && now%job.Offset == 0 { + job.OffsetWait = false + s.enque(job, execQueue) + continue + } + + if now%job.Rule.Frequency == 0 { + if job.Offset > 0 { + job.OffsetWait = true + } else { + s.enque(job, execQueue) + } } } } + +func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) { + s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id) + execQueue <- job +} diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go new file mode 100644 index 00000000000..c3fbaeb4f1c --- /dev/null +++ b/pkg/services/alerting/test_notification.go @@ -0,0 +1,93 @@ +package alerting + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" +) + +type NotificationTestCommand struct { + Severity string + Name string + Type string + Settings *simplejson.Json +} + +func init() { + bus.AddHandler("alerting", handleNotificationTestCommand) + +} + +func handleNotificationTestCommand(cmd *NotificationTestCommand) error { + notifier := NewRootNotifier() + + model := &models.AlertNotification{ + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + } + + notifiers, err := notifier.createNotifierFor(model) + + if err != nil { + log.Error2("Failed to create notifier", "error", err.Error()) + return err + } + + severity := models.AlertSeverityType(cmd.Severity) + notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext(severity)) + + return nil +} + +func createTestEvalContext(severity models.AlertSeverityType) *EvalContext { + state := models.AlertStateOK + firing := false + if severity == models.AlertSeverityCritical { + state = models.AlertStateCritical + firing = true + } + if severity == models.AlertSeverityWarning { + state = models.AlertStateWarning + firing = true + } + + testRule := &Rule{ + DashboardId: 1, + PanelId: 1, + Name: "Test notification", + Message: "Someone is testing the alert notification within grafana.", + State: state, + Severity: severity, + } + + ctx := NewEvalContext(testRule) + ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png" + + ctx.IsTestRun = true + ctx.Firing = firing + ctx.Error = nil + ctx.EvalMatches = evalMatchesBasedOnSeverity(severity) + + return ctx +} + +func evalMatchesBasedOnSeverity(severity models.AlertSeverityType) []*EvalMatch { + matches := make([]*EvalMatch, 0) + if severity == models.AlertSeverityOK { + return matches + } + + matches = append(matches, &EvalMatch{ + Metric: "High value", + Value: 100, + }) + + matches = append(matches, &EvalMatch{ + Metric: "Higher Value", + Value: 200, + }) + + return matches +} diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index fd8c8ebdb9a..06be152ec31 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -8,6 +8,15 @@ import ( type Repository interface { Save(item *Item) error + Find(query *ItemQuery) ([]*Item, error) +} + +type ItemQuery struct { + OrgId int64 `json:"orgId"` + Type ItemType `json:"type"` + AlertId int64 `json:"alertId"` + + Limit int64 `json:"alertId"` } var repositoryInstance Repository diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index be67749a627..cfabe7c12d2 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -159,7 +159,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor } else { alert.Updated = time.Now() alert.Created = time.Now() - alert.State = m.AlertStatePending + alert.State = m.AlertStateUnknown alert.NewStateDate = time.Now() _, err := sess.Insert(alert) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index c5b3a2f5975..5105ca39eff 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -16,6 +16,8 @@ func init() { bus.AddHandler("sql", CreateAlertNotificationCommand) bus.AddHandler("sql", UpdateAlertNotification) bus.AddHandler("sql", DeleteAlertNotification) + bus.AddHandler("sql", GetAlertNotificationsToSend) + bus.AddHandler("sql", GetAllAlertNotifications) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -32,73 +34,122 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { } func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error { - return getAlertNotificationsInternal(query, x.NewSession()) + return getAlertNotificationInternal(query, x.NewSession()) } -func getAlertNotificationsInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error { +func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error { + results := make([]*m.AlertNotification, 0) + if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil { + return err + } + + query.Result = results + return nil +} + +func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error { var sql bytes.Buffer params := make([]interface{}, 0) sql.WriteString(`SELECT - alert_notification.id, - alert_notification.org_id, - alert_notification.name, - alert_notification.type, - alert_notification.created, - alert_notification.updated, - alert_notification.settings - FROM alert_notification - `) + alert_notification.id, + alert_notification.org_id, + alert_notification.name, + alert_notification.type, + alert_notification.created, + alert_notification.updated, + alert_notification.settings, + alert_notification.is_default + FROM alert_notification + `) sql.WriteString(` WHERE alert_notification.org_id = ?`) params = append(params, query.OrgId) - if query.Name != "" { - sql.WriteString(` AND alert_notification.name = ?`) - params = append(params, query.Name) - } - - if query.Id != 0 { - sql.WriteString(` AND alert_notification.id = ?`) - params = append(params, query.Id) - } - + sql.WriteString(` AND ((alert_notification.is_default = 1)`) if len(query.Ids) > 0 { - sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") + sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") for _, v := range query.Ids { params = append(params, v) } } + sql.WriteString(`)`) + + results := make([]*m.AlertNotification, 0) + if err := x.Sql(sql.String(), params...).Find(&results); err != nil { + return err + } + + query.Result = results + return nil +} + +func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error { + var sql bytes.Buffer + params := make([]interface{}, 0) + + sql.WriteString(`SELECT + alert_notification.id, + alert_notification.org_id, + alert_notification.name, + alert_notification.type, + alert_notification.created, + alert_notification.updated, + alert_notification.settings, + alert_notification.is_default + FROM alert_notification + `) + + sql.WriteString(` WHERE alert_notification.org_id = ?`) + params = append(params, query.OrgId) + + if query.Name != "" || query.Id != 0 { + if query.Name != "" { + sql.WriteString(` AND alert_notification.name = ?`) + params = append(params, query.Name) + } + + if query.Id != 0 { + sql.WriteString(` AND alert_notification.id = ?`) + params = append(params, query.Id) + } + } results := make([]*m.AlertNotification, 0) if err := sess.Sql(sql.String(), params...).Find(&results); err != nil { return err } - query.Result = results + if len(results) == 0 { + query.Result = nil + } else { + query.Result = results[0] + } + return nil } func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error { return inTransaction(func(sess *xorm.Session) error { existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} - err := getAlertNotificationsInternal(existingQuery, sess) + err := getAlertNotificationInternal(existingQuery, sess) if err != nil { return err } - if len(existingQuery.Result) > 0 { + if existingQuery.Result != nil { return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - Created: time.Now(), - Updated: time.Now(), + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } if _, err = sess.Insert(alertNotification); err != nil { @@ -120,11 +171,11 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { // check if name exists sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} - if err := getAlertNotificationsInternal(sameNameQuery, sess); err != nil { + if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil { return err } - if len(sameNameQuery.Result) > 0 && sameNameQuery.Result[0].Id != current.Id { + if sameNameQuery.Result != nil && sameNameQuery.Result.Id != current.Id { return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } @@ -132,6 +183,9 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Settings = cmd.Settings current.Name = cmd.Name current.Type = cmd.Type + current.IsDefault = cmd.IsDefault + + sess.UseBool("is_default") if affected, err := sess.Id(cmd.Id).Update(current); err != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 4cbcf13a000..d37062fb58f 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -23,7 +23,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { err := GetAlertNotifications(cmd) fmt.Printf("errror %v", err) So(err, ShouldBeNil) - So(len(cmd.Result), ShouldEqual, 0) + So(cmd.Result, ShouldBeNil) }) Convey("Can save Alert Notification", func() { @@ -63,20 +63,35 @@ func TestAlertNotificationSQLAccess(t *testing.T) { cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()} cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()} cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()} + + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd3), ShouldBeNil) + So(CreateAlertNotificationCommand(&cmd4), ShouldBeNil) + So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil) Convey("search", func() { - query := &m.GetAlertNotificationsQuery{ + query := &m.GetAlertNotificationsToSendQuery{ Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231}, OrgId: 1, } - err := GetAlertNotifications(query) + err := GetAlertNotificationsToSend(query) So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) + So(len(query.Result), ShouldEqual, 3) + }) + + Convey("all", func() { + query := &m.GetAllAlertNotificationsQuery{ + OrgId: 1, + } + + err := GetAllAlertNotifications(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 4) }) }) }) diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 0530952144e..5cf4461b370 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -1,6 +1,9 @@ package sqlstore import ( + "bytes" + "fmt" + "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/services/annotations" ) @@ -17,5 +20,39 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error { return nil }) - +} + +func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) { + var sql bytes.Buffer + params := make([]interface{}, 0) + + sql.WriteString(`SELECT * + from annotation + `) + + sql.WriteString(`WHERE org_id = ?`) + params = append(params, query.OrgId) + + if query.AlertId != 0 { + sql.WriteString(` AND alert_id = ?`) + params = append(params, query.AlertId) + } + + if query.Type != "" { + sql.WriteString(` AND type = ?`) + params = append(params, string(query.Type)) + } + + if query.Limit == 0 { + query.Limit = 10 + } + + sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit)) + + items := make([]*annotations.Item, 0) + if err := x.Sql(sql.String(), params...).Find(&items); err != nil { + return nil, err + } + + return items, nil } diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 63d61d8b196..b6956be41c7 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -62,5 +62,9 @@ func addAlertMigrations(mg *Migrator) { } mg.AddMigration("create alert_notification table v1", NewAddTableMigration(alert_notification)) + mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{ + Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0", + })) mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) + } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 830291b7ffa..fd010c42ca2 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -190,7 +190,7 @@ func ToAbsUrl(relativeUrl string) string { func shouldRedactKey(s string) bool { uppercased := strings.ToUpper(s) - return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET") + return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET") || strings.Contains(uppercased, "PROVIDER_CONFIG") } func shouldRedactURLKey(s string) bool { diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index db2f0fb3802..71c4ade1468 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -11,8 +11,9 @@ type OAuthInfo struct { } type OAuther struct { - GitHub, Google, Twitter bool - OAuthInfos map[string]*OAuthInfo + GitHub, Google, Twitter, Generic bool + OAuthInfos map[string]*OAuthInfo + OAuthProviderName string } var OAuthService *OAuther diff --git a/pkg/social/common.go b/pkg/social/common.go new file mode 100644 index 00000000000..7bce5d2ae8f --- /dev/null +++ b/pkg/social/common.go @@ -0,0 +1,20 @@ +package social + +import ( + "fmt" + "strings" +) + +func isEmailAllowed(email string, allowedDomains []string) bool { + if len(allowedDomains) == 0 { + return true + } + + valid := false + for _, domain := range allowedDomains { + emailSuffix := fmt.Sprintf("@%s", domain) + valid = valid || strings.HasSuffix(email, emailSuffix) + } + + return valid +} diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go new file mode 100644 index 00000000000..f016c87e201 --- /dev/null +++ b/pkg/social/generic_oauth.go @@ -0,0 +1,205 @@ +package social + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type GenericOAuth struct { + *oauth2.Config + allowedDomains []string + allowedOrganizations []string + apiUrl string + allowSignup bool + teamIds []int +} + +func (s *GenericOAuth) Type() int { + return int(models.GENERIC) +} + +func (s *GenericOAuth) IsEmailAllowed(email string) bool { + return isEmailAllowed(email, s.allowedDomains) +} + +func (s *GenericOAuth) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *GenericOAuth) IsTeamMember(client *http.Client) bool { + if len(s.teamIds) == 0 { + return true + } + + teamMemberships, err := s.FetchTeamMemberships(client) + if err != nil { + return false + } + + for _, teamId := range s.teamIds { + for _, membershipId := range teamMemberships { + if teamId == membershipId { + return true + } + } + } + + return false +} + +func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool { + if len(s.allowedOrganizations) == 0 { + return true + } + + organizations, err := s.FetchOrganizations(client) + if err != nil { + return false + } + + for _, allowedOrganization := range s.allowedOrganizations { + for _, organization := range organizations { + if organization == allowedOrganization { + return true + } + } + } + + return false +} + +func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) { + type Record struct { + Email string `json:"email"` + Primary bool `json:"primary"` + Verified bool `json:"verified"` + } + + emailsUrl := fmt.Sprintf(s.apiUrl + "/emails") + r, err := client.Get(emailsUrl) + if err != nil { + return "", err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return "", err + } + + var email = "" + for _, record := range records { + if record.Primary { + email = record.Email + } + } + + return email, nil +} + +func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) { + type Record struct { + Id int `json:"id"` + } + + membershipUrl := fmt.Sprintf(s.apiUrl + "/teams") + r, err := client.Get(membershipUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var ids = make([]int, len(records)) + for i, record := range records { + ids[i] = record.Id + } + + return ids, nil +} + +func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) { + type Record struct { + Login string `json:"login"` + } + + url := fmt.Sprintf(s.apiUrl + "/orgs") + r, err := client.Get(url) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var logins = make([]string, len(records)) + for i, record := range records { + logins[i] = record.Login + } + + return logins, nil +} + +func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { + var data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + } + + var err error + client := s.Client(oauth2.NoContext, token) + r, err := client.Get(s.apiUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + + userInfo := &BasicUserInfo{ + Identity: strconv.Itoa(data.Id), + Name: data.Name, + Email: data.Email, + } + + if !s.IsTeamMember(client) { + return nil, errors.New("User not a member of one of the required teams") + } + + if !s.IsOrganizationMember(client) { + return nil, errors.New("User not a member of one of the required organizations") + } + + if userInfo.Email == "" { + userInfo.Email, err = s.FetchPrivateEmail(client) + if err != nil { + return nil, err + } + } + + return userInfo, nil +} diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go new file mode 100644 index 00000000000..40c8f2a2f7c --- /dev/null +++ b/pkg/social/github_oauth.go @@ -0,0 +1,213 @@ +package social + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGithub struct { + *oauth2.Config + allowedDomains []string + allowedOrganizations []string + apiUrl string + allowSignup bool + teamIds []int +} + +var ( + ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") +) + +var ( + ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations") +) + +func (s *SocialGithub) Type() int { + return int(models.GITHUB) +} + +func (s *SocialGithub) IsEmailAllowed(email string) bool { + return isEmailAllowed(email, s.allowedDomains) +} + +func (s *SocialGithub) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGithub) IsTeamMember(client *http.Client) bool { + if len(s.teamIds) == 0 { + return true + } + + teamMemberships, err := s.FetchTeamMemberships(client) + if err != nil { + return false + } + + for _, teamId := range s.teamIds { + for _, membershipId := range teamMemberships { + if teamId == membershipId { + return true + } + } + } + + return false +} + +func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { + if len(s.allowedOrganizations) == 0 { + return true + } + + organizations, err := s.FetchOrganizations(client) + if err != nil { + return false + } + + for _, allowedOrganization := range s.allowedOrganizations { + for _, organization := range organizations { + if organization == allowedOrganization { + return true + } + } + } + + return false +} + +func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) { + type Record struct { + Email string `json:"email"` + Primary bool `json:"primary"` + Verified bool `json:"verified"` + } + + emailsUrl := fmt.Sprintf(s.apiUrl + "/emails") + r, err := client.Get(emailsUrl) + if err != nil { + return "", err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return "", err + } + + var email = "" + for _, record := range records { + if record.Primary { + email = record.Email + } + } + + return email, nil +} + +func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { + type Record struct { + Id int `json:"id"` + } + + membershipUrl := fmt.Sprintf(s.apiUrl + "/teams") + r, err := client.Get(membershipUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var ids = make([]int, len(records)) + for i, record := range records { + ids[i] = record.Id + } + + return ids, nil +} + +func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) { + type Record struct { + Login string `json:"login"` + } + + url := fmt.Sprintf(s.apiUrl + "/orgs") + r, err := client.Get(url) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var logins = make([]string, len(records)) + for i, record := range records { + logins[i] = record.Login + } + + return logins, nil +} + +func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { + var data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + } + + var err error + client := s.Client(oauth2.NoContext, token) + r, err := client.Get(s.apiUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + + userInfo := &BasicUserInfo{ + Identity: strconv.Itoa(data.Id), + Name: data.Name, + Email: data.Email, + } + + if !s.IsTeamMember(client) { + return nil, ErrMissingTeamMembership + } + + if !s.IsOrganizationMember(client) { + return nil, ErrMissingOrganizationMembership + } + + if userInfo.Email == "" { + userInfo.Email, err = s.FetchPrivateEmail(client) + if err != nil { + return nil, err + } + } + + return userInfo, nil +} diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go new file mode 100644 index 00000000000..7f0fdcc250a --- /dev/null +++ b/pkg/social/google_oauth.go @@ -0,0 +1,52 @@ +package social + +import ( + "encoding/json" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGoogle struct { + *oauth2.Config + allowedDomains []string + apiUrl string + allowSignup bool +} + +func (s *SocialGoogle) Type() int { + return int(models.GOOGLE) +} + +func (s *SocialGoogle) IsEmailAllowed(email string) bool { + return isEmailAllowed(email, s.allowedDomains) +} + +func (s *SocialGoogle) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { + var data struct { + Id string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } + var err error + + client := s.Client(oauth2.NoContext, token) + r, err := client.Get(s.apiUrl) + if err != nil { + return nil, err + } + defer r.Body.Close() + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + return &BasicUserInfo{ + Identity: data.Id, + Name: data.Name, + Email: data.Email, + }, nil +} diff --git a/pkg/social/social.go b/pkg/social/social.go index 4a8cb8bac5d..66d0f5fa778 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -1,14 +1,8 @@ package social import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strconv" "strings" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "golang.org/x/net/context" @@ -42,7 +36,7 @@ func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "google"} + allOauthes := []string{"github", "google", "generic_oauth"} for _, name := range allOauthes { sec := setting.Cfg.Section("auth." + name) @@ -98,269 +92,21 @@ func NewOAuthService() { allowSignup: info.AllowSignup, } } - } -} -func isEmailAllowed(email string, allowedDomains []string) bool { - if len(allowedDomains) == 0 { - return true - } - - valid := false - for _, domain := range allowedDomains { - emailSuffix := fmt.Sprintf("@%s", domain) - valid = valid || strings.HasSuffix(email, emailSuffix) - } - - return valid -} - -type SocialGithub struct { - *oauth2.Config - allowedDomains []string - allowedOrganizations []string - apiUrl string - allowSignup bool - teamIds []int -} - -var ( - ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") -) - -var ( - ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations") -) - -func (s *SocialGithub) Type() int { - return int(models.GITHUB) -} - -func (s *SocialGithub) IsEmailAllowed(email string) bool { - return isEmailAllowed(email, s.allowedDomains) -} - -func (s *SocialGithub) IsSignupAllowed() bool { - return s.allowSignup -} - -func (s *SocialGithub) IsTeamMember(client *http.Client) bool { - if len(s.teamIds) == 0 { - return true - } - - teamMemberships, err := s.FetchTeamMemberships(client) - if err != nil { - return false - } - - for _, teamId := range s.teamIds { - for _, membershipId := range teamMemberships { - if teamId == membershipId { - return true + // Generic - Uses the same scheme as Github. + if name == "generic_oauth" { + setting.OAuthService.Generic = true + setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String() + teamIds := sec.Key("team_ids").Ints(",") + allowedOrganizations := sec.Key("allowed_organizations").Strings(" ") + SocialMap["generic_oauth"] = &GenericOAuth{ + Config: &config, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + teamIds: teamIds, + allowedOrganizations: allowedOrganizations, } } } - - return false -} - -func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { - if len(s.allowedOrganizations) == 0 { - return true - } - - organizations, err := s.FetchOrganizations(client) - if err != nil { - return false - } - - for _, allowedOrganization := range s.allowedOrganizations { - for _, organization := range organizations { - if organization == allowedOrganization { - return true - } - } - } - - return false -} - -func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) { - type Record struct { - Email string `json:"email"` - Primary bool `json:"primary"` - Verified bool `json:"verified"` - } - - emailsUrl := fmt.Sprintf(s.apiUrl + "/emails") - r, err := client.Get(emailsUrl) - if err != nil { - return "", err - } - - defer r.Body.Close() - - var records []Record - - if err = json.NewDecoder(r.Body).Decode(&records); err != nil { - return "", err - } - - var email = "" - for _, record := range records { - if record.Primary { - email = record.Email - } - } - - return email, nil -} - -func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { - type Record struct { - Id int `json:"id"` - } - - membershipUrl := fmt.Sprintf(s.apiUrl + "/teams") - r, err := client.Get(membershipUrl) - if err != nil { - return nil, err - } - - defer r.Body.Close() - - var records []Record - - if err = json.NewDecoder(r.Body).Decode(&records); err != nil { - return nil, err - } - - var ids = make([]int, len(records)) - for i, record := range records { - ids[i] = record.Id - } - - return ids, nil -} - -func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) { - type Record struct { - Login string `json:"login"` - } - - url := fmt.Sprintf(s.apiUrl + "/orgs") - r, err := client.Get(url) - if err != nil { - return nil, err - } - - defer r.Body.Close() - - var records []Record - - if err = json.NewDecoder(r.Body).Decode(&records); err != nil { - return nil, err - } - - var logins = make([]string, len(records)) - for i, record := range records { - logins[i] = record.Login - } - - return logins, nil -} - -func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { - var data struct { - Id int `json:"id"` - Name string `json:"login"` - Email string `json:"email"` - } - - var err error - client := s.Client(oauth2.NoContext, token) - r, err := client.Get(s.apiUrl) - if err != nil { - return nil, err - } - - defer r.Body.Close() - - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { - return nil, err - } - - userInfo := &BasicUserInfo{ - Identity: strconv.Itoa(data.Id), - Name: data.Name, - Email: data.Email, - } - - if !s.IsTeamMember(client) { - return nil, ErrMissingTeamMembership - } - - if !s.IsOrganizationMember(client) { - return nil, ErrMissingOrganizationMembership - } - - if userInfo.Email == "" { - userInfo.Email, err = s.FetchPrivateEmail(client) - if err != nil { - return nil, err - } - } - - return userInfo, nil -} - -// ________ .__ -// / _____/ ____ ____ ____ | | ____ -// / \ ___ / _ \ / _ \ / ___\| | _/ __ \ -// \ \_\ ( <_> | <_> ) /_/ > |_\ ___/ -// \______ /\____/ \____/\___ /|____/\___ > -// \/ /_____/ \/ - -type SocialGoogle struct { - *oauth2.Config - allowedDomains []string - apiUrl string - allowSignup bool -} - -func (s *SocialGoogle) Type() int { - return int(models.GOOGLE) -} - -func (s *SocialGoogle) IsEmailAllowed(email string) bool { - return isEmailAllowed(email, s.allowedDomains) -} - -func (s *SocialGoogle) IsSignupAllowed() bool { - return s.allowSignup -} - -func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { - var data struct { - Id string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - } - var err error - - client := s.Client(oauth2.NoContext, token) - r, err := client.Get(s.apiUrl) - if err != nil { - return nil, err - } - defer r.Body.Close() - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { - return nil, err - } - return &BasicUserInfo{ - Identity: data.Id, - Name: data.Name, - Email: data.Email, - }, nil } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 2a68b23df71..6e554dbe810 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -2,16 +2,23 @@ package graphite import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "net/url" + "path" "strings" "time" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" ) +var ( + HttpClient = http.Client{Timeout: time.Duration(10 * time.Second)} +) + type GraphiteExecutor struct { *tsdb.DataSourceInfo } @@ -30,7 +37,7 @@ func init() { func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { result := &tsdb.BatchResult{} - params := url.Values{ + formData := url.Values{ "from": []string{"-" + formatTimeRange(context.TimeRange.From)}, "until": []string{formatTimeRange(context.TimeRange.To)}, "format": []string{"json"}, @@ -38,28 +45,26 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC } for _, query := range queries { - params["target"] = []string{query.Query} - glog.Debug("Graphite request", "query", query.Query) + formData["target"] = []string{query.Query} } - client := http.Client{Timeout: time.Duration(10 * time.Second)} - res, err := client.PostForm(e.Url+"/render?", params) + if setting.Env == setting.DEV { + glog.Debug("Graphite request", "params", formData) + } + + req, err := e.createRequest(formData) if err != nil { result.Error = err return result } - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) + res, err := HttpClient.Do(req) if err != nil { result.Error = err return result } - var data []TargetResponseDTO - err = json.Unmarshal(body, &data) + data, err := e.parseResponse(res) if err != nil { - glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body)) result.Error = err return result } @@ -71,12 +76,56 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC Name: series.Target, Points: series.DataPoints, }) + + if setting.Env == setting.DEV { + glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints)) + } } result.QueryResults["A"] = queryRes return result } +func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDTO, error) { + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return nil, err + } + + if res.StatusCode == http.StatusUnauthorized { + glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body)) + return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body)) + } + + var data []TargetResponseDTO + err = json.Unmarshal(body, &data) + if err != nil { + glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body)) + return nil, err + } + + return data, nil +} + +func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) { + u, _ := url.Parse(e.Url) + u.Path = path.Join(u.Path, "render") + + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode())) + if err != nil { + glog.Info("Failed to create request", "error", err) + return nil, fmt.Errorf("Failed to create request. error: %v", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if e.BasicAuth { + req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) + } + + return req, err +} + func formatTimeRange(input string) string { if input == "now" { return input diff --git a/pkg/tsdb/graphite/types.go b/pkg/tsdb/graphite/types.go index 4cd1b601bbc..085b1fb2b94 100644 --- a/pkg/tsdb/graphite/types.go +++ b/pkg/tsdb/graphite/types.go @@ -1,6 +1,6 @@ package graphite type TargetResponseDTO struct { - Target string `json:"target"` - DataPoints [][2]float64 `json:"datapoints"` + Target string `json:"target"` + DataPoints [][2]*float64 `json:"datapoints"` } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 2954c630b68..05a8b13ef84 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -46,13 +46,13 @@ type QueryResult struct { } type TimeSeries struct { - Name string `json:"name"` - Points [][2]float64 `json:"points"` + Name string `json:"name"` + Points [][2]*float64 `json:"points"` } type TimeSeriesSlice []*TimeSeries -func NewTimeSeries(name string, points [][2]float64) *TimeSeries { +func NewTimeSeries(name string, points [][2]*float64) *TimeSeries { return &TimeSeries{ Name: name, Points: points, diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 748b64dc5c1..3f31407f454 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -17,8 +17,10 @@ function (angular, coreModule, config) { $scope.googleAuthEnabled = config.googleAuthEnabled; $scope.githubAuthEnabled = config.githubAuthEnabled; - $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled; + $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled; $scope.allowUserPassLogin = config.allowUserPassLogin; + $scope.genericOAuthEnabled = config.genericOAuthEnabled; + $scope.oauthProviderName = config.oauthProviderName; $scope.disableUserSignUp = config.disableUserSignUp; $scope.loginHint = config.loginHint; diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 4b3cd2e8de3..7669f2fb709 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -113,7 +113,7 @@ function (_, $, coreModule) { if (str[0] === '/') { str = str.substring(1); } if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); } try { - return item.toLowerCase().match(str); + return item.toLowerCase().match(str.toLowerCase()); } catch(e) { return false; } diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 4ef329f975e..286f03db5f5 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -80,28 +80,27 @@ export class AlertSrv { showConfirmModal(payload) { var scope = this.$rootScope.$new(); - scope.title = payload.title; - scope.text = payload.text; - scope.text2 = payload.text2; - scope.confirmTextRequired = payload.confirmText !== undefined && payload.confirmText !== ""; - scope.onConfirm = function() { - if (!scope.confirmTextRequired || (scope.confirmTextRequired && scope.confirmTextValid)) { - payload.onConfirm(); - scope.dismiss(); - } + payload.onConfirm(); + scope.dismiss(); }; scope.updateConfirmText = function(value) { scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase(); }; + scope.title = payload.title; + scope.text = payload.text; + scope.text2 = payload.text2; + scope.confirmText = payload.confirmText; + scope.onConfirm = payload.onConfirm; scope.onAltAction = payload.onAltAction; scope.altActionText = payload.altActionText; scope.icon = payload.icon || "fa-check"; scope.yesText = payload.yesText || "Yes"; scope.noText = payload.noText || "Cancel"; + scope.confirmTextValid = scope.confirmText ? false : true; var confirmModal = this.$modal({ template: 'public/app/partials/confirm_modal.html', diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 0b8efbe9889..1c0312c3028 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -36,6 +36,13 @@ var reducerTypes = [ {text: 'count()', value: 'count'}, ]; +var noDataModes = [ + {text: 'OK', value: 'ok'}, + {text: 'Critical', value: 'critical'}, + {text: 'Warning', value: 'warning'}, + {text: 'Unknown', value: 'unknown'}, +]; + function createReducerPart(model) { var def = new QueryPartDef({type: model.type, defaultParams: []}); return new QueryPart(model, def); @@ -69,9 +76,9 @@ function getStateDisplayModel(state) { stateClass: 'alert-state-warning' }; } - case 'pending': { + case 'unknown': { return { - text: 'PENDING', + text: 'UNKNOWN', iconClass: "fa fa-question", stateClass: 'alert-state-warning' }; @@ -100,6 +107,7 @@ export default { conditionTypes: conditionTypes, evalFunctions: evalFunctions, severityLevels: severityLevels, + noDataModes: noDataModes, reducerTypes: reducerTypes, createReducerPart: createReducerPart, }; diff --git a/public/app/features/alerting/alert_list_ctrl.ts b/public/app/features/alerting/alert_list_ctrl.ts index b1ae2730fbb..0a8e9addcd7 100644 --- a/public/app/features/alerting/alert_list_ctrl.ts +++ b/public/app/features/alerting/alert_list_ctrl.ts @@ -13,7 +13,7 @@ export class AlertListCtrl { stateFilters = [ {text: 'All', value: null}, {text: 'OK', value: 'ok'}, - {text: 'Pending', value: 'pending'}, + {text: 'Unknown', value: 'unknown'}, {text: 'Warning', value: 'warning'}, {text: 'Critical', value: 'critical'}, {text: 'Execution Error', value: 'execution_error'}, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 2f222802438..f3cc998deaa 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -5,6 +5,7 @@ import {ThresholdMapper} from './threshold_mapper'; import {QueryPart} from 'app/core/components/query_part/query_part'; import alertDef from './alert_def'; import config from 'app/core/config'; +import moment from 'moment'; export class AlertTabCtrl { panel: any; @@ -17,11 +18,13 @@ export class AlertTabCtrl { conditionModels: any; evalFunctions: any; severityLevels: any; + noDataModes: any; addNotificationSegment; notifications; alertNotifications; error: string; appSubUrl: string; + alertHistory: any; /** @ngInject */ constructor(private $scope, @@ -39,6 +42,7 @@ export class AlertTabCtrl { this.evalFunctions = alertDef.evalFunctions; this.conditionTypes = alertDef.conditionTypes; this.severityLevels = alertDef.severityLevels; + this.noDataModes = alertDef.noDataModes; this.appSubUrl = config.appSubUrl; } @@ -60,6 +64,7 @@ export class AlertTabCtrl { // build notification model this.notifications = []; this.alertNotifications = []; + this.alertHistory = []; return this.backendSrv.get('/api/alert-notifications').then(res => { this.notifications = res; @@ -74,6 +79,21 @@ export class AlertTabCtrl { }); } + getAlertHistory() { + this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}`).then(res => { + this.alertHistory = _.map(res, ah => { + ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss'); + ah.stateModel = alertDef.getStateDisplayModel(ah.newState); + + ah.metrics = _.map(ah.data, ev=> { + return ev.Metric + "=" + ev.Value; + }).join(', '); + + return ah; + }); + }); + } + getNotificationIcon(type) { switch (type) { case "email": return "fa fa-envelope"; @@ -88,6 +108,13 @@ export class AlertTabCtrl { })); } + changeTabIndex(newTabIndex) { + this.subTabIndex = newTabIndex; + + if (this.subTabIndex === 2) { + this.getAlertHistory(); + } + } notificationAdded() { var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value}); @@ -109,13 +136,18 @@ export class AlertTabCtrl { } initModel() { - var alert = this.alert = this.panel.alert = this.panel.alert || {}; + var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false}; + + if (!this.alert.enabled) { + return; + } alert.conditions = alert.conditions || []; if (alert.conditions.length === 0) { alert.conditions.push(this.buildDefaultCondition()); } + alert.noDataState = alert.noDataState || 'unknown'; alert.severity = alert.severity || 'critical'; alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; @@ -129,11 +161,9 @@ export class AlertTabCtrl { return memo; }, []); - if (this.alert.enabled) { - this.panelCtrl.editingThresholds = true; - } - ThresholdMapper.alertToGraphThresholds(this.panel); + + this.panelCtrl.editingThresholds = true; this.panelCtrl.render(); } @@ -157,6 +187,10 @@ export class AlertTabCtrl { } validateModel() { + if (!this.alert.enabled) { + return; + } + let firstTarget; var fixed = false; let foundTarget = null; @@ -192,6 +226,8 @@ export class AlertTabCtrl { this.error = 'Currently the alerting backend only supports Graphite queries'; } else if (this.templateSrv.variableExists(foundTarget.target)) { this.error = 'Template variables are not supported in alert queries'; + } else { + this.error = ''; } }); } diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index fe7aac197bc..3ef9e822333 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -7,6 +7,8 @@ import config from 'app/core/config'; export class AlertNotificationEditCtrl { model: any; + showTest: boolean = false; + testSeverity: string = "critical"; /** @ngInject */ constructor(private $routeParams, private backendSrv, private $scope, private $location) { @@ -15,7 +17,10 @@ export class AlertNotificationEditCtrl { } else { this.model = { type: 'email', - settings: {} + settings: { + severityFilter: 'none' + }, + isDefault: false }; } } @@ -38,8 +43,8 @@ export class AlertNotificationEditCtrl { }); } else { this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => { - this.$location.path('alerting/notification/' + res.id + '/edit'); this.$scope.appEvent('alert-success', ['Notification created', '']); + this.$location.path('alerting/notifications'); }); } } @@ -47,6 +52,24 @@ export class AlertNotificationEditCtrl { typeChanged() { this.model.settings = {}; } + + toggleTest() { + this.showTest = !this.showTest; + } + + testNotification() { + var payload = { + name: this.model.name, + type: this.model.type, + settings: this.model.settings, + severity: this.testSeverity + }; + + this.backendSrv.post(`/api/alert-notifications/test`, payload) + .then(res => { + this.$scope.appEvent('alert-succes', ['Test notification sent', '']); + }); + } } coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl); diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index a386d23945b..770e3a2cb05 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -2,15 +2,15 @@