Skip to content

Commit

Permalink
Merge pull request #137 from cybozu-go/arithmetic
Browse files Browse the repository at this point in the history
Arithmetic functions for Ignition templates, et al.
  • Loading branch information
morimoto-cybozu committed Feb 14, 2019
2 parents 08c9385 + 1e05597 commit 9b9c66a
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 13 deletions.
5 changes: 3 additions & 2 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ When a backward-incompatible change is to be merged to `master`, the schema vers
and conversion from old schema need to be implemented.

1. Increment `SchemaVersion` in [version.go](./version.go) by 1.
2. Add conversion method from old schema. Example: [models/etcd/convert2.go](./models/etcd/convert2.go).
3. Call the conversion method from `driver.Upgrade` defined in [models/etcd/schema.go](./models/etcd/schema.go).
2. Increment schema version at the top of [docs/schema.md](./docs/schema.md) by 1.
3. Add conversion method from old schema. Example: [models/etcd/convert2.go](./models/etcd/convert2.go).
4. Call the conversion method from `driver.Upgrade` defined in [models/etcd/schema.go](./models/etcd/schema.go).

Bump version
------------
Expand Down
7 changes: 7 additions & 0 deletions docs/ignition.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ Following additional template functions are defined and can be used:
* `MyURL`: returns the URL of the sabakan server.
* `Metadata`: takes a key to retrieve metadata value saved along with the template.
* `json`: renders the argument as JSON.
* `add`, `sub`, `mul`, `div`: do arithmetic on parameters.

For example, the following template may be replaced with 6 when `Machine.Spec.Rack` is 3.

