Skip to content

Commit

Permalink
feat: added ParseResourceLimits utility and fixed mounted file encodi…
Browse files Browse the repository at this point in the history
…ng bug (#70)

Signed-off-by: Ben Meier <[email protected]>
  • Loading branch information
astromechza authored Jan 13, 2025
1 parent 2c11108 commit eb4256a
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 31 deletions.
16 changes: 14 additions & 2 deletions framework/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"maps"
"reflect"
"slices"
"sort"

score "github.com/score-spec/score-go/types"
)
Expand Down Expand Up @@ -120,6 +121,15 @@ func uuidV4() string {
return fmt.Sprintf("%x-%x-%x-%x-%x", d[:4], d[4:6], d[6:8], d[8:10], d[10:])
}

func sortedStringMapKeys[v any](input map[string]v) []string {
out := make([]string, 0, len(input))
for s := range input {
out = append(out, s)
}
sort.Strings(out)
return out
}

// WithPrimedResources returns a new copy of State with all workload resources resolved to at least their initial type,
// class and id. New resources will have an empty provider set. Existing resources will not be touched.
// This is not a deep copy, but any writes are executed in a copy-on-write manner to avoid modifying the source.
Expand All @@ -132,8 +142,10 @@ func (s *State[StateExtras, WorkloadExtras, ResourceExtras]) WithPrimedResources
}

