diff --git a/go.mod b/go.mod index 90a691fe8b..37d5c21697 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,12 @@ require ( github.com/containers/image v3.0.2+incompatible github.com/containers/storage v1.13.5 github.com/coreos/container-linux-config-transpiler v0.9.0 + github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f github.com/coreos/ignition v0.33.0 github.com/creack/pty v1.1.9 // indirect github.com/cri-o/cri-o v1.16.2 github.com/davecgh/go-spew v1.1.1 + github.com/deckarep/golang-set v1.7.1 github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 // indirect github.com/emicklei/go-restful v2.10.0+incompatible // indirect github.com/ghodss/yaml v1.0.0 diff --git a/go.sum b/go.sum index 7aa53bb240..c972f0e411 100644 --- a/go.sum +++ b/go.sum @@ -216,6 +216,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= +github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/distribution v0.0.0-20170817175659-5f6282db7d65/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= diff --git a/pkg/daemon/reboot_filter.go b/pkg/daemon/reboot_filter.go new file mode 100644 index 0000000000..830f908819 --- /dev/null +++ b/pkg/daemon/reboot_filter.go @@ -0,0 +1,385 @@ +package daemon + +import ( + "fmt" + "os/exec" + "path/filepath" + // "reflect" + + systemdDbus "github.com/coreos/go-systemd/dbus" + igntypes "github.com/coreos/ignition/config/v2_2/types" + "github.com/deckarep/golang-set" + "github.com/golang/glog" +) + +type FileFilterEntry struct { + glob string + postUpdateAction PostUpdateAction +} + +type UnitFilterEntry struct { + name string + drainRequired bool +} + +type AvoidRebootConfig struct { + // Files filter which do not require reboot + Files []*FileFilterEntry + // List of systemd unit that do not require system reboot, but rather just unit restart + Units []*UnitFilterEntry +} + +// TODO: create a proper filter config as this one is just a testing one +var filterConfig = AvoidRebootConfig{ + Files: []*FileFilterEntry{ + &FileFilterEntry{ + glob: "/home/core/testfile", + postUpdateAction: RunBinaryAction{ + binary: "/bin/bash", + args: []string{ + "-c", + "echo \"$(date)\" >> /home/core/testfile.out", + }, + DrainRequired: DrainRequired{drainRequired: false}, + }, + }, + &FileFilterEntry{ + glob: "/home/core/drain_required", + postUpdateAction: RunBinaryAction{ + binary: "/bin/bash", + args: []string{ + "-c", + "echo \"$(date)\" >> /home/core/drain_required.out", + }, + DrainRequired: DrainRequired{drainRequired: true}, + }, + }, + }, + Units: []*UnitFilterEntry{ + &UnitFilterEntry{ + name: "test-service-drain.service", + drainRequired: true, + }, + &UnitFilterEntry{ + name: "test-service.service", + drainRequired: false, + }, + }, +} + +func (config AvoidRebootConfig) getFileAction(filePath string) PostUpdateAction { + for _, entry := range config.Files { + matched, err := filepath.Match(entry.glob, filePath) + if err != nil { + // TODO: log + continue + } + if matched { + return entry.postUpdateAction + } + } + return nil +} + +func (config AvoidRebootConfig) getUnitAction(unit igntypes.Unit, systemdConnection *systemdDbus.Conn) PostUpdateAction { + for _, entry := range config.Units { + if entry.name == unit.Name { + // same logic like in writeUnits() + enableUnit := false + if unit.Enable { + enableUnit = true + } else if unit.Enabled != nil { + enableUnit = *unit.Enabled + } + return SystemdAction{ + unit.Name, + unitRestart, + enableUnit, + systemdConnection, + DrainRequired{drainRequired: entry.drainRequired}, + } + } + } + return nil +} + +type PostUpdateAction interface { + Run() error + getIsDrainRequired() bool +} + +type DrainRequired struct { + drainRequired bool +} + +func (idr DrainRequired) getIsDrainRequired() bool { + return idr.drainRequired +} + +type RunBinaryAction struct { + binary string + args []string + DrainRequired +} + +func (action RunBinaryAction) Run() error { + glog.Infof( + "Running post update action: running command: %v %v", action.binary, action.args, + ) + output, err := exec.Command(action.binary, action.args...).CombinedOutput() + // TODO: Add some timeout? + if err != nil { + glog.Errorf("Running post update action (running command: '%s %s') failed: %s; command output: %s", action.binary, action.args, err, output) + return err + } + return nil +} + +type UnitOperation string + +const ( + unitRestart UnitOperation = "restart" + unitReload UnitOperation = "reload" +) + +type SystemdAction struct { + unitName string + operation UnitOperation + enabled bool + systemdConnection *systemdDbus.Conn + DrainRequired +} + +func (action SystemdAction) Run() error { + // TODO: add support for reload operation + // For now only restart operation is supported + if action.systemdConnection == nil { + return fmt.Errorf( + "Unable to run post update action for unit %q: systemd dbus connection not specified", + action.unitName, + ) + } + var err error + outputChannel := make(chan string) + if action.enabled { + glog.Infof("Restarting unit %q", action.unitName) + _, err = action.systemdConnection.RestartUnit(action.unitName, "replace", outputChannel) + } else { + glog.Infof("Stopping unit %q", action.unitName) + _, err = action.systemdConnection.StopUnit(action.unitName, "replace", outputChannel) + } + if err != nil { + return fmt.Errorf("Running systemd action failed: %s", err) + } + output := <-outputChannel + + switch output { + case "done": + fallthrough + case "skipped": + glog.Infof("Systemd action successful: %s", output) + default: + return fmt.Errorf("Systemd action %s", output) + } + return nil +} + +type ChangeType string + +const ( + changeCreated ChangeType = "created" + changeDeleted ChangeType = "deleted" + changeUpdated ChangeType = "updated" +) + +type FileChange struct { + name string + file igntypes.File + changeType ChangeType +} + +func getFileNames(files []igntypes.File) []interface{} { + names := make([]interface{}, len(files)) + for i, file := range files { + names[i] = file.Path + } + return names +} + +func filesToMap(files []igntypes.File) map[string]igntypes.File { + fileMap := make(map[string]igntypes.File, len(files)) + for _, file := range files { + fileMap[file.Path] = file + } + return fileMap +} + +func getFilesChanges(oldFilesConfig, newFilesConfig []igntypes.File) []*FileChange { + oldFiles := mapset.NewSetFromSlice(getFileNames(oldFilesConfig)) + oldFilesMap := filesToMap(oldFilesConfig) + newFiles := mapset.NewSetFromSlice(getFileNames(newFilesConfig)) + newFilesMap := filesToMap(newFilesConfig) + changes := make([]*FileChange, 0, newFiles.Cardinality()) + for created := range newFiles.Difference(oldFiles).Iter() { + changes = append(changes, &FileChange{ + name: created.(string), + file: newFilesMap[created.(string)], + changeType: changeCreated, + }) + } + for deleted := range oldFiles.Difference(newFiles).Iter() { + changes = append(changes, &FileChange{ + name: deleted.(string), + file: oldFilesMap[deleted.(string)], + changeType: changeDeleted, + }) + } + for changeCandidate := range newFiles.Intersect(oldFiles).Iter() { + newFile := newFilesMap[changeCandidate.(string)] + // if !reflect.DeepEqual(newFile, oldFilesMap[changeCandidate.(string)]) { + if !checkFiles([]igntypes.File{newFile}) { + changes = append(changes, &FileChange{ + name: changeCandidate.(string), + file: newFile, + changeType: changeUpdated, + }) + } + } + return changes +} + +type UnitChange struct { + name string + oldUnit igntypes.Unit + newUnit igntypes.Unit + changeType ChangeType +} + +func getUnitNames(units []igntypes.Unit) []interface{} { + names := make([]interface{}, len(units)) + for i, unit := range units { + names[i] = unit.Name + } + return names +} + +func unitsToMap(units []igntypes.Unit) map[string]igntypes.Unit { + unitMap := make(map[string]igntypes.Unit, len(units)) + for _, unit := range units { + unitMap[unit.Name] = unit + } + return unitMap +} + +func getUnitsChanges(oldUnitsConfig, newUnitsConfig []igntypes.Unit) []*UnitChange { + oldUnits := mapset.NewSetFromSlice(getUnitNames(oldUnitsConfig)) + oldUnitsMap := unitsToMap(oldUnitsConfig) + newUnits := mapset.NewSetFromSlice(getUnitNames(newUnitsConfig)) + newUnitsMap := unitsToMap(newUnitsConfig) + changes := make([]*UnitChange, 0, newUnits.Cardinality()) + for created := range newUnits.Difference(oldUnits).Iter() { + changes = append(changes, &UnitChange{ + name: created.(string), + newUnit: newUnitsMap[created.(string)], + changeType: changeCreated, + }) + } + for deleted := range oldUnits.Difference(newUnits).Iter() { + changes = append(changes, &UnitChange{ + name: deleted.(string), + oldUnit: oldUnitsMap[deleted.(string)], + changeType: changeDeleted, + }) + } + for changeCandidate := range newUnits.Intersect(oldUnits).Iter() { + changedUnitName := changeCandidate.(string) + newUnit := newUnitsMap[changedUnitName] + oldUnit := oldUnitsMap[changedUnitName] + // if !reflect.DeepEqual(newUnit, oldUnit) { + if !checkUnits([]igntypes.Unit{newUnit}) { + changes = append(changes, &UnitChange{ + name: changedUnitName, + newUnit: newUnit, + oldUnit: oldUnit, + changeType: changeUpdated, + }) + } + } + return changes +} + +func getPostUpdateActions(filesChanges []*FileChange, unitsChanges []*UnitChange, systemdConnection *systemdDbus.Conn) ([]PostUpdateAction, error) { + glog.Info("Trying to check whether changes in files and units require system reboot.") + actions := make([]PostUpdateAction, 0, len(filesChanges)+len(unitsChanges)) + rebootRequiredMsg := ", reboot will be required" + for _, change := range filesChanges { + switch change.changeType { + case changeCreated: + fallthrough + case changeUpdated: + action := filterConfig.getFileAction(change.name) + if action == nil { + err := fmt.Errorf("No action found for file %q", change.name) + glog.Infof("%s%s", err, rebootRequiredMsg) + return nil, err + } + actions = append(actions, action) + glog.Infof("Action found for file %q", change.name) + default: + err := fmt.Errorf("File %q was removed", change.name) + glog.Infof("%s%s", err, rebootRequiredMsg) + return nil, err + } + } + + for _, change := range unitsChanges { + switch change.changeType { + case changeCreated: + fallthrough + case changeUpdated: + action := filterConfig.getUnitAction(change.newUnit, systemdConnection) + if action == nil { + err := fmt.Errorf("No action found for unit %q", change.name) + glog.Infof("%s%s", err, rebootRequiredMsg) + return nil, err + } + if systemdConnection == nil { + err := fmt.Errorf( + "Missing systemd connection for running post update action for unit %q", + change.name, + ) + glog.Errorf("%s%s", err, rebootRequiredMsg) + return nil, err + } + actions = append(actions, action) + glog.Infof("Action found for unit %q", change.name) + default: + err := fmt.Errorf("Unit %q was removed", change.name) + glog.Infof("%s%s", err, rebootRequiredMsg) + return nil, err + } + } + return actions, nil +} + +func isDrainRequired(actions []PostUpdateAction) bool { + isRequired := false + for _, action := range actions { + isRequired = isRequired || action.getIsDrainRequired() + } + return isRequired +} + +// returns true in case reboot is required (some actions failed), false +// otherwise +func runPostUpdateActions(actions []PostUpdateAction) bool { + glog.Infof("Running %d post update action(s)...", len(actions)) + for _, action := range actions { + if err := action.Run(); err != nil { + glog.Errorf("Post update action failed: %s", err) + return true + } + } + glog.Info("Running post update Actions were sucessfull") + return false +} diff --git a/pkg/daemon/update.go b/pkg/daemon/update.go index 08f685eebf..496187c480 100644 --- a/pkg/daemon/update.go +++ b/pkg/daemon/update.go @@ -17,6 +17,7 @@ import ( "syscall" "time" + systemdDbus "github.com/coreos/go-systemd/dbus" ign "github.com/coreos/ignition/config/v2_2" igntypes "github.com/coreos/ignition/config/v2_2/types" "github.com/golang/glog" @@ -123,6 +124,13 @@ func getNodeRef(node *corev1.Node) *corev1.ObjectReference { } } +func (dn *Daemon) drainAndReboot(newConfig *mcfgv1.MachineConfig) error { + if err := dn.drain(); err != nil { + return err + } + return dn.finalizeAndReboot(newConfig) +} + // updateOSAndReboot is the last step in an update(), and it can also // be called as a special case for the "bootstrap pivot". func (dn *Daemon) updateOSAndReboot(newConfig *mcfgv1.MachineConfig) (retErr error) { @@ -316,10 +324,47 @@ func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig) (retErr err return errors.Wrapf(errUnreconcilable, "%v", wrappedErr) } + oldIgnConfig, report, err := ign.Parse(oldConfig.Spec.Config.Raw) + if err != nil { + return fmt.Errorf("parsing old Ignition config failed with error: %v\nReport: %v", err, report) + } + newIgnConfig, report, err := ign.Parse(newConfig.Spec.Config.Raw) + if err != nil { + return fmt.Errorf("parsing new Ignition config failed with error: %v\nReport: %v", err, report) + } + dn.logSystem("Starting update from %s to %s: %+v", oldConfigName, newConfigName, diff) + rebootRequired := diff.osUpdate || diff.kargs || diff.fips || diff.kernelType - if err := dn.drain(); err != nil { - return err + systemdConnection, dbusConnErr := systemdDbus.NewSystemConnection() + if dbusConnErr == nil { + defer systemdConnection.Close() + } else { + glog.Warningf("Unable to establish systemd dbus connection: %s", dbusConnErr) + // No more actions needed here as a systemd connection is not always + // required (only if there is systemd related post update action + // present). If a connection should be required, getPostUpdateActions + // function will return error if nil connection is provided and then + // rebootRequired will be se to true + } + + postUpdateActions, err := getPostUpdateActions( + getFilesChanges(oldIgnConfig.Storage.Files, newIgnConfig.Storage.Files), + getUnitsChanges(oldIgnConfig.Systemd.Units, newIgnConfig.Systemd.Units), + systemdConnection, + ) + if err != nil { + rebootRequired = true + } + + drainRequired := rebootRequired || isDrainRequired(postUpdateActions) + + if drainRequired { + if err := dn.drain(); err != nil { + return err + } + } else { + glog.Info("Draining node skipped as it is not required") } // update files on disk that need updating @@ -336,15 +381,6 @@ func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig) (retErr err } }() - oldIgnConfig, report, err := ign.Parse(oldConfig.Spec.Config.Raw) - if err != nil { - return fmt.Errorf("parsing old Ignition config failed with error: %v\nReport: %v", err, report) - } - newIgnConfig, report, err := ign.Parse(newConfig.Spec.Config.Raw) - if err != nil { - return fmt.Errorf("parsing new Ignition config failed with error: %v\nReport: %v", err, report) - } - if err := dn.updateSSHKeys(newIgnConfig.Passwd.Users); err != nil { return err } @@ -397,7 +433,32 @@ func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig) (retErr err } }() - return dn.updateOSAndReboot(newConfig) + if err := dn.updateOS(newConfig); err != nil { + return err + } + if rebootRequired || runPostUpdateActions(postUpdateActions) { + return dn.drainAndReboot(newConfig) + } + glog.Info("Reboot skipped as it is not required") + if err := dn.nodeWriter.SetDone( + dn.kubeClient.CoreV1().Nodes(), + dn.nodeLister, + dn.name, + newConfigName, + ); err != nil { + glog.Errorf("Setting node's state to Done failed, node will reboot: %v", err) + return dn.drainAndReboot(newConfig) + } + if drainRequired { + glog.Infof("Starting uncordoning node %v", dn.node.GetName()) + if err := drain.RunCordonOrUncordon(dn.drainer, dn.node, false); err != nil { + glog.Errorf("Uncordoning node failed, node will reboot: %v", err) + return dn.finalizeAndReboot(newConfig) + } + } + glog.Infof("In desired config %s", newConfigName) + MCDUpdateState.WithLabelValues(newConfigName, "").SetToCurrentTime() + return nil } // MachineConfigDiff represents an ad-hoc difference between two MachineConfig objects. diff --git a/vendor/github.com/deckarep/golang-set/.gitignore b/vendor/github.com/deckarep/golang-set/.gitignore new file mode 100644 index 0000000000..00268614f0 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/deckarep/golang-set/.travis.yml b/vendor/github.com/deckarep/golang-set/.travis.yml new file mode 100644 index 0000000000..c760d24d1e --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/.travis.yml @@ -0,0 +1,11 @@ +language: go + +go: + - 1.8 + - 1.9 + - tip + +script: + - go test -race ./... + - go test -bench=. + diff --git a/vendor/github.com/deckarep/golang-set/LICENSE b/vendor/github.com/deckarep/golang-set/LICENSE new file mode 100644 index 0000000000..b5768f89cf --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/LICENSE @@ -0,0 +1,22 @@ +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/deckarep/golang-set/README.md b/vendor/github.com/deckarep/golang-set/README.md new file mode 100644 index 0000000000..c3b50b2c5c --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/README.md @@ -0,0 +1,95 @@ +[![Build Status](https://travis-ci.org/deckarep/golang-set.svg?branch=master)](https://travis-ci.org/deckarep/golang-set) +[![Go Report Card](https://goreportcard.com/badge/github.com/deckarep/golang-set)](https://goreportcard.com/report/github.com/deckarep/golang-set) +[![GoDoc](https://godoc.org/github.com/deckarep/golang-set?status.svg)](http://godoc.org/github.com/deckarep/golang-set) + +## golang-set + + +The missing set collection for the Go language. Until Go has sets built-in...use this. + +Coming from Python one of the things I miss is the superbly wonderful set collection. This is my attempt to mimic the primary features of the set from Python. +You can of course argue that there is no need for a set in Go, otherwise the creators would have added one to the standard library. To those I say simply ignore this repository +and carry-on and to the rest that find this useful please contribute in helping me make it better by: + +* Helping to make more idiomatic improvements to the code. +* Helping to increase the performance of it. ~~(So far, no attempt has been made, but since it uses a map internally, I expect it to be mostly performant.)~~ +* Helping to make the unit-tests more robust and kick-ass. +* Helping to fill in the [documentation.](http://godoc.org/github.com/deckarep/golang-set) +* Simply offering feedback and suggestions. (Positive, constructive feedback is appreciated.) + +I have to give some credit for helping seed the idea with this post on [stackoverflow.](http://programmers.stackexchange.com/questions/177428/sets-data-structure-in-golang) + +*Update* - as of 3/9/2014, you can use a compile-time generic version of this package in the [gen](http://clipperhouse.github.io/gen/) framework. This framework allows you to use the golang-set in a completely generic and type-safe way by allowing you to generate a supporting .go file based on your custom types. + +## Features (as of 9/22/2014) + +* a CartesianProduct() method has been added with unit-tests: [Read more about the cartesian product](http://en.wikipedia.org/wiki/Cartesian_product) + +## Features (as of 9/15/2014) + +* a PowerSet() method has been added with unit-tests: [Read more about the Power set](http://en.wikipedia.org/wiki/Power_set) + +## Features (as of 4/22/2014) + +* One common interface to both implementations +* Two set implementations to choose from + * a thread-safe implementation designed for concurrent use + * a non-thread-safe implementation designed for performance +* 75 benchmarks for both implementations +* 35 unit tests for both implementations +* 14 concurrent tests for the thread-safe implementation + + + +Please see the unit test file for additional usage examples. The Python set documentation will also do a better job than I can of explaining how a set typically [works.](http://docs.python.org/2/library/sets.html) Please keep in mind +however that the Python set is a built-in type and supports additional features and syntax that make it awesome. + +## Examples but not exhaustive: + +```go +requiredClasses := mapset.NewSet() +requiredClasses.Add("Cooking") +requiredClasses.Add("English") +requiredClasses.Add("Math") +requiredClasses.Add("Biology") + +scienceSlice := []interface{}{"Biology", "Chemistry"} +scienceClasses := mapset.NewSetFromSlice(scienceSlice) + +electiveClasses := mapset.NewSet() +electiveClasses.Add("Welding") +electiveClasses.Add("Music") +electiveClasses.Add("Automotive") + +bonusClasses := mapset.NewSet() +bonusClasses.Add("Go Programming") +bonusClasses.Add("Python Programming") + +//Show me all the available classes I can take +allClasses := requiredClasses.Union(scienceClasses).Union(electiveClasses).Union(bonusClasses) +fmt.Println(allClasses) //Set{Cooking, English, Math, Chemistry, Welding, Biology, Music, Automotive, Go Programming, Python Programming} + + +//Is cooking considered a science class? +fmt.Println(scienceClasses.Contains("Cooking")) //false + +//Show me all classes that are not science classes, since I hate science. +fmt.Println(allClasses.Difference(scienceClasses)) //Set{Music, Automotive, Go Programming, Python Programming, Cooking, English, Math, Welding} + +//Which science classes are also required classes? +fmt.Println(scienceClasses.Intersect(requiredClasses)) //Set{Biology} + +//How many bonus classes do you offer? +fmt.Println(bonusClasses.Cardinality()) //2 + +//Do you have the following classes? Welding, Automotive and English? +fmt.Println(allClasses.IsSuperset(mapset.NewSetFromSlice([]interface{}{"Welding", "Automotive", "English"}))) //true +``` + +Thanks! + +-Ralph + +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/deckarep/golang-set/trend.png)](https://bitdeli.com/free "Bitdeli Badge") + +[![Analytics](https://ga-beacon.appspot.com/UA-42584447-2/deckarep/golang-set)](https://github.com/igrigorik/ga-beacon) diff --git a/vendor/github.com/deckarep/golang-set/iterator.go b/vendor/github.com/deckarep/golang-set/iterator.go new file mode 100644 index 0000000000..9dfecade42 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/iterator.go @@ -0,0 +1,58 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +// Iterator defines an iterator over a Set, its C channel can be used to range over the Set's +// elements. +type Iterator struct { + C <-chan interface{} + stop chan struct{} +} + +// Stop stops the Iterator, no further elements will be received on C, C will be closed. +func (i *Iterator) Stop() { + // Allows for Stop() to be called multiple times + // (close() panics when called on already closed channel) + defer func() { + recover() + }() + + close(i.stop) + + // Exhaust any remaining elements. + for range i.C { + } +} + +// newIterator returns a new Iterator instance together with its item and stop channels. +func newIterator() (*Iterator, chan<- interface{}, <-chan struct{}) { + itemChan := make(chan interface{}) + stopChan := make(chan struct{}) + return &Iterator{ + C: itemChan, + stop: stopChan, + }, itemChan, stopChan +} diff --git a/vendor/github.com/deckarep/golang-set/set.go b/vendor/github.com/deckarep/golang-set/set.go new file mode 100644 index 0000000000..29eb2e5a22 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/set.go @@ -0,0 +1,217 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Package mapset implements a simple and generic set collection. +// Items stored within it are unordered and unique. It supports +// typical set operations: membership testing, intersection, union, +// difference, symmetric difference and cloning. +// +// Package mapset provides two implementations of the Set +// interface. The default implementation is safe for concurrent +// access, but a non-thread-safe implementation is also provided for +// programs that can benefit from the slight speed improvement and +// that can enforce mutual exclusion through other means. +package mapset + +// Set is the primary interface provided by the mapset package. It +// represents an unordered set of data and a large number of +// operations that can be applied to that set. +type Set interface { + // Adds an element to the set. Returns whether + // the item was added. + Add(i interface{}) bool + + // Returns the number of elements in the set. + Cardinality() int + + // Removes all elements from the set, leaving + // the empty set. + Clear() + + // Returns a clone of the set using the same + // implementation, duplicating all keys. + Clone() Set + + // Returns whether the given items + // are all in the set. + Contains(i ...interface{}) bool + + // Returns the difference between this set + // and other. The returned set will contain + // all elements of this set that are not also + // elements of other. + // + // Note that the argument to Difference + // must be of the same type as the receiver + // of the method. Otherwise, Difference will + // panic. + Difference(other Set) Set + + // Determines if two sets are equal to each + // other. If they have the same cardinality + // and contain the same elements, they are + // considered equal. The order in which + // the elements were added is irrelevant. + // + // Note that the argument to Equal must be + // of the same type as the receiver of the + // method. Otherwise, Equal will panic. + Equal(other Set) bool + + // Returns a new set containing only the elements + // that exist only in both sets. + // + // Note that the argument to Intersect + // must be of the same type as the receiver + // of the method. Otherwise, Intersect will + // panic. + Intersect(other Set) Set + + // Determines if every element in this set is in + // the other set but the two sets are not equal. + // + // Note that the argument to IsProperSubset + // must be of the same type as the receiver + // of the method. Otherwise, IsProperSubset + // will panic. + IsProperSubset(other Set) bool + + // Determines if every element in the other set + // is in this set but the two sets are not + // equal. + // + // Note that the argument to IsSuperset + // must be of the same type as the receiver + // of the method. Otherwise, IsSuperset will + // panic. + IsProperSuperset(other Set) bool + + // Determines if every element in this set is in + // the other set. + // + // Note that the argument to IsSubset + // must be of the same type as the receiver + // of the method. Otherwise, IsSubset will + // panic. + IsSubset(other Set) bool + + // Determines if every element in the other set + // is in this set. + // + // Note that the argument to IsSuperset + // must be of the same type as the receiver + // of the method. Otherwise, IsSuperset will + // panic. + IsSuperset(other Set) bool + + // Iterates over elements and executes the passed func against each element. + // If passed func returns true, stop iteration at the time. + Each(func(interface{}) bool) + + // Returns a channel of elements that you can + // range over. + Iter() <-chan interface{} + + // Returns an Iterator object that you can + // use to range over the set. + Iterator() *Iterator + + // Remove a single element from the set. + Remove(i interface{}) + + // Provides a convenient string representation + // of the current state of the set. + String() string + + // Returns a new set with all elements which are + // in either this set or the other set but not in both. + // + // Note that the argument to SymmetricDifference + // must be of the same type as the receiver + // of the method. Otherwise, SymmetricDifference + // will panic. + SymmetricDifference(other Set) Set + + // Returns a new set with all elements in both sets. + // + // Note that the argument to Union must be of the + + // same type as the receiver of the method. + // Otherwise, IsSuperset will panic. + Union(other Set) Set + + // Pop removes and returns an arbitrary item from the set. + Pop() interface{} + + // Returns all subsets of a given set (Power Set). + PowerSet() Set + + // Returns the Cartesian Product of two sets. + CartesianProduct(other Set) Set + + // Returns the members of the set as a slice. + ToSlice() []interface{} +} + +// NewSet creates and returns a reference to an empty set. Operations +// on the resulting set are thread-safe. +func NewSet(s ...interface{}) Set { + set := newThreadSafeSet() + for _, item := range s { + set.Add(item) + } + return &set +} + +// NewSetWith creates and returns a new set with the given elements. +// Operations on the resulting set are thread-safe. +func NewSetWith(elts ...interface{}) Set { + return NewSetFromSlice(elts) +} + +// NewSetFromSlice creates and returns a reference to a set from an +// existing slice. Operations on the resulting set are thread-safe. +func NewSetFromSlice(s []interface{}) Set { + a := NewSet(s...) + return a +} + +// NewThreadUnsafeSet creates and returns a reference to an empty set. +// Operations on the resulting set are not thread-safe. +func NewThreadUnsafeSet() Set { + set := newThreadUnsafeSet() + return &set +} + +// NewThreadUnsafeSetFromSlice creates and returns a reference to a +// set from an existing slice. Operations on the resulting set are +// not thread-safe. +func NewThreadUnsafeSetFromSlice(s []interface{}) Set { + a := NewThreadUnsafeSet() + for _, item := range s { + a.Add(item) + } + return a +} diff --git a/vendor/github.com/deckarep/golang-set/threadsafe.go b/vendor/github.com/deckarep/golang-set/threadsafe.go new file mode 100644 index 0000000000..269b4ab0cb --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/threadsafe.go @@ -0,0 +1,283 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +import "sync" + +type threadSafeSet struct { + s threadUnsafeSet + sync.RWMutex +} + +func newThreadSafeSet() threadSafeSet { + return threadSafeSet{s: newThreadUnsafeSet()} +} + +func (set *threadSafeSet) Add(i interface{}) bool { + set.Lock() + ret := set.s.Add(i) + set.Unlock() + return ret +} + +func (set *threadSafeSet) Contains(i ...interface{}) bool { + set.RLock() + ret := set.s.Contains(i...) + set.RUnlock() + return ret +} + +func (set *threadSafeSet) IsSubset(other Set) bool { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + ret := set.s.IsSubset(&o.s) + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) IsProperSubset(other Set) bool { + o := other.(*threadSafeSet) + + set.RLock() + defer set.RUnlock() + o.RLock() + defer o.RUnlock() + + return set.s.IsProperSubset(&o.s) +} + +func (set *threadSafeSet) IsSuperset(other Set) bool { + return other.IsSubset(set) +} + +func (set *threadSafeSet) IsProperSuperset(other Set) bool { + return other.IsProperSubset(set) +} + +func (set *threadSafeSet) Union(other Set) Set { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + unsafeUnion := set.s.Union(&o.s).(*threadUnsafeSet) + ret := &threadSafeSet{s: *unsafeUnion} + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) Intersect(other Set) Set { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + unsafeIntersection := set.s.Intersect(&o.s).(*threadUnsafeSet) + ret := &threadSafeSet{s: *unsafeIntersection} + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) Difference(other Set) Set { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + unsafeDifference := set.s.Difference(&o.s).(*threadUnsafeSet) + ret := &threadSafeSet{s: *unsafeDifference} + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) SymmetricDifference(other Set) Set { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + unsafeDifference := set.s.SymmetricDifference(&o.s).(*threadUnsafeSet) + ret := &threadSafeSet{s: *unsafeDifference} + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) Clear() { + set.Lock() + set.s = newThreadUnsafeSet() + set.Unlock() +} + +func (set *threadSafeSet) Remove(i interface{}) { + set.Lock() + delete(set.s, i) + set.Unlock() +} + +func (set *threadSafeSet) Cardinality() int { + set.RLock() + defer set.RUnlock() + return len(set.s) +} + +func (set *threadSafeSet) Each(cb func(interface{}) bool) { + set.RLock() + for elem := range set.s { + if cb(elem) { + break + } + } + set.RUnlock() +} + +func (set *threadSafeSet) Iter() <-chan interface{} { + ch := make(chan interface{}) + go func() { + set.RLock() + + for elem := range set.s { + ch <- elem + } + close(ch) + set.RUnlock() + }() + + return ch +} + +func (set *threadSafeSet) Iterator() *Iterator { + iterator, ch, stopCh := newIterator() + + go func() { + set.RLock() + L: + for elem := range set.s { + select { + case <-stopCh: + break L + case ch <- elem: + } + } + close(ch) + set.RUnlock() + }() + + return iterator +} + +func (set *threadSafeSet) Equal(other Set) bool { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + ret := set.s.Equal(&o.s) + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) Clone() Set { + set.RLock() + + unsafeClone := set.s.Clone().(*threadUnsafeSet) + ret := &threadSafeSet{s: *unsafeClone} + set.RUnlock() + return ret +} + +func (set *threadSafeSet) String() string { + set.RLock() + ret := set.s.String() + set.RUnlock() + return ret +} + +func (set *threadSafeSet) PowerSet() Set { + set.RLock() + unsafePowerSet := set.s.PowerSet().(*threadUnsafeSet) + set.RUnlock() + + ret := &threadSafeSet{s: newThreadUnsafeSet()} + for subset := range unsafePowerSet.Iter() { + unsafeSubset := subset.(*threadUnsafeSet) + ret.Add(&threadSafeSet{s: *unsafeSubset}) + } + return ret +} + +func (set *threadSafeSet) Pop() interface{} { + set.Lock() + defer set.Unlock() + return set.s.Pop() +} + +func (set *threadSafeSet) CartesianProduct(other Set) Set { + o := other.(*threadSafeSet) + + set.RLock() + o.RLock() + + unsafeCartProduct := set.s.CartesianProduct(&o.s).(*threadUnsafeSet) + ret := &threadSafeSet{s: *unsafeCartProduct} + set.RUnlock() + o.RUnlock() + return ret +} + +func (set *threadSafeSet) ToSlice() []interface{} { + keys := make([]interface{}, 0, set.Cardinality()) + set.RLock() + for elem := range set.s { + keys = append(keys, elem) + } + set.RUnlock() + return keys +} + +func (set *threadSafeSet) MarshalJSON() ([]byte, error) { + set.RLock() + b, err := set.s.MarshalJSON() + set.RUnlock() + + return b, err +} + +func (set *threadSafeSet) UnmarshalJSON(p []byte) error { + set.RLock() + err := set.s.UnmarshalJSON(p) + set.RUnlock() + + return err +} diff --git a/vendor/github.com/deckarep/golang-set/threadunsafe.go b/vendor/github.com/deckarep/golang-set/threadunsafe.go new file mode 100644 index 0000000000..10bdd46f15 --- /dev/null +++ b/vendor/github.com/deckarep/golang-set/threadunsafe.go @@ -0,0 +1,337 @@ +/* +Open Source Initiative OSI - The MIT License (MIT):Licensing + +The MIT License (MIT) +Copyright (c) 2013 Ralph Caraveo (deckarep@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package mapset + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +type threadUnsafeSet map[interface{}]struct{} + +// An OrderedPair represents a 2-tuple of values. +type OrderedPair struct { + First interface{} + Second interface{} +} + +func newThreadUnsafeSet() threadUnsafeSet { + return make(threadUnsafeSet) +} + +// Equal says whether two 2-tuples contain the same values in the same order. +func (pair *OrderedPair) Equal(other OrderedPair) bool { + if pair.First == other.First && + pair.Second == other.Second { + return true + } + + return false +} + +func (set *threadUnsafeSet) Add(i interface{}) bool { + _, found := (*set)[i] + if found { + return false //False if it existed already + } + + (*set)[i] = struct{}{} + return true +} + +func (set *threadUnsafeSet) Contains(i ...interface{}) bool { + for _, val := range i { + if _, ok := (*set)[val]; !ok { + return false + } + } + return true +} + +func (set *threadUnsafeSet) IsSubset(other Set) bool { + _ = other.(*threadUnsafeSet) + for elem := range *set { + if !other.Contains(elem) { + return false + } + } + return true +} + +func (set *threadUnsafeSet) IsProperSubset(other Set) bool { + return set.IsSubset(other) && !set.Equal(other) +} + +func (set *threadUnsafeSet) IsSuperset(other Set) bool { + return other.IsSubset(set) +} + +func (set *threadUnsafeSet) IsProperSuperset(other Set) bool { + return set.IsSuperset(other) && !set.Equal(other) +} + +func (set *threadUnsafeSet) Union(other Set) Set { + o := other.(*threadUnsafeSet) + + unionedSet := newThreadUnsafeSet() + + for elem := range *set { + unionedSet.Add(elem) + } + for elem := range *o { + unionedSet.Add(elem) + } + return &unionedSet +} + +func (set *threadUnsafeSet) Intersect(other Set) Set { + o := other.(*threadUnsafeSet) + + intersection := newThreadUnsafeSet() + // loop over smaller set + if set.Cardinality() < other.Cardinality() { + for elem := range *set { + if other.Contains(elem) { + intersection.Add(elem) + } + } + } else { + for elem := range *o { + if set.Contains(elem) { + intersection.Add(elem) + } + } + } + return &intersection +} + +func (set *threadUnsafeSet) Difference(other Set) Set { + _ = other.(*threadUnsafeSet) + + difference := newThreadUnsafeSet() + for elem := range *set { + if !other.Contains(elem) { + difference.Add(elem) + } + } + return &difference +} + +func (set *threadUnsafeSet) SymmetricDifference(other Set) Set { + _ = other.(*threadUnsafeSet) + + aDiff := set.Difference(other) + bDiff := other.Difference(set) + return aDiff.Union(bDiff) +} + +func (set *threadUnsafeSet) Clear() { + *set = newThreadUnsafeSet() +} + +func (set *threadUnsafeSet) Remove(i interface{}) { + delete(*set, i) +} + +func (set *threadUnsafeSet) Cardinality() int { + return len(*set) +} + +func (set *threadUnsafeSet) Each(cb func(interface{}) bool) { + for elem := range *set { + if cb(elem) { + break + } + } +} + +func (set *threadUnsafeSet) Iter() <-chan interface{} { + ch := make(chan interface{}) + go func() { + for elem := range *set { + ch <- elem + } + close(ch) + }() + + return ch +} + +func (set *threadUnsafeSet) Iterator() *Iterator { + iterator, ch, stopCh := newIterator() + + go func() { + L: + for elem := range *set { + select { + case <-stopCh: + break L + case ch <- elem: + } + } + close(ch) + }() + + return iterator +} + +func (set *threadUnsafeSet) Equal(other Set) bool { + _ = other.(*threadUnsafeSet) + + if set.Cardinality() != other.Cardinality() { + return false + } + for elem := range *set { + if !other.Contains(elem) { + return false + } + } + return true +} + +func (set *threadUnsafeSet) Clone() Set { + clonedSet := newThreadUnsafeSet() + for elem := range *set { + clonedSet.Add(elem) + } + return &clonedSet +} + +func (set *threadUnsafeSet) String() string { + items := make([]string, 0, len(*set)) + + for elem := range *set { + items = append(items, fmt.Sprintf("%v", elem)) + } + return fmt.Sprintf("Set{%s}", strings.Join(items, ", ")) +} + +// String outputs a 2-tuple in the form "(A, B)". +func (pair OrderedPair) String() string { + return fmt.Sprintf("(%v, %v)", pair.First, pair.Second) +} + +func (set *threadUnsafeSet) Pop() interface{} { + for item := range *set { + delete(*set, item) + return item + } + return nil +} + +func (set *threadUnsafeSet) PowerSet() Set { + powSet := NewThreadUnsafeSet() + nullset := newThreadUnsafeSet() + powSet.Add(&nullset) + + for es := range *set { + u := newThreadUnsafeSet() + j := powSet.Iter() + for er := range j { + p := newThreadUnsafeSet() + if reflect.TypeOf(er).Name() == "" { + k := er.(*threadUnsafeSet) + for ek := range *(k) { + p.Add(ek) + } + } else { + p.Add(er) + } + p.Add(es) + u.Add(&p) + } + + powSet = powSet.Union(&u) + } + + return powSet +} + +func (set *threadUnsafeSet) CartesianProduct(other Set) Set { + o := other.(*threadUnsafeSet) + cartProduct := NewThreadUnsafeSet() + + for i := range *set { + for j := range *o { + elem := OrderedPair{First: i, Second: j} + cartProduct.Add(elem) + } + } + + return cartProduct +} + +func (set *threadUnsafeSet) ToSlice() []interface{} { + keys := make([]interface{}, 0, set.Cardinality()) + for elem := range *set { + keys = append(keys, elem) + } + + return keys +} + +// MarshalJSON creates a JSON array from the set, it marshals all elements +func (set *threadUnsafeSet) MarshalJSON() ([]byte, error) { + items := make([]string, 0, set.Cardinality()) + + for elem := range *set { + b, err := json.Marshal(elem) + if err != nil { + return nil, err + } + + items = append(items, string(b)) + } + + return []byte(fmt.Sprintf("[%s]", strings.Join(items, ","))), nil +} + +// UnmarshalJSON recreates a set from a JSON array, it only decodes +// primitive types. Numbers are decoded as json.Number. +func (set *threadUnsafeSet) UnmarshalJSON(b []byte) error { + var i []interface{} + + d := json.NewDecoder(bytes.NewReader(b)) + d.UseNumber() + err := d.Decode(&i) + if err != nil { + return err + } + + for _, v := range i { + switch t := v.(type) { + case []interface{}, map[string]interface{}: + continue + default: + set.Add(t) + } + } + + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ce7792962a..568d865a3e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -305,6 +305,8 @@ github.com/cri-o/ocicni/pkg/ocicni github.com/cyphar/filepath-securejoin # github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew/spew +# github.com/deckarep/golang-set v1.7.1 +github.com/deckarep/golang-set # github.com/docker/distribution v2.7.1+incompatible github.com/docker/distribution github.com/docker/distribution/digestset