```
{{ add .Spec.Rack 3 }}
```
Uploading templates to sabakan
------------------------------
Expand Down
12 changes: 5 additions & 7 deletions ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,11 @@ func renderString(name string, src string, params *IgnitionParams) (string, erro
Funcs(template.FuncMap{
"MyURL": getMyURL,
"Metadata": getMetadata,
"json": func(i interface{}) (string, error) {
data, err := json.Marshal(i)
if err != nil {
return "", err
}
return string(data), nil
},
"json": jsonFunc,
"add": addFunc,
"sub": subFunc,
"mul": mulFunc,
"div": divFunc,
}).
Parse(src)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions models/etcd/convert2.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ func (d *driver) convertTo2(ctx context.Context, mu *concurrency.Mutex) error {
}

// update schema version
const thisVersion = "2"
tresp, err := d.client.Txn(ctx).If(mu.IsOwner()).
Then(clientv3.OpPut(KeyVersion, sabakan.SchemaVersion)).
Then(clientv3.OpPut(KeyVersion, thisVersion)).
Commit()
if err != nil {
return err
Expand All @@ -145,7 +146,7 @@ func (d *driver) convertTo2(ctx context.Context, mu *concurrency.Mutex) error {
}

log.Info("updated schema version", map[string]interface{}{
"to": sabakan.SchemaVersion,
"to": thisVersion,
})
return nil
}
28 changes: 26 additions & 2 deletions models/etcd/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"errors"
"time"

"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/clientv3util"
"github.com/coreos/etcd/clientv3/concurrency"
"github.com/cybozu-go/log"
"github.com/cybozu-go/sabakan"
Expand All @@ -13,16 +15,38 @@ import (
const noVersion = "1"

func (d *driver) Version(ctx context.Context) (string, error) {
RETRY:
resp, err := d.client.Get(ctx, KeyVersion)
if err != nil {
return "", err
}

if resp.Count == 0 {
if resp.Count > 0 {
return string(resp.Kvs[0].Value), nil
}

resp, err = d.client.Get(ctx, KeyIPAM)
if err != nil {
return "", err
}
if resp.Count > 0 {
return noVersion, nil
}

return string(resp.Kvs[0].Value), nil
// For sabakan < 1.2.0, when IPAM config is not set, convertTo2 does nothing.
// Therefore it is safe to set schema version to sabakan.SchemaVersion as an
// initialization.
tresp, err := d.client.Txn(ctx).
If(clientv3util.KeyMissing(KeyVersion)).
Then(clientv3.OpPut(KeyVersion, sabakan.SchemaVersion)).
Commit()
if err != nil {
return "", err
}
if !tresp.Succeeded {
goto RETRY
}
return sabakan.SchemaVersion, nil
}

func (d *driver) Upgrade(ctx context.Context) error {
Expand Down
146 changes: 146 additions & 0 deletions template_funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package sabakan

import (
"encoding/json"
"errors"
)

var (
errNotInt = errors.New("not an integer")
errNotFloat = errors.New("not a float")
errZeroDivision = errors.New("zero division")
)

func jsonFunc(i interface{}) (string, error) {
data, err := json.Marshal(i)
if err != nil {
return "", err
}
return string(data), nil
}

func getAsInt64(a interface{}) (int64, error) {
switch i := a.(type) {
case int:
return int64(i), nil
case int8:
return int64(i), nil
case int16:
return int64(i), nil
case int32:
return int64(i), nil
case int64:
return int64(i), nil
case uint:
return int64(i), nil
case uint8:
return int64(i), nil
case uint16:
return int64(i), nil
case uint32:
return int64(i), nil
case uint64:
return int64(i), nil
}

return 0, errNotInt
}

func getAsFloat64(a interface{}) (float64, error) {
switch f := a.(type) {
case float32:
return float64(f), nil
case float64:
return f, nil
}

return 0, errNotFloat
}

func getAsInts(a, b interface{}) (int64, int64, error) {
ia, err := getAsInt64(a)
if err != nil {
return 0, 0, err
}
ib, err := getAsInt64(b)
if err != nil {
return 0, 0, err
}

return ia, ib, nil
}

func getAsFloats(a, b interface{}) (float64, float64, error) {
fa, err := getAsFloat64(a)
if err != nil {
ia, err2 := getAsInt64(a)
if err2 != nil {
return 0, 0, err
}
fa = float64(ia)
}
fb, err := getAsFloat64(b)
if err != nil {
ib, err2 := getAsInt64(b)
if err2 != nil {
return 0, 0, err
}
fb = float64(ib)
}

return fa, fb, nil
}

func addFunc(a, b interface{}) (interface{}, error) {
ia, ib, err := getAsInts(a, b)
if err == nil {
return ia + ib, nil
}
fa, fb, err := getAsFloats(a, b)
if err == nil {
return fa + fb, nil
}
return nil, err
}

func subFunc(a, b interface{}) (interface{}, error) {
ia, ib, err := getAsInts(a, b)
if err == nil {
return ia - ib, nil
}
fa, fb, err := getAsFloats(a, b)
if err == nil {
return fa - fb, nil
}
return nil, err
}

func mulFunc(a, b interface{}) (interface{}, error) {
ia, ib, err := getAsInts(a, b)
if err == nil {
return ia * ib, nil
}
fa, fb, err := getAsFloats(a, b)
if err == nil {
return fa * fb, nil
}
return nil, err
}

func divFunc(a, b interface{}) (interface{}, error) {
ia, ib, err := getAsInts(a, b)
if err == nil {
if ib == 0 {
return nil, errZeroDivision
}
return ia / ib, nil
}
fa, fb, err := getAsFloats(a, b)
if err == nil {
if fb == 0 {
return nil, errZeroDivision
}
return fa / fb, nil
}
return nil, err
}
89 changes: 89 additions & 0 deletions template_funcs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package sabakan

import (
"math"
"testing"

"github.com/google/go-cmp/cmp"
)

func testArithmeticFuncs(t *testing.T) {
cases := []struct {
name string
f func(a, b interface{}) (interface{}, error)
a interface{}
b interface{}
expect interface{}
wantErr bool
}{
{"addInt2", addFunc, int(3), int(-1), int64(2), false},
{"addInt8Int16", addFunc, int8(3), int16(-1), int64(2), false},
{"addInt32Int64", addFunc, int32(3), int64(-1), int64(2), false},
{"addUintUint8", addFunc, uint(3), uint8(1), int64(4), false},
{"addUint16Uint32", addFunc, uint16(3), uint32(1), int64(4), false},
{"addUint32Uint64", addFunc, uint32(3), uint64(1), int64(4), false},
{"addIntFloat32", addFunc, int(3), float32(-1.3), float64(1.7), false},
{"addFloat64Uint", addFunc, float64(3.3), uint(1), float64(4.3), false},
{"addFloat32Float64", addFunc, float32(3.3), float64(1), float64(4.3), false},
{"addInt64String", addFunc, int64(-1), "", nil, true},
{"addFloat64String", addFunc, float64(1.7), "", nil, true},
{"subInt2", subFunc, int(3), int(-1), int64(4), false},
{"subInt8Int16", subFunc, int8(3), int16(-1), int64(4), false},
{"subInt32Int64", subFunc, int32(3), int64(-1), int64(4), false},
{"subUintUint8", subFunc, uint(3), uint8(1), int64(2), false},
{"subUint16Uint32", subFunc, uint16(3), uint32(1), int64(2), false},
{"subUint32Uint64", subFunc, uint32(3), uint64(1), int64(2), false},
{"subIntFloat32", subFunc, int(3), float32(-1.3), float64(4.3), false},
{"subFloat64Uint", subFunc, float64(3.3), uint(1), float64(2.3), false},
{"subInt64String", subFunc, int64(-1), "", nil, true},
{"subFloat64String", subFunc, float64(1.7), "", nil, true},
{"mulInt2", mulFunc, int(3), int(-1), int64(-3), false},
{"mulInt8Int16", mulFunc, int8(3), int16(-1), int64(-3), false},
{"mulInt32Int64", mulFunc, int32(3), int64(-1), int64(-3), false},
{"mulUintUint8", mulFunc, uint(3), uint8(1), int64(3), false},
{"mulUint16Uint32", mulFunc, uint16(3), uint32(1), int64(3), false},
{"mulUint32Uint64", mulFunc, uint32(3), uint64(1), int64(3), false},
{"mulIntFloat32", mulFunc, int(3), float32(-1.3), float64(-3.9), false},
{"mulFloat64Uint", mulFunc, float64(3.3), uint(1), float64(3.3), false},
{"mulInt64String", mulFunc, int64(-1), "", nil, true},
{"mulFloat64String", mulFunc, float64(1.7), "", nil, true},
{"divInt2", divFunc, int(4), int(2), int64(2), false},
{"divInt8Int16", divFunc, int8(4), int16(2), int64(2), false},
{"divInt32Int64", divFunc, int32(4), int64(2), int64(2), false},
{"divUintUint8", divFunc, uint(4), uint8(2), int64(2), false},
{"divUint16Uint32", divFunc, uint16(4), uint32(2), int64(2), false},
{"divUint32Uint64", divFunc, uint32(4), uint64(2), int64(2), false},
{"divFloat32Int", divFunc, float32(3.9), int(3), float64(1.3), false},
{"divFloat64Uint", divFunc, float64(3.9), uint(3), float64(1.3), false},
{"divisionByIntZero", divFunc, int(10), int64(0), nil, true},
{"divisionByFloat64Zero", divFunc, int(10), float64(0), nil, true},
{"divInt64String", divFunc, int64(-1), "", nil, true},
{"divFloat64String", divFunc, float64(1.7), "", nil, true},
}

for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual, err := tt.f(tt.a, tt.b)
if err != nil {
if !tt.wantErr {
t.Error("unexpected error:", err)
}
return
}

if !cmp.Equal(tt.expect, actual, testCmp) {
t.Error("unexpected result:", cmp.Diff(tt.expect, actual, testCmp))
}
})
}
}

var testCmp = cmp.Comparer(func(a, b float64) bool {
return math.Abs(a-b) < 0.0001
})

func TestTemplateFuncs(t *testing.T) {
t.Run("arithmetic", testArithmeticFuncs)
}

0 comments on commit 9b9c66a

Please sign in to comment.