primedResourceUids := make(map[ResourceUid]bool)
for workloadName, workload := range s.Workloads {
for resName, res := range workload.Spec.Resources {
for _, workloadName := range sortedStringMapKeys(s.Workloads) {
workload := s.Workloads[workloadName]
for _, resName := range sortedStringMapKeys(workload.Spec.Resources) {
res := workload.Spec.Resources[resName]
resUid := NewResourceUid(workloadName, resName, res.Type, res.Class, res.Id)
if existing, ok := out.Resources[resUid]; !ok {
out.Resources[resUid] = ScoreResourceState[ResourceExtras]{
Expand Down
56 changes: 27 additions & 29 deletions framework/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,7 @@ resources:
})

t.Run("two workload - nominal", func(t *testing.T) {
t.Run("one workload - nominal", func(t *testing.T) {
next := mustAddWorkload(t, start, `
next := mustAddWorkload(t, start, `
metadata: {"name": "example1"}
resources:
one:
Expand All @@ -263,7 +262,7 @@ resources:
type: thing2
id: dog
`)
next = mustAddWorkload(t, next, `
next = mustAddWorkload(t, next, `
metadata: {"name": "example2"}
resources:
one:
Expand All @@ -272,32 +271,31 @@ resources:
type: thing2
id: dog
`)
next, err := next.WithPrimedResources()
require.NoError(t, err)
assert.Len(t, start.Resources, 0)
assert.Len(t, next.Resources, 3)
checkAndResetGuids(t, next.Resources)
assert.Equal(t, map[ResourceUid]ScoreResourceState[NoExtras]{
"thing.default#example1.one": {
Guid: "00000000-0000-0000-0000-000000000000",
Type: "thing", Class: "default", Id: "example1.one", State: map[string]interface{}{},
SourceWorkload: "example1",
Outputs: map[string]interface{}{},
},
"thing.default#example2.one": {
Guid: "00000000-0000-0000-0000-000000000000",
Type: "thing", Class: "default", Id: "example2.one", State: map[string]interface{}{},
SourceWorkload: "example2",
Outputs: map[string]interface{}{},
},
"thing2.default#dog": {
Guid: "00000000-0000-0000-0000-000000000000",
Type: "thing2", Class: "default", Id: "dog", State: map[string]interface{}{},
SourceWorkload: "example1",
Outputs: map[string]interface{}{},
},
}, next.Resources)
})
next, err := next.WithPrimedResources()
require.NoError(t, err)
assert.Len(t, start.Resources, 0)
assert.Len(t, next.Resources, 3)
checkAndResetGuids(t, next.Resources)
assert.Equal(t, map[ResourceUid]ScoreResourceState[NoExtras]{
"thing.default#example1.one": {
Guid: "00000000-0000-0000-0000-000000000000",
Type: "thing", Class: "default", Id: "example1.one", State: map[string]interface{}{},
SourceWorkload: "example1",
Outputs: map[string]interface{}{},
},
"thing.default#example2.one": {
Guid: "00000000-0000-0000-0000-000000000000",
Type: "thing", Class: "default", Id: "example2.one", State: map[string]interface{}{},
SourceWorkload: "example2",
Outputs: map[string]interface{}{},
},
"thing2.default#dog": {
Guid: "00000000-0000-0000-0000-000000000000",
Type: "thing2", Class: "default", Id: "dog", State: map[string]interface{}{},
SourceWorkload: "example1",
Outputs: map[string]interface{}{},
},
}, next.Resources)
})

}
Expand Down
5 changes: 5 additions & 0 deletions loader/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"os"
"path/filepath"
"unicode/utf8"

"github.com/score-spec/score-go/types"
)
Expand Down Expand Up @@ -53,5 +54,9 @@ func readFile(baseDir, path string) (string, error) {
return "", err
}

if !utf8.Valid(raw) {
return "", fmt.Errorf("file contains non-utf8 characters")
}

return string(raw), nil
}
55 changes: 55 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

package types

import (
"fmt"
"strconv"
"strings"
)

//go:generate go run github.com/atombender/[email protected] -v --schema-output=https://score.dev/schemas/score=types.gen.go --schema-package=https://score.dev/schemas/score=types --schema-root-type=https://score.dev/schemas/score=Workload ../schema/files/score-v1b1.json.modified

func (m *ResourceMetadata) UnmarshalYAML(unmarshal func(interface{}) error) error {
Expand All @@ -33,3 +39,52 @@ func (m *WorkloadMetadata) UnmarshalYAML(unmarshal func(interface{}) error) erro
*m = WorkloadMetadata(out)
return nil
}

// findIPowerSuffix is used in ParseResourceLimits to parse memory units.
func findIPowerSuffix(raw string, suffi []string, base int64) (suffix string, m int64) {
m = 1
for _, s := range suffi {
m *= base
if strings.HasSuffix(raw, s) {
return s, m
}
}
return "", 0
}

// ParseResourceLimits parses a resource limits definition into milli-cpus and memory bytes if present.
// For example, 500m cpus = 500 millicpus, while 2 cpus = 2000 cpus. 1M == 1000000 bytes of memory, while 1Ki = 1024.
func ParseResourceLimits(rl ResourcesLimits) (milliCpus *int, memoryBytes *int64, err error) {
if rl.Cpu != nil {
isMilli := strings.HasSuffix(*rl.Cpu, "m")
c := strings.TrimSuffix(*rl.Cpu, "m")
v, err := strconv.ParseFloat(c, 64)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse cpus '%s' as a number", c)
}
if !isMilli {
v *= 1000
}
iv := int(v)
milliCpus = &iv
}
if rl.Memory != nil {
// https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/#memory-units
raw := *rl.Memory
var multiplier int64 = 1
if s, m := findIPowerSuffix(raw, []string{"K", "M", "G", "T"}, 1000); m > 0 {
raw = strings.TrimSuffix(raw, s)
multiplier = m
} else if s, m = findIPowerSuffix(raw, []string{"Ki", "Mi", "Gi", "Ti"}, 1024); m > 0 {
raw = strings.TrimSuffix(raw, s)
multiplier = m
}
if v, err := strconv.ParseInt(raw, 10, 64); err != nil {
return nil, nil, fmt.Errorf("failed to parse memory '%s' as a number", raw)
} else {
v *= multiplier
memoryBytes = &v
}
}
return
}
46 changes: 46 additions & 0 deletions types/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2020 Humanitec
//
// 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 types

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func Ref[k any](in k) *k {
return &in
}

func DerefOr[k any](in *k, def k) k {
if in == nil {
return def
}
return *in
}

func parseAndFormatResourceLimits(rl ResourcesLimits) string {
c, m, err := ParseResourceLimits(rl)
return fmt.Sprintf("%d %d %v", DerefOr(c, -1), DerefOr(m, -1), err)
}

func TestParseResourceLimits(t *testing.T) {
assert.Equal(t, "-1 -1 <nil>", parseAndFormatResourceLimits(ResourcesLimits{}))
assert.Equal(t, "1000 1000000 <nil>", parseAndFormatResourceLimits(ResourcesLimits{Cpu: Ref("1"), Memory: Ref("1M")}))
assert.Equal(t, "-1 -1 failed to parse cpus 'banana' as a number", parseAndFormatResourceLimits(ResourcesLimits{Cpu: Ref("banana"), Memory: nil}))
assert.Equal(t, "-1 -1 failed to parse memory 'banana' as a number", parseAndFormatResourceLimits(ResourcesLimits{Cpu: nil, Memory: Ref("banana")}))
assert.Equal(t, "200 128974848 <nil>", parseAndFormatResourceLimits(ResourcesLimits{Cpu: Ref("200m"), Memory: Ref("123Mi")}))
}

0 comments on commit eb4256a

Please sign in to comment.