diff --git a/cli/command/service/generic_resource_opts.go b/cli/command/service/generic_resource_opts.go index 064111a3d..426b44d15 100644 --- a/cli/command/service/generic_resource_opts.go +++ b/cli/command/service/generic_resource_opts.go @@ -4,9 +4,8 @@ import ( "fmt" "strings" + "github.com/docker/cli/cli/command/service/internal/genericresource" "github.com/moby/moby/api/types/swarm" - swarmapi "github.com/moby/swarmkit/v2/api" - "github.com/moby/swarmkit/v2/api/genericresource" ) // GenericResource is a concept that a user can use to advertise user-defined @@ -33,12 +32,11 @@ func ParseGenericResources(value []string) ([]swarm.GenericResource, error) { return nil, nil } - resources, err := genericresource.Parse(value) + swarmResources, err := genericresource.Parse(value) if err != nil { return nil, fmt.Errorf("invalid generic resource specification: %w", err) } - swarmResources := genericResourcesFromGRPC(resources) for _, res := range swarmResources { if res.NamedResourceSpec != nil { return nil, fmt.Errorf("invalid generic-resource request `%s=%s`, Named Generic Resources is not supported for service create or update", @@ -50,31 +48,6 @@ func ParseGenericResources(value []string) ([]swarm.GenericResource, error) { return swarmResources, nil } -// genericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource -func genericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []swarm.GenericResource { - generic := make([]swarm.GenericResource, 0, len(genericRes)) - for _, res := range genericRes { - var current swarm.GenericResource - - switch r := res.Resource.(type) { - case *swarmapi.GenericResource_DiscreteResourceSpec: - current.DiscreteResourceSpec = &swarm.DiscreteGenericResource{ - Kind: r.DiscreteResourceSpec.Kind, - Value: r.DiscreteResourceSpec.Value, - } - case *swarmapi.GenericResource_NamedResourceSpec: - current.NamedResourceSpec = &swarm.NamedGenericResource{ - Kind: r.NamedResourceSpec.Kind, - Value: r.NamedResourceSpec.Value, - } - } - - generic = append(generic, current) - } - - return generic -} - func buildGenericResourceMap(genericRes []swarm.GenericResource) (map[string]swarm.GenericResource, error) { m := make(map[string]swarm.GenericResource) diff --git a/cli/command/service/internal/genericresource/helpers.go b/cli/command/service/internal/genericresource/helpers.go new file mode 100644 index 000000000..4cece4ce7 --- /dev/null +++ b/cli/command/service/internal/genericresource/helpers.go @@ -0,0 +1,48 @@ +package genericresource + +import ( + api "github.com/moby/moby/api/types/swarm" +) + +// NewSet creates a set object +func NewSet(key string, vals ...string) []api.GenericResource { + rs := make([]api.GenericResource, 0, len(vals)) + for _, v := range vals { + rs = append(rs, NewString(key, v)) + } + return rs +} + +// NewString creates a String resource +func NewString(kind, value string) api.GenericResource { + return api.GenericResource{ + NamedResourceSpec: &api.NamedGenericResource{ + Kind: kind, + Value: value, + }, + } +} + +// NewDiscrete creates a Discrete resource +func NewDiscrete(key string, val int64) api.GenericResource { + return api.GenericResource{ + DiscreteResourceSpec: &api.DiscreteGenericResource{ + Kind: key, + Value: val, + }, + } +} + +// GetResource returns resources from the "resources" parameter matching the kind key +func GetResource(kind string, resources []api.GenericResource) []api.GenericResource { + var res []api.GenericResource + for _, r := range resources { + switch { + case r.DiscreteResourceSpec != nil && r.DiscreteResourceSpec.Kind == kind: + res = append(res, r) + case r.NamedResourceSpec != nil && r.NamedResourceSpec.Kind == kind: + res = append(res, r) + } + } + return res +} diff --git a/vendor/github.com/moby/swarmkit/v2/api/genericresource/parse.go b/cli/command/service/internal/genericresource/parse.go similarity index 77% rename from vendor/github.com/moby/swarmkit/v2/api/genericresource/parse.go rename to cli/command/service/internal/genericresource/parse.go index 7c2a35913..e06ed95c1 100644 --- a/vendor/github.com/moby/swarmkit/v2/api/genericresource/parse.go +++ b/cli/command/service/internal/genericresource/parse.go @@ -1,3 +1,10 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +// Package genericresource is a local fork of SwarmKit's [genericresource] package, +// without protobuf dependencies. +// +// [genericresource]: https://github.com/moby/swarmkit/blob/v2.1.1/api/genericresource/parse.go package genericresource import ( @@ -6,10 +13,10 @@ import ( "strconv" "strings" - "github.com/moby/swarmkit/v2/api" + api "github.com/moby/moby/api/types/swarm" ) -func newParseError(format string, args ...interface{}) error { +func newParseError(format string, args ...any) error { return fmt.Errorf("could not parse GenericResource: "+format, args...) } @@ -32,15 +39,14 @@ func allNamedResources(res []string) bool { } // ParseCmd parses the Generic Resource command line argument -// and returns a list of *api.GenericResource -func ParseCmd(cmd string) ([]*api.GenericResource, error) { +// and returns a list of api.GenericResource +func ParseCmd(cmd string) ([]api.GenericResource, error) { if strings.Contains(cmd, "\n") { return nil, newParseError("unexpected '\\n' character") } r := csv.NewReader(strings.NewReader(cmd)) records, err := r.ReadAll() - if err != nil { return nil, newParseError("%v", err) } @@ -53,7 +59,7 @@ func ParseCmd(cmd string) ([]*api.GenericResource, error) { } // Parse parses a table of GenericResource resources -func Parse(cmds []string) ([]*api.GenericResource, error) { +func Parse(cmds []string) ([]api.GenericResource, error) { tokens := make(map[string][]string) for _, term := range cmds { @@ -69,7 +75,7 @@ func Parse(cmds []string) ([]*api.GenericResource, error) { tokens[key] = append(tokens[key], val) } - var rs []*api.GenericResource + var rs []api.GenericResource for k, v := range tokens { if u, ok := isDiscreteResource(v); ok { if u < 0 { @@ -107,5 +113,4 @@ func isDiscreteResource(values []string) (int64, bool) { } return u, true - } diff --git a/cli/command/service/internal/genericresource/parse_test.go b/cli/command/service/internal/genericresource/parse_test.go new file mode 100644 index 000000000..3a3390ac9 --- /dev/null +++ b/cli/command/service/internal/genericresource/parse_test.go @@ -0,0 +1,65 @@ +package genericresource + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestParseDiscrete(t *testing.T) { + res, err := ParseCmd("apple=3") + assert.NilError(t, err) + assert.Equal(t, len(res), 1) + + apples := GetResource("apple", res) + assert.Equal(t, len(apples), 1) + if apples[0].DiscreteResourceSpec == nil { + t.Fatalf("expected discrete resource spec, got nil") + } + assert.Equal(t, apples[0].DiscreteResourceSpec.Value, int64(3)) + + _, err = ParseCmd("apple=3\napple=4") + assert.Assert(t, err != nil) + + _, err = ParseCmd("apple=3,apple=4") + assert.Assert(t, err != nil) + + _, err = ParseCmd("apple=-3") + assert.Assert(t, err != nil) +} + +func TestParseStr(t *testing.T) { + res, err := ParseCmd("orange=red,orange=green,orange=blue") + assert.NilError(t, err) + assert.Equal(t, len(res), 3) + + oranges := GetResource("orange", res) + assert.Equal(t, len(oranges), 3) + for _, k := range []string{"red", "green", "blue"} { + assert.Assert(t, HasResource(NewString("orange", k), oranges)) + } +} + +func TestParseDiscreteAndStr(t *testing.T) { + res, err := ParseCmd("orange=red,orange=green,orange=blue,apple=3") + assert.NilError(t, err) + assert.Equal(t, len(res), 4) + + oranges := GetResource("orange", res) + assert.Equal(t, len(oranges), 3) + for _, k := range []string{"red", "green", "blue"} { + assert.Assert(t, HasResource(NewString("orange", k), oranges)) + } + + apples := GetResource("apple", res) + assert.Equal(t, len(apples), 1) + if apples[0].DiscreteResourceSpec == nil { + t.Fatalf("expected discrete resource spec, got nil") + } + assert.Equal(t, apples[0].DiscreteResourceSpec.Value, int64(3)) +} + +func TestParseMixedForSameKindFails(t *testing.T) { + _, err := ParseCmd("gpu=fast,gpu=slow,gpu=2") + assert.Assert(t, err != nil) +} diff --git a/cli/command/service/internal/genericresource/validate.go b/cli/command/service/internal/genericresource/validate.go new file mode 100644 index 000000000..40ce4ec79 --- /dev/null +++ b/cli/command/service/internal/genericresource/validate.go @@ -0,0 +1,29 @@ +package genericresource + +import ( + api "github.com/moby/moby/api/types/swarm" +) + +// HasResource checks if there is enough "res" in the "resources" argument +func HasResource(res api.GenericResource, resources []api.GenericResource) bool { + for _, r := range resources { + if equalResource(r, res) { + return true + } + } + return false +} + +// equalResource matches the resource *type* (named vs discrete), and then kind+value. +func equalResource(a, b api.GenericResource) bool { + switch { + case a.NamedResourceSpec != nil && b.NamedResourceSpec != nil: + return a.NamedResourceSpec.Kind == b.NamedResourceSpec.Kind && + a.NamedResourceSpec.Value == b.NamedResourceSpec.Value + + case a.DiscreteResourceSpec != nil && b.DiscreteResourceSpec != nil: + return a.DiscreteResourceSpec.Kind == b.DiscreteResourceSpec.Kind && + a.DiscreteResourceSpec.Value == b.DiscreteResourceSpec.Value + } + return false +} diff --git a/vendor/github.com/moby/swarmkit/v2/api/genericresource/helpers.go b/vendor/github.com/moby/swarmkit/v2/api/genericresource/helpers.go deleted file mode 100644 index 8c2910035..000000000 --- a/vendor/github.com/moby/swarmkit/v2/api/genericresource/helpers.go +++ /dev/null @@ -1,111 +0,0 @@ -package genericresource - -import ( - "github.com/moby/swarmkit/v2/api" -) - -// NewSet creates a set object -func NewSet(key string, vals ...string) []*api.GenericResource { - rs := make([]*api.GenericResource, 0, len(vals)) - - for _, v := range vals { - rs = append(rs, NewString(key, v)) - } - - return rs -} - -// NewString creates a String resource -func NewString(key, val string) *api.GenericResource { - return &api.GenericResource{ - Resource: &api.GenericResource_NamedResourceSpec{ - NamedResourceSpec: &api.NamedGenericResource{ - Kind: key, - Value: val, - }, - }, - } -} - -// NewDiscrete creates a Discrete resource -func NewDiscrete(key string, val int64) *api.GenericResource { - return &api.GenericResource{ - Resource: &api.GenericResource_DiscreteResourceSpec{ - DiscreteResourceSpec: &api.DiscreteGenericResource{ - Kind: key, - Value: val, - }, - }, - } -} - -// GetResource returns resources from the "resources" parameter matching the kind key -func GetResource(kind string, resources []*api.GenericResource) []*api.GenericResource { - var res []*api.GenericResource - - for _, r := range resources { - if Kind(r) != kind { - continue - } - - res = append(res, r) - } - - return res -} - -// ConsumeNodeResources removes "res" from nodeAvailableResources -func ConsumeNodeResources(nodeAvailableResources *[]*api.GenericResource, res []*api.GenericResource) { - if nodeAvailableResources == nil { - return - } - - w := 0 - -loop: - for _, na := range *nodeAvailableResources { - for _, r := range res { - if Kind(na) != Kind(r) { - continue - } - - if remove(na, r) { - continue loop - } - // If this wasn't the right element then - // we need to continue - } - - (*nodeAvailableResources)[w] = na - w++ - } - - *nodeAvailableResources = (*nodeAvailableResources)[:w] -} - -// Returns true if the element is to be removed from the list -func remove(na, r *api.GenericResource) bool { - switch tr := r.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - if na.GetDiscreteResourceSpec() == nil { - return false // Type change, ignore - } - - na.GetDiscreteResourceSpec().Value -= tr.DiscreteResourceSpec.Value - if na.GetDiscreteResourceSpec().Value <= 0 { - return true - } - case *api.GenericResource_NamedResourceSpec: - if na.GetNamedResourceSpec() == nil { - return false // Type change, ignore - } - - if tr.NamedResourceSpec.Value != na.GetNamedResourceSpec().Value { - return false // not the right item, ignore - } - - return true - } - - return false -} diff --git a/vendor/github.com/moby/swarmkit/v2/api/genericresource/resource_management.go b/vendor/github.com/moby/swarmkit/v2/api/genericresource/resource_management.go deleted file mode 100644 index 86a358363..000000000 --- a/vendor/github.com/moby/swarmkit/v2/api/genericresource/resource_management.go +++ /dev/null @@ -1,203 +0,0 @@ -package genericresource - -import ( - "fmt" - - "github.com/moby/swarmkit/v2/api" -) - -// Claim assigns GenericResources to a task by taking them from the -// node's GenericResource list and storing them in the task's available list -func Claim(nodeAvailableResources, taskAssigned *[]*api.GenericResource, - taskReservations []*api.GenericResource) error { - var resSelected []*api.GenericResource - - for _, res := range taskReservations { - tr := res.GetDiscreteResourceSpec() - if tr == nil { - return fmt.Errorf("task should only hold Discrete type") - } - - // Select the resources - nrs, err := selectNodeResources(*nodeAvailableResources, tr) - if err != nil { - return err - } - - resSelected = append(resSelected, nrs...) - } - - ClaimResources(nodeAvailableResources, taskAssigned, resSelected) - return nil -} - -// ClaimResources adds the specified resources to the task's list -// and removes them from the node's generic resource list -func ClaimResources(nodeAvailableResources, taskAssigned *[]*api.GenericResource, - resSelected []*api.GenericResource) { - *taskAssigned = append(*taskAssigned, resSelected...) - ConsumeNodeResources(nodeAvailableResources, resSelected) -} - -func selectNodeResources(nodeRes []*api.GenericResource, - tr *api.DiscreteGenericResource) ([]*api.GenericResource, error) { - var nrs []*api.GenericResource - - for _, res := range nodeRes { - if Kind(res) != tr.Kind { - continue - } - - switch nr := res.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - if nr.DiscreteResourceSpec.Value >= tr.Value && tr.Value != 0 { - nrs = append(nrs, NewDiscrete(tr.Kind, tr.Value)) - } - - return nrs, nil - case *api.GenericResource_NamedResourceSpec: - nrs = append(nrs, res.Copy()) - - if int64(len(nrs)) == tr.Value { - return nrs, nil - } - } - } - - if len(nrs) == 0 { - return nil, fmt.Errorf("not enough resources available for task reservations: %+v", tr) - } - - return nrs, nil -} - -// Reclaim adds the resources taken by the task to the node's store -func Reclaim(nodeAvailableResources *[]*api.GenericResource, taskAssigned, nodeRes []*api.GenericResource) error { - err := reclaimResources(nodeAvailableResources, taskAssigned) - if err != nil { - return err - } - - sanitize(nodeRes, nodeAvailableResources) - - return nil -} - -func reclaimResources(nodeAvailableResources *[]*api.GenericResource, taskAssigned []*api.GenericResource) error { - // The node could have been updated - if nodeAvailableResources == nil { - return fmt.Errorf("node no longer has any resources") - } - - for _, res := range taskAssigned { - switch tr := res.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - nrs := GetResource(tr.DiscreteResourceSpec.Kind, *nodeAvailableResources) - - // If the resource went down to 0 it's no longer in the - // available list - if len(nrs) == 0 { - *nodeAvailableResources = append(*nodeAvailableResources, res.Copy()) - } - - if len(nrs) != 1 { - continue // Type change - } - - nr := nrs[0].GetDiscreteResourceSpec() - if nr == nil { - continue // Type change - } - - nr.Value += tr.DiscreteResourceSpec.Value - case *api.GenericResource_NamedResourceSpec: - *nodeAvailableResources = append(*nodeAvailableResources, res.Copy()) - } - } - - return nil -} - -// sanitize checks that nodeAvailableResources does not add resources unknown -// to the nodeSpec (nodeRes) or goes over the integer bound specified -// by the spec. -// Note this is because the user is able to update a node's resources -func sanitize(nodeRes []*api.GenericResource, nodeAvailableResources *[]*api.GenericResource) { - // - We add the sanitized resources at the end, after - // having removed the elements from the list - - // - When a set changes to a Discrete we also need - // to make sure that we don't add the Discrete multiple - // time hence, the need of a map to remember that - var sanitized []*api.GenericResource - kindSanitized := make(map[string]struct{}) - w := 0 - - for _, na := range *nodeAvailableResources { - ok, nrs := sanitizeResource(nodeRes, na) - if !ok { - if _, ok = kindSanitized[Kind(na)]; ok { - continue - } - - kindSanitized[Kind(na)] = struct{}{} - sanitized = append(sanitized, nrs...) - - continue - } - - (*nodeAvailableResources)[w] = na - w++ - } - - *nodeAvailableResources = (*nodeAvailableResources)[:w] - *nodeAvailableResources = append(*nodeAvailableResources, sanitized...) -} - -// Returns true if the element is in nodeRes and "sane" -// Returns false if the element isn't in nodeRes and "sane" and the element(s) that should be replacing it -func sanitizeResource(nodeRes []*api.GenericResource, res *api.GenericResource) (ok bool, nrs []*api.GenericResource) { - switch na := res.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - nrs := GetResource(na.DiscreteResourceSpec.Kind, nodeRes) - - // Type change or removed: reset - if len(nrs) != 1 { - return false, nrs - } - - // Type change: reset - nr := nrs[0].GetDiscreteResourceSpec() - if nr == nil { - return false, nrs - } - - // Amount change: reset - if na.DiscreteResourceSpec.Value > nr.Value { - return false, nrs - } - case *api.GenericResource_NamedResourceSpec: - nrs := GetResource(na.NamedResourceSpec.Kind, nodeRes) - - // Type change - if len(nrs) == 0 { - return false, nrs - } - - for _, nr := range nrs { - // Type change: reset - if nr.GetDiscreteResourceSpec() != nil { - return false, nrs - } - - if na.NamedResourceSpec.Value == nr.GetNamedResourceSpec().Value { - return true, nil - } - } - - // Removed - return false, nil - } - - return true, nil -} diff --git a/vendor/github.com/moby/swarmkit/v2/api/genericresource/string.go b/vendor/github.com/moby/swarmkit/v2/api/genericresource/string.go deleted file mode 100644 index ded52ffed..000000000 --- a/vendor/github.com/moby/swarmkit/v2/api/genericresource/string.go +++ /dev/null @@ -1,54 +0,0 @@ -package genericresource - -import ( - "strconv" - "strings" - - "github.com/moby/swarmkit/v2/api" -) - -func discreteToString(d *api.GenericResource_DiscreteResourceSpec) string { - return strconv.FormatInt(d.DiscreteResourceSpec.Value, 10) -} - -// Kind returns the kind key as a string -func Kind(res *api.GenericResource) string { - switch r := res.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - return r.DiscreteResourceSpec.Kind - case *api.GenericResource_NamedResourceSpec: - return r.NamedResourceSpec.Kind - } - - return "" -} - -// Value returns the value key as a string -func Value(res *api.GenericResource) string { - switch res := res.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - return discreteToString(res) - case *api.GenericResource_NamedResourceSpec: - return res.NamedResourceSpec.Value - } - - return "" -} - -// EnvFormat returns the environment string version of the resource -func EnvFormat(res []*api.GenericResource, prefix string) []string { - envs := make(map[string][]string) - for _, v := range res { - key := Kind(v) - val := Value(v) - envs[key] = append(envs[key], val) - } - - env := make([]string, 0, len(res)) - for k, v := range envs { - k = strings.ToUpper(prefix + "_" + k) - env = append(env, k+"="+strings.Join(v, ",")) - } - - return env -} diff --git a/vendor/github.com/moby/swarmkit/v2/api/genericresource/validate.go b/vendor/github.com/moby/swarmkit/v2/api/genericresource/validate.go deleted file mode 100644 index 909ac3e7e..000000000 --- a/vendor/github.com/moby/swarmkit/v2/api/genericresource/validate.go +++ /dev/null @@ -1,85 +0,0 @@ -package genericresource - -import ( - "fmt" - - "github.com/moby/swarmkit/v2/api" -) - -// ValidateTask validates that the task only uses integers -// for generic resources -func ValidateTask(resources *api.Resources) error { - for _, v := range resources.Generic { - if v.GetDiscreteResourceSpec() != nil { - continue - } - - return fmt.Errorf("invalid argument for resource %s", Kind(v)) - } - - return nil -} - -// HasEnough returns true if node can satisfy the task's GenericResource request -func HasEnough(nodeRes []*api.GenericResource, taskRes *api.GenericResource) (bool, error) { - t := taskRes.GetDiscreteResourceSpec() - if t == nil { - return false, fmt.Errorf("task should only hold Discrete type") - } - - if nodeRes == nil { - return false, nil - } - - nrs := GetResource(t.Kind, nodeRes) - if len(nrs) == 0 { - return false, nil - } - - switch nr := nrs[0].Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - if t.Value > nr.DiscreteResourceSpec.Value { - return false, nil - } - case *api.GenericResource_NamedResourceSpec: - if t.Value > int64(len(nrs)) { - return false, nil - } - } - - return true, nil -} - -// HasResource checks if there is enough "res" in the "resources" argument -func HasResource(res *api.GenericResource, resources []*api.GenericResource) bool { - for _, r := range resources { - if Kind(res) != Kind(r) { - continue - } - - switch rtype := r.Resource.(type) { - case *api.GenericResource_DiscreteResourceSpec: - if res.GetDiscreteResourceSpec() == nil { - return false - } - - if res.GetDiscreteResourceSpec().Value > rtype.DiscreteResourceSpec.Value { - return false - } - - return true - case *api.GenericResource_NamedResourceSpec: - if res.GetNamedResourceSpec() == nil { - return false - } - - if res.GetNamedResourceSpec().Value != rtype.NamedResourceSpec.Value { - continue - } - - return true - } - } - - return false -} diff --git a/vendor/modules.txt b/vendor/modules.txt index f6bf3a277..d30f213cf 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -214,7 +214,6 @@ github.com/moby/patternmatcher/ignorefile github.com/moby/swarmkit/v2/api github.com/moby/swarmkit/v2/api/deepcopy github.com/moby/swarmkit/v2/api/defaults -github.com/moby/swarmkit/v2/api/genericresource github.com/moby/swarmkit/v2/manager/raftselector github.com/moby/swarmkit/v2/protobuf/plugin # github.com/moby/sys/atomicwriter v0.1.0