// ParseQuantity turns str into a Quantity, or returns an error. func ParseQuantity(str string) (*Quantity, error) { parts := splitRE.FindStringSubmatch(strings.TrimSpace(str)) // regexp returns are entire match, followed by an entry for each () section. if len(parts) != 3 { return nil, ErrFormatWrong } amount := new(inf.Dec) if _, ok := amount.SetString(parts[1]); !ok { return nil, ErrNumeric } base, exponent, format, ok := quantitySuffixer.interpret(suffix(parts[2])) if !ok { return nil, ErrSuffix } // So that no one but us has to think about suffixes, remove it. if base == 10 { amount.SetScale(amount.Scale() + inf.Scale(-exponent)) } else if base == 2 { // numericSuffix = 2 ** exponent numericSuffix := big.NewInt(1).Lsh(bigOne, uint(exponent)) ub := amount.UnscaledBig() amount.SetUnscaledBig(ub.Mul(ub, numericSuffix)) } // Cap at min/max bounds. sign := amount.Sign() if sign == -1 { amount.Neg(amount) } // This rounds non-zero values up to the minimum representable // value, under the theory that if you want some resources, you // should get some resources, even if you asked for way too small // of an amount. // Arguably, this should be inf.RoundHalfUp (normal rounding), but // that would have the side effect of rounding values < .5m to zero. if v, ok := amount.Unscaled(); v != int64(0) || !ok { amount.Round(amount, 3, inf.RoundUp) } // The max is just a simple cap. if amount.Cmp(maxAllowed) > 0 { amount.Set(maxAllowed) } if format == BinarySI && amount.Cmp(decOne) < 0 && amount.Cmp(decZero) > 0 { // This avoids rounding and hopefully confusion, too. format = DecimalSI } if sign == -1 { amount.Neg(amount) } return &Quantity{amount, format}, nil }
func TestReadNodeConfigLocalVolumeDirQuota(t *testing.T) { tests := map[string]struct { config string expected string }{ "null quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: null `, expected: "", }, "missing quota": { config: ` apiVersion: v1 volumeConfig: localQuota: `, expected: "", }, "missing localQuota": { config: ` apiVersion: v1 volumeConfig: `, expected: "", }, "missing volumeConfig": { config: ` apiVersion: v1 `, expected: "", }, "no unit (bytes) quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: 200000 `, expected: "200000", }, "Kb quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: 200Ki `, expected: "204800", }, "Mb quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: 512Mi `, expected: "536870912", }, "Gb quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: 2Gi `, expected: "2147483648", }, "Tb quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: 2Ti `, expected: "2199023255552", }, // This is invalid config, would be caught by validation but just // testing it parses ok: "negative quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: -512Mi `, expected: "-536870912", }, "zero quota": { config: ` apiVersion: v1 volumeConfig: localQuota: perFSGroup: 0 `, expected: "0", }, } for name, test := range tests { t.Logf("Running test: %s", name) nodeConfig := &internal.NodeConfig{} if err := latest.ReadYAMLInto([]byte(test.config), nodeConfig); err != nil { t.Errorf("Error reading yaml: %s", err.Error()) } if test.expected == "" && nodeConfig.VolumeConfig.LocalQuota.PerFSGroup != nil { t.Errorf("Expected empty quota but got: %s", *nodeConfig.VolumeConfig.LocalQuota.PerFSGroup) } if test.expected != "" { if nodeConfig.VolumeConfig.LocalQuota.PerFSGroup == nil { t.Errorf("Expected quota: %s, got: nil", test.expected) } else { amount := nodeConfig.VolumeConfig.LocalQuota.PerFSGroup.Amount t.Logf("%s", amount.String()) rounded := new(inf.Dec) rounded.Round(amount, 0, inf.RoundUp) t.Logf("%s", rounded.String()) if test.expected != rounded.String() { t.Errorf("Expected quota: %s, got: %s", test.expected, rounded.String()) } } } } }