ImageCache is now independent of `Daemon` and is located in
`image/cache` package.
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
| ... | ... |
@@ -1,69 +1,18 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "encoding/json" |
|
| 5 |
- "fmt" |
|
| 6 |
- "reflect" |
|
| 7 |
- "strings" |
|
| 8 |
- |
|
| 9 | 4 |
"github.com/Sirupsen/logrus" |
| 10 |
- containertypes "github.com/docker/docker/api/types/container" |
|
| 11 | 5 |
"github.com/docker/docker/builder" |
| 12 |
- "github.com/docker/docker/dockerversion" |
|
| 13 |
- "github.com/docker/docker/image" |
|
| 14 |
- "github.com/docker/docker/layer" |
|
| 15 |
- "github.com/docker/docker/runconfig" |
|
| 16 |
- "github.com/pkg/errors" |
|
| 6 |
+ "github.com/docker/docker/image/cache" |
|
| 17 | 7 |
) |
| 18 | 8 |
|
| 19 |
-// getLocalCachedImage returns the most recent created image that is a child |
|
| 20 |
-// of the image with imgID, that had the same config when it was |
|
| 21 |
-// created. nil is returned if a child cannot be found. An error is |
|
| 22 |
-// returned if the parent image cannot be found. |
|
| 23 |
-func (daemon *Daemon) getLocalCachedImage(imgID image.ID, config *containertypes.Config) (*image.Image, error) {
|
|
| 24 |
- // Loop on the children of the given image and check the config |
|
| 25 |
- getMatch := func(siblings []image.ID) (*image.Image, error) {
|
|
| 26 |
- var match *image.Image |
|
| 27 |
- for _, id := range siblings {
|
|
| 28 |
- img, err := daemon.imageStore.Get(id) |
|
| 29 |
- if err != nil {
|
|
| 30 |
- return nil, fmt.Errorf("unable to find image %q", id)
|
|
| 31 |
- } |
|
| 32 |
- |
|
| 33 |
- if runconfig.Compare(&img.ContainerConfig, config) {
|
|
| 34 |
- // check for the most up to date match |
|
| 35 |
- if match == nil || match.Created.Before(img.Created) {
|
|
| 36 |
- match = img |
|
| 37 |
- } |
|
| 38 |
- } |
|
| 39 |
- } |
|
| 40 |
- return match, nil |
|
| 41 |
- } |
|
| 42 |
- |
|
| 43 |
- // In this case, this is `FROM scratch`, which isn't an actual image. |
|
| 44 |
- if imgID == "" {
|
|
| 45 |
- images := daemon.imageStore.Map() |
|
| 46 |
- var siblings []image.ID |
|
| 47 |
- for id, img := range images {
|
|
| 48 |
- if img.Parent == imgID {
|
|
| 49 |
- siblings = append(siblings, id) |
|
| 50 |
- } |
|
| 51 |
- } |
|
| 52 |
- return getMatch(siblings) |
|
| 53 |
- } |
|
| 54 |
- |
|
| 55 |
- // find match from child images |
|
| 56 |
- siblings := daemon.imageStore.Children(imgID) |
|
| 57 |
- return getMatch(siblings) |
|
| 58 |
-} |
|
| 59 |
- |
|
| 60 | 9 |
// MakeImageCache creates a stateful image cache. |
| 61 | 10 |
func (daemon *Daemon) MakeImageCache(sourceRefs []string) builder.ImageCache {
|
| 62 | 11 |
if len(sourceRefs) == 0 {
|
| 63 |
- return &localImageCache{daemon}
|
|
| 12 |
+ return cache.NewLocal(daemon.imageStore) |
|
| 64 | 13 |
} |
| 65 | 14 |
|
| 66 |
- cache := &imageCache{daemon: daemon, localImageCache: &localImageCache{daemon}}
|
|
| 15 |
+ cache := cache.New(daemon.imageStore) |
|
| 67 | 16 |
|
| 68 | 17 |
for _, ref := range sourceRefs {
|
| 69 | 18 |
img, err := daemon.GetImage(ref) |
| ... | ... |
@@ -71,184 +20,8 @@ func (daemon *Daemon) MakeImageCache(sourceRefs []string) builder.ImageCache {
|
| 71 | 71 |
logrus.Warnf("Could not look up %s for cache resolution, skipping: %+v", ref, err)
|
| 72 | 72 |
continue |
| 73 | 73 |
} |
| 74 |
- cache.sources = append(cache.sources, img) |
|
| 74 |
+ cache.Populate(img) |
|
| 75 | 75 |
} |
| 76 | 76 |
|
| 77 | 77 |
return cache |
| 78 | 78 |
} |
| 79 |
- |
|
| 80 |
-// localImageCache is cache based on parent chain. |
|
| 81 |
-type localImageCache struct {
|
|
| 82 |
- daemon *Daemon |
|
| 83 |
-} |
|
| 84 |
- |
|
| 85 |
-func (lic *localImageCache) GetCache(imgID string, config *containertypes.Config) (string, error) {
|
|
| 86 |
- return getImageIDAndError(lic.daemon.getLocalCachedImage(image.ID(imgID), config)) |
|
| 87 |
-} |
|
| 88 |
- |
|
| 89 |
-// imageCache is cache based on history objects. Requires initial set of images. |
|
| 90 |
-type imageCache struct {
|
|
| 91 |
- sources []*image.Image |
|
| 92 |
- daemon *Daemon |
|
| 93 |
- localImageCache *localImageCache |
|
| 94 |
-} |
|
| 95 |
- |
|
| 96 |
-func (ic *imageCache) restoreCachedImage(parent, target *image.Image, cfg *containertypes.Config) (image.ID, error) {
|
|
| 97 |
- var history []image.History |
|
| 98 |
- rootFS := image.NewRootFS() |
|
| 99 |
- lenHistory := 0 |
|
| 100 |
- if parent != nil {
|
|
| 101 |
- history = parent.History |
|
| 102 |
- rootFS = parent.RootFS |
|
| 103 |
- lenHistory = len(parent.History) |
|
| 104 |
- } |
|
| 105 |
- history = append(history, target.History[lenHistory]) |
|
| 106 |
- if layer := getLayerForHistoryIndex(target, lenHistory); layer != "" {
|
|
| 107 |
- rootFS.Append(layer) |
|
| 108 |
- } |
|
| 109 |
- |
|
| 110 |
- config, err := json.Marshal(&image.Image{
|
|
| 111 |
- V1Image: image.V1Image{
|
|
| 112 |
- DockerVersion: dockerversion.Version, |
|
| 113 |
- Config: cfg, |
|
| 114 |
- Architecture: target.Architecture, |
|
| 115 |
- OS: target.OS, |
|
| 116 |
- Author: target.Author, |
|
| 117 |
- Created: history[len(history)-1].Created, |
|
| 118 |
- }, |
|
| 119 |
- RootFS: rootFS, |
|
| 120 |
- History: history, |
|
| 121 |
- OSFeatures: target.OSFeatures, |
|
| 122 |
- OSVersion: target.OSVersion, |
|
| 123 |
- }) |
|
| 124 |
- if err != nil {
|
|
| 125 |
- return "", errors.Wrap(err, "failed to marshal image config") |
|
| 126 |
- } |
|
| 127 |
- |
|
| 128 |
- imgID, err := ic.daemon.imageStore.Create(config) |
|
| 129 |
- if err != nil {
|
|
| 130 |
- return "", errors.Wrap(err, "failed to create cache image") |
|
| 131 |
- } |
|
| 132 |
- |
|
| 133 |
- if parent != nil {
|
|
| 134 |
- if err := ic.daemon.imageStore.SetParent(imgID, parent.ID()); err != nil {
|
|
| 135 |
- return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID()) |
|
| 136 |
- } |
|
| 137 |
- } |
|
| 138 |
- return imgID, nil |
|
| 139 |
-} |
|
| 140 |
- |
|
| 141 |
-func (ic *imageCache) isParent(imgID, parentID image.ID) bool {
|
|
| 142 |
- nextParent, err := ic.daemon.imageStore.GetParent(imgID) |
|
| 143 |
- if err != nil {
|
|
| 144 |
- return false |
|
| 145 |
- } |
|
| 146 |
- if nextParent == parentID {
|
|
| 147 |
- return true |
|
| 148 |
- } |
|
| 149 |
- return ic.isParent(nextParent, parentID) |
|
| 150 |
-} |
|
| 151 |
- |
|
| 152 |
-func (ic *imageCache) GetCache(parentID string, cfg *containertypes.Config) (string, error) {
|
|
| 153 |
- imgID, err := ic.localImageCache.GetCache(parentID, cfg) |
|
| 154 |
- if err != nil {
|
|
| 155 |
- return "", err |
|
| 156 |
- } |
|
| 157 |
- if imgID != "" {
|
|
| 158 |
- for _, s := range ic.sources {
|
|
| 159 |
- if ic.isParent(s.ID(), image.ID(imgID)) {
|
|
| 160 |
- return imgID, nil |
|
| 161 |
- } |
|
| 162 |
- } |
|
| 163 |
- } |
|
| 164 |
- |
|
| 165 |
- var parent *image.Image |
|
| 166 |
- lenHistory := 0 |
|
| 167 |
- if parentID != "" {
|
|
| 168 |
- parent, err = ic.daemon.imageStore.Get(image.ID(parentID)) |
|
| 169 |
- if err != nil {
|
|
| 170 |
- return "", errors.Wrapf(err, "unable to find image %v", parentID) |
|
| 171 |
- } |
|
| 172 |
- lenHistory = len(parent.History) |
|
| 173 |
- } |
|
| 174 |
- |
|
| 175 |
- for _, target := range ic.sources {
|
|
| 176 |
- if !isValidParent(target, parent) || !isValidConfig(cfg, target.History[lenHistory]) {
|
|
| 177 |
- continue |
|
| 178 |
- } |
|
| 179 |
- |
|
| 180 |
- if len(target.History)-1 == lenHistory { // last
|
|
| 181 |
- if parent != nil {
|
|
| 182 |
- if err := ic.daemon.imageStore.SetParent(target.ID(), parent.ID()); err != nil {
|
|
| 183 |
- return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID()) |
|
| 184 |
- } |
|
| 185 |
- } |
|
| 186 |
- return target.ID().String(), nil |
|
| 187 |
- } |
|
| 188 |
- |
|
| 189 |
- imgID, err := ic.restoreCachedImage(parent, target, cfg) |
|
| 190 |
- if err != nil {
|
|
| 191 |
- return "", errors.Wrapf(err, "failed to restore cached image from %q to %v", parentID, target.ID()) |
|
| 192 |
- } |
|
| 193 |
- |
|
| 194 |
- ic.sources = []*image.Image{target} // avoid jumping to different target, tuned for safety atm
|
|
| 195 |
- return imgID.String(), nil |
|
| 196 |
- } |
|
| 197 |
- |
|
| 198 |
- return "", nil |
|
| 199 |
-} |
|
| 200 |
- |
|
| 201 |
-func getImageIDAndError(img *image.Image, err error) (string, error) {
|
|
| 202 |
- if img == nil || err != nil {
|
|
| 203 |
- return "", err |
|
| 204 |
- } |
|
| 205 |
- return img.ID().String(), nil |
|
| 206 |
-} |
|
| 207 |
- |
|
| 208 |
-func isValidParent(img, parent *image.Image) bool {
|
|
| 209 |
- if len(img.History) == 0 {
|
|
| 210 |
- return false |
|
| 211 |
- } |
|
| 212 |
- if parent == nil || len(parent.History) == 0 && len(parent.RootFS.DiffIDs) == 0 {
|
|
| 213 |
- return true |
|
| 214 |
- } |
|
| 215 |
- if len(parent.History) >= len(img.History) {
|
|
| 216 |
- return false |
|
| 217 |
- } |
|
| 218 |
- if len(parent.RootFS.DiffIDs) >= len(img.RootFS.DiffIDs) {
|
|
| 219 |
- return false |
|
| 220 |
- } |
|
| 221 |
- |
|
| 222 |
- for i, h := range parent.History {
|
|
| 223 |
- if !reflect.DeepEqual(h, img.History[i]) {
|
|
| 224 |
- return false |
|
| 225 |
- } |
|
| 226 |
- } |
|
| 227 |
- for i, d := range parent.RootFS.DiffIDs {
|
|
| 228 |
- if d != img.RootFS.DiffIDs[i] {
|
|
| 229 |
- return false |
|
| 230 |
- } |
|
| 231 |
- } |
|
| 232 |
- return true |
|
| 233 |
-} |
|
| 234 |
- |
|
| 235 |
-func getLayerForHistoryIndex(image *image.Image, index int) layer.DiffID {
|
|
| 236 |
- layerIndex := 0 |
|
| 237 |
- for i, h := range image.History {
|
|
| 238 |
- if i == index {
|
|
| 239 |
- if h.EmptyLayer {
|
|
| 240 |
- return "" |
|
| 241 |
- } |
|
| 242 |
- break |
|
| 243 |
- } |
|
| 244 |
- if !h.EmptyLayer {
|
|
| 245 |
- layerIndex++ |
|
| 246 |
- } |
|
| 247 |
- } |
|
| 248 |
- return image.RootFS.DiffIDs[layerIndex] // validate? |
|
| 249 |
-} |
|
| 250 |
- |
|
| 251 |
-func isValidConfig(cfg *containertypes.Config, h image.History) bool {
|
|
| 252 |
- // todo: make this format better than join that loses data |
|
| 253 |
- return strings.Join(cfg.Cmd, " ") == h.CreatedBy |
|
| 254 |
-} |
| 255 | 79 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,253 @@ |
| 0 |
+package cache |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "reflect" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ containertypes "github.com/docker/docker/api/types/container" |
|
| 9 |
+ "github.com/docker/docker/dockerversion" |
|
| 10 |
+ "github.com/docker/docker/image" |
|
| 11 |
+ "github.com/docker/docker/layer" |
|
| 12 |
+ "github.com/pkg/errors" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+// NewLocal returns a local image cache, based on parent chain |
|
| 16 |
+func NewLocal(store image.Store) *LocalImageCache {
|
|
| 17 |
+ return &LocalImageCache{
|
|
| 18 |
+ store: store, |
|
| 19 |
+ } |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+// LocalImageCache is cache based on parent chain. |
|
| 23 |
+type LocalImageCache struct {
|
|
| 24 |
+ store image.Store |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+// GetCache returns the image id found in the cache |
|
| 28 |
+func (lic *LocalImageCache) GetCache(imgID string, config *containertypes.Config) (string, error) {
|
|
| 29 |
+ return getImageIDAndError(getLocalCachedImage(lic.store, image.ID(imgID), config)) |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+// New returns an image cache, based on history objects |
|
| 33 |
+func New(store image.Store) *ImageCache {
|
|
| 34 |
+ return &ImageCache{
|
|
| 35 |
+ store: store, |
|
| 36 |
+ localImageCache: NewLocal(store), |
|
| 37 |
+ } |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// ImageCache is cache based on history objects. Requires initial set of images. |
|
| 41 |
+type ImageCache struct {
|
|
| 42 |
+ sources []*image.Image |
|
| 43 |
+ store image.Store |
|
| 44 |
+ localImageCache *LocalImageCache |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+// Populate adds an image to the cache (to be queried later) |
|
| 48 |
+func (ic *ImageCache) Populate(image *image.Image) {
|
|
| 49 |
+ ic.sources = append(ic.sources, image) |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+// GetCache returns the image id found in the cache |
|
| 53 |
+func (ic *ImageCache) GetCache(parentID string, cfg *containertypes.Config) (string, error) {
|
|
| 54 |
+ imgID, err := ic.localImageCache.GetCache(parentID, cfg) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ return "", err |
|
| 57 |
+ } |
|
| 58 |
+ if imgID != "" {
|
|
| 59 |
+ for _, s := range ic.sources {
|
|
| 60 |
+ if ic.isParent(s.ID(), image.ID(imgID)) {
|
|
| 61 |
+ return imgID, nil |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ var parent *image.Image |
|
| 67 |
+ lenHistory := 0 |
|
| 68 |
+ if parentID != "" {
|
|
| 69 |
+ parent, err = ic.store.Get(image.ID(parentID)) |
|
| 70 |
+ if err != nil {
|
|
| 71 |
+ return "", errors.Wrapf(err, "unable to find image %v", parentID) |
|
| 72 |
+ } |
|
| 73 |
+ lenHistory = len(parent.History) |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ for _, target := range ic.sources {
|
|
| 77 |
+ if !isValidParent(target, parent) || !isValidConfig(cfg, target.History[lenHistory]) {
|
|
| 78 |
+ continue |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ if len(target.History)-1 == lenHistory { // last
|
|
| 82 |
+ if parent != nil {
|
|
| 83 |
+ if err := ic.store.SetParent(target.ID(), parent.ID()); err != nil {
|
|
| 84 |
+ return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID()) |
|
| 85 |
+ } |
|
| 86 |
+ } |
|
| 87 |
+ return target.ID().String(), nil |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ imgID, err := ic.restoreCachedImage(parent, target, cfg) |
|
| 91 |
+ if err != nil {
|
|
| 92 |
+ return "", errors.Wrapf(err, "failed to restore cached image from %q to %v", parentID, target.ID()) |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ ic.sources = []*image.Image{target} // avoid jumping to different target, tuned for safety atm
|
|
| 96 |
+ return imgID.String(), nil |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ return "", nil |
|
| 100 |
+} |
|
| 101 |
+ |
|
| 102 |
+func (ic *ImageCache) restoreCachedImage(parent, target *image.Image, cfg *containertypes.Config) (image.ID, error) {
|
|
| 103 |
+ var history []image.History |
|
| 104 |
+ rootFS := image.NewRootFS() |
|
| 105 |
+ lenHistory := 0 |
|
| 106 |
+ if parent != nil {
|
|
| 107 |
+ history = parent.History |
|
| 108 |
+ rootFS = parent.RootFS |
|
| 109 |
+ lenHistory = len(parent.History) |
|
| 110 |
+ } |
|
| 111 |
+ history = append(history, target.History[lenHistory]) |
|
| 112 |
+ if layer := getLayerForHistoryIndex(target, lenHistory); layer != "" {
|
|
| 113 |
+ rootFS.Append(layer) |
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ config, err := json.Marshal(&image.Image{
|
|
| 117 |
+ V1Image: image.V1Image{
|
|
| 118 |
+ DockerVersion: dockerversion.Version, |
|
| 119 |
+ Config: cfg, |
|
| 120 |
+ Architecture: target.Architecture, |
|
| 121 |
+ OS: target.OS, |
|
| 122 |
+ Author: target.Author, |
|
| 123 |
+ Created: history[len(history)-1].Created, |
|
| 124 |
+ }, |
|
| 125 |
+ RootFS: rootFS, |
|
| 126 |
+ History: history, |
|
| 127 |
+ OSFeatures: target.OSFeatures, |
|
| 128 |
+ OSVersion: target.OSVersion, |
|
| 129 |
+ }) |
|
| 130 |
+ if err != nil {
|
|
| 131 |
+ return "", errors.Wrap(err, "failed to marshal image config") |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ imgID, err := ic.store.Create(config) |
|
| 135 |
+ if err != nil {
|
|
| 136 |
+ return "", errors.Wrap(err, "failed to create cache image") |
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ if parent != nil {
|
|
| 140 |
+ if err := ic.store.SetParent(imgID, parent.ID()); err != nil {
|
|
| 141 |
+ return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID()) |
|
| 142 |
+ } |
|
| 143 |
+ } |
|
| 144 |
+ return imgID, nil |
|
| 145 |
+} |
|
| 146 |
+ |
|
| 147 |
+func (ic *ImageCache) isParent(imgID, parentID image.ID) bool {
|
|
| 148 |
+ nextParent, err := ic.store.GetParent(imgID) |
|
| 149 |
+ if err != nil {
|
|
| 150 |
+ return false |
|
| 151 |
+ } |
|
| 152 |
+ if nextParent == parentID {
|
|
| 153 |
+ return true |
|
| 154 |
+ } |
|
| 155 |
+ return ic.isParent(nextParent, parentID) |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+func getLayerForHistoryIndex(image *image.Image, index int) layer.DiffID {
|
|
| 159 |
+ layerIndex := 0 |
|
| 160 |
+ for i, h := range image.History {
|
|
| 161 |
+ if i == index {
|
|
| 162 |
+ if h.EmptyLayer {
|
|
| 163 |
+ return "" |
|
| 164 |
+ } |
|
| 165 |
+ break |
|
| 166 |
+ } |
|
| 167 |
+ if !h.EmptyLayer {
|
|
| 168 |
+ layerIndex++ |
|
| 169 |
+ } |
|
| 170 |
+ } |
|
| 171 |
+ return image.RootFS.DiffIDs[layerIndex] // validate? |
|
| 172 |
+} |
|
| 173 |
+ |
|
| 174 |
+func isValidConfig(cfg *containertypes.Config, h image.History) bool {
|
|
| 175 |
+ // todo: make this format better than join that loses data |
|
| 176 |
+ return strings.Join(cfg.Cmd, " ") == h.CreatedBy |
|
| 177 |
+} |
|
| 178 |
+ |
|
| 179 |
+func isValidParent(img, parent *image.Image) bool {
|
|
| 180 |
+ if len(img.History) == 0 {
|
|
| 181 |
+ return false |
|
| 182 |
+ } |
|
| 183 |
+ if parent == nil || len(parent.History) == 0 && len(parent.RootFS.DiffIDs) == 0 {
|
|
| 184 |
+ return true |
|
| 185 |
+ } |
|
| 186 |
+ if len(parent.History) >= len(img.History) {
|
|
| 187 |
+ return false |
|
| 188 |
+ } |
|
| 189 |
+ if len(parent.RootFS.DiffIDs) >= len(img.RootFS.DiffIDs) {
|
|
| 190 |
+ return false |
|
| 191 |
+ } |
|
| 192 |
+ |
|
| 193 |
+ for i, h := range parent.History {
|
|
| 194 |
+ if !reflect.DeepEqual(h, img.History[i]) {
|
|
| 195 |
+ return false |
|
| 196 |
+ } |
|
| 197 |
+ } |
|
| 198 |
+ for i, d := range parent.RootFS.DiffIDs {
|
|
| 199 |
+ if d != img.RootFS.DiffIDs[i] {
|
|
| 200 |
+ return false |
|
| 201 |
+ } |
|
| 202 |
+ } |
|
| 203 |
+ return true |
|
| 204 |
+} |
|
| 205 |
+ |
|
| 206 |
+func getImageIDAndError(img *image.Image, err error) (string, error) {
|
|
| 207 |
+ if img == nil || err != nil {
|
|
| 208 |
+ return "", err |
|
| 209 |
+ } |
|
| 210 |
+ return img.ID().String(), nil |
|
| 211 |
+} |
|
| 212 |
+ |
|
| 213 |
+// getLocalCachedImage returns the most recent created image that is a child |
|
| 214 |
+// of the image with imgID, that had the same config when it was |
|
| 215 |
+// created. nil is returned if a child cannot be found. An error is |
|
| 216 |
+// returned if the parent image cannot be found. |
|
| 217 |
+func getLocalCachedImage(imageStore image.Store, imgID image.ID, config *containertypes.Config) (*image.Image, error) {
|
|
| 218 |
+ // Loop on the children of the given image and check the config |
|
| 219 |
+ getMatch := func(siblings []image.ID) (*image.Image, error) {
|
|
| 220 |
+ var match *image.Image |
|
| 221 |
+ for _, id := range siblings {
|
|
| 222 |
+ img, err := imageStore.Get(id) |
|
| 223 |
+ if err != nil {
|
|
| 224 |
+ return nil, fmt.Errorf("unable to find image %q", id)
|
|
| 225 |
+ } |
|
| 226 |
+ |
|
| 227 |
+ if compare(&img.ContainerConfig, config) {
|
|
| 228 |
+ // check for the most up to date match |
|
| 229 |
+ if match == nil || match.Created.Before(img.Created) {
|
|
| 230 |
+ match = img |
|
| 231 |
+ } |
|
| 232 |
+ } |
|
| 233 |
+ } |
|
| 234 |
+ return match, nil |
|
| 235 |
+ } |
|
| 236 |
+ |
|
| 237 |
+ // In this case, this is `FROM scratch`, which isn't an actual image. |
|
| 238 |
+ if imgID == "" {
|
|
| 239 |
+ images := imageStore.Map() |
|
| 240 |
+ var siblings []image.ID |
|
| 241 |
+ for id, img := range images {
|
|
| 242 |
+ if img.Parent == imgID {
|
|
| 243 |
+ siblings = append(siblings, id) |
|
| 244 |
+ } |
|
| 245 |
+ } |
|
| 246 |
+ return getMatch(siblings) |
|
| 247 |
+ } |
|
| 248 |
+ |
|
| 249 |
+ // find match from child images |
|
| 250 |
+ siblings := imageStore.Children(imgID) |
|
| 251 |
+ return getMatch(siblings) |
|
| 252 |
+} |
| 0 | 253 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,61 @@ |
| 0 |
+package cache |
|
| 1 |
+ |
|
| 2 |
+import "github.com/docker/docker/api/types/container" |
|
| 3 |
+ |
|
| 4 |
+// compare two Config struct. Do not compare the "Image" nor "Hostname" fields |
|
| 5 |
+// If OpenStdin is set, then it differs |
|
| 6 |
+func compare(a, b *container.Config) bool {
|
|
| 7 |
+ if a == nil || b == nil || |
|
| 8 |
+ a.OpenStdin || b.OpenStdin {
|
|
| 9 |
+ return false |
|
| 10 |
+ } |
|
| 11 |
+ if a.AttachStdout != b.AttachStdout || |
|
| 12 |
+ a.AttachStderr != b.AttachStderr || |
|
| 13 |
+ a.User != b.User || |
|
| 14 |
+ a.OpenStdin != b.OpenStdin || |
|
| 15 |
+ a.Tty != b.Tty {
|
|
| 16 |
+ return false |
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ if len(a.Cmd) != len(b.Cmd) || |
|
| 20 |
+ len(a.Env) != len(b.Env) || |
|
| 21 |
+ len(a.Labels) != len(b.Labels) || |
|
| 22 |
+ len(a.ExposedPorts) != len(b.ExposedPorts) || |
|
| 23 |
+ len(a.Entrypoint) != len(b.Entrypoint) || |
|
| 24 |
+ len(a.Volumes) != len(b.Volumes) {
|
|
| 25 |
+ return false |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ for i := 0; i < len(a.Cmd); i++ {
|
|
| 29 |
+ if a.Cmd[i] != b.Cmd[i] {
|
|
| 30 |
+ return false |
|
| 31 |
+ } |
|
| 32 |
+ } |
|
| 33 |
+ for i := 0; i < len(a.Env); i++ {
|
|
| 34 |
+ if a.Env[i] != b.Env[i] {
|
|
| 35 |
+ return false |
|
| 36 |
+ } |
|
| 37 |
+ } |
|
| 38 |
+ for k, v := range a.Labels {
|
|
| 39 |
+ if v != b.Labels[k] {
|
|
| 40 |
+ return false |
|
| 41 |
+ } |
|
| 42 |
+ } |
|
| 43 |
+ for k := range a.ExposedPorts {
|
|
| 44 |
+ if _, exists := b.ExposedPorts[k]; !exists {
|
|
| 45 |
+ return false |
|
| 46 |
+ } |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ for i := 0; i < len(a.Entrypoint); i++ {
|
|
| 50 |
+ if a.Entrypoint[i] != b.Entrypoint[i] {
|
|
| 51 |
+ return false |
|
| 52 |
+ } |
|
| 53 |
+ } |
|
| 54 |
+ for key := range a.Volumes {
|
|
| 55 |
+ if _, exists := b.Volumes[key]; !exists {
|
|
| 56 |
+ return false |
|
| 57 |
+ } |
|
| 58 |
+ } |
|
| 59 |
+ return true |
|
| 60 |
+} |
| 0 | 61 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,126 @@ |
| 0 |
+package cache |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/types/container" |
|
| 6 |
+ "github.com/docker/docker/api/types/strslice" |
|
| 7 |
+ "github.com/docker/go-connections/nat" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// Just to make life easier |
|
| 11 |
+func newPortNoError(proto, port string) nat.Port {
|
|
| 12 |
+ p, _ := nat.NewPort(proto, port) |
|
| 13 |
+ return p |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func TestCompare(t *testing.T) {
|
|
| 17 |
+ ports1 := make(nat.PortSet) |
|
| 18 |
+ ports1[newPortNoError("tcp", "1111")] = struct{}{}
|
|
| 19 |
+ ports1[newPortNoError("tcp", "2222")] = struct{}{}
|
|
| 20 |
+ ports2 := make(nat.PortSet) |
|
| 21 |
+ ports2[newPortNoError("tcp", "3333")] = struct{}{}
|
|
| 22 |
+ ports2[newPortNoError("tcp", "4444")] = struct{}{}
|
|
| 23 |
+ ports3 := make(nat.PortSet) |
|
| 24 |
+ ports3[newPortNoError("tcp", "1111")] = struct{}{}
|
|
| 25 |
+ ports3[newPortNoError("tcp", "2222")] = struct{}{}
|
|
| 26 |
+ ports3[newPortNoError("tcp", "5555")] = struct{}{}
|
|
| 27 |
+ volumes1 := make(map[string]struct{})
|
|
| 28 |
+ volumes1["/test1"] = struct{}{}
|
|
| 29 |
+ volumes2 := make(map[string]struct{})
|
|
| 30 |
+ volumes2["/test2"] = struct{}{}
|
|
| 31 |
+ volumes3 := make(map[string]struct{})
|
|
| 32 |
+ volumes3["/test1"] = struct{}{}
|
|
| 33 |
+ volumes3["/test3"] = struct{}{}
|
|
| 34 |
+ envs1 := []string{"ENV1=value1", "ENV2=value2"}
|
|
| 35 |
+ envs2 := []string{"ENV1=value1", "ENV3=value3"}
|
|
| 36 |
+ entrypoint1 := strslice.StrSlice{"/bin/sh", "-c"}
|
|
| 37 |
+ entrypoint2 := strslice.StrSlice{"/bin/sh", "-d"}
|
|
| 38 |
+ entrypoint3 := strslice.StrSlice{"/bin/sh", "-c", "echo"}
|
|
| 39 |
+ cmd1 := strslice.StrSlice{"/bin/sh", "-c"}
|
|
| 40 |
+ cmd2 := strslice.StrSlice{"/bin/sh", "-d"}
|
|
| 41 |
+ cmd3 := strslice.StrSlice{"/bin/sh", "-c", "echo"}
|
|
| 42 |
+ labels1 := map[string]string{"LABEL1": "value1", "LABEL2": "value2"}
|
|
| 43 |
+ labels2 := map[string]string{"LABEL1": "value1", "LABEL2": "value3"}
|
|
| 44 |
+ labels3 := map[string]string{"LABEL1": "value1", "LABEL2": "value2", "LABEL3": "value3"}
|
|
| 45 |
+ |
|
| 46 |
+ sameConfigs := map[*container.Config]*container.Config{
|
|
| 47 |
+ // Empty config |
|
| 48 |
+ &container.Config{}: {},
|
|
| 49 |
+ // Does not compare hostname, domainname & image |
|
| 50 |
+ &container.Config{
|
|
| 51 |
+ Hostname: "host1", |
|
| 52 |
+ Domainname: "domain1", |
|
| 53 |
+ Image: "image1", |
|
| 54 |
+ User: "user", |
|
| 55 |
+ }: {
|
|
| 56 |
+ Hostname: "host2", |
|
| 57 |
+ Domainname: "domain2", |
|
| 58 |
+ Image: "image2", |
|
| 59 |
+ User: "user", |
|
| 60 |
+ }, |
|
| 61 |
+ // only OpenStdin |
|
| 62 |
+ &container.Config{OpenStdin: false}: {OpenStdin: false},
|
|
| 63 |
+ // only env |
|
| 64 |
+ &container.Config{Env: envs1}: {Env: envs1},
|
|
| 65 |
+ // only cmd |
|
| 66 |
+ &container.Config{Cmd: cmd1}: {Cmd: cmd1},
|
|
| 67 |
+ // only labels |
|
| 68 |
+ &container.Config{Labels: labels1}: {Labels: labels1},
|
|
| 69 |
+ // only exposedPorts |
|
| 70 |
+ &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports1},
|
|
| 71 |
+ // only entrypoints |
|
| 72 |
+ &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint1},
|
|
| 73 |
+ // only volumes |
|
| 74 |
+ &container.Config{Volumes: volumes1}: {Volumes: volumes1},
|
|
| 75 |
+ } |
|
| 76 |
+ differentConfigs := map[*container.Config]*container.Config{
|
|
| 77 |
+ nil: nil, |
|
| 78 |
+ &container.Config{
|
|
| 79 |
+ Hostname: "host1", |
|
| 80 |
+ Domainname: "domain1", |
|
| 81 |
+ Image: "image1", |
|
| 82 |
+ User: "user1", |
|
| 83 |
+ }: {
|
|
| 84 |
+ Hostname: "host1", |
|
| 85 |
+ Domainname: "domain1", |
|
| 86 |
+ Image: "image1", |
|
| 87 |
+ User: "user2", |
|
| 88 |
+ }, |
|
| 89 |
+ // only OpenStdin |
|
| 90 |
+ &container.Config{OpenStdin: false}: {OpenStdin: true},
|
|
| 91 |
+ &container.Config{OpenStdin: true}: {OpenStdin: false},
|
|
| 92 |
+ // only env |
|
| 93 |
+ &container.Config{Env: envs1}: {Env: envs2},
|
|
| 94 |
+ // only cmd |
|
| 95 |
+ &container.Config{Cmd: cmd1}: {Cmd: cmd2},
|
|
| 96 |
+ // not the same number of parts |
|
| 97 |
+ &container.Config{Cmd: cmd1}: {Cmd: cmd3},
|
|
| 98 |
+ // only labels |
|
| 99 |
+ &container.Config{Labels: labels1}: {Labels: labels2},
|
|
| 100 |
+ // not the same number of labels |
|
| 101 |
+ &container.Config{Labels: labels1}: {Labels: labels3},
|
|
| 102 |
+ // only exposedPorts |
|
| 103 |
+ &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports2},
|
|
| 104 |
+ // not the same number of ports |
|
| 105 |
+ &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports3},
|
|
| 106 |
+ // only entrypoints |
|
| 107 |
+ &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint2},
|
|
| 108 |
+ // not the same number of parts |
|
| 109 |
+ &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint3},
|
|
| 110 |
+ // only volumes |
|
| 111 |
+ &container.Config{Volumes: volumes1}: {Volumes: volumes2},
|
|
| 112 |
+ // not the same number of labels |
|
| 113 |
+ &container.Config{Volumes: volumes1}: {Volumes: volumes3},
|
|
| 114 |
+ } |
|
| 115 |
+ for config1, config2 := range sameConfigs {
|
|
| 116 |
+ if !compare(config1, config2) {
|
|
| 117 |
+ t.Fatalf("Compare should be true for [%v] and [%v]", config1, config2)
|
|
| 118 |
+ } |
|
| 119 |
+ } |
|
| 120 |
+ for config1, config2 := range differentConfigs {
|
|
| 121 |
+ if compare(config1, config2) {
|
|
| 122 |
+ t.Fatalf("Compare should be false for [%v] and [%v]", config1, config2)
|
|
| 123 |
+ } |
|
| 124 |
+ } |
|
| 125 |
+} |
| 0 | 126 |
deleted file mode 100644 |
| ... | ... |
@@ -1,61 +0,0 @@ |
| 1 |
-package runconfig |
|
| 2 |
- |
|
| 3 |
-import "github.com/docker/docker/api/types/container" |
|
| 4 |
- |
|
| 5 |
-// Compare two Config struct. Do not compare the "Image" nor "Hostname" fields |
|
| 6 |
-// If OpenStdin is set, then it differs |
|
| 7 |
-func Compare(a, b *container.Config) bool {
|
|
| 8 |
- if a == nil || b == nil || |
|
| 9 |
- a.OpenStdin || b.OpenStdin {
|
|
| 10 |
- return false |
|
| 11 |
- } |
|
| 12 |
- if a.AttachStdout != b.AttachStdout || |
|
| 13 |
- a.AttachStderr != b.AttachStderr || |
|
| 14 |
- a.User != b.User || |
|
| 15 |
- a.OpenStdin != b.OpenStdin || |
|
| 16 |
- a.Tty != b.Tty {
|
|
| 17 |
- return false |
|
| 18 |
- } |
|
| 19 |
- |
|
| 20 |
- if len(a.Cmd) != len(b.Cmd) || |
|
| 21 |
- len(a.Env) != len(b.Env) || |
|
| 22 |
- len(a.Labels) != len(b.Labels) || |
|
| 23 |
- len(a.ExposedPorts) != len(b.ExposedPorts) || |
|
| 24 |
- len(a.Entrypoint) != len(b.Entrypoint) || |
|
| 25 |
- len(a.Volumes) != len(b.Volumes) {
|
|
| 26 |
- return false |
|
| 27 |
- } |
|
| 28 |
- |
|
| 29 |
- for i := 0; i < len(a.Cmd); i++ {
|
|
| 30 |
- if a.Cmd[i] != b.Cmd[i] {
|
|
| 31 |
- return false |
|
| 32 |
- } |
|
| 33 |
- } |
|
| 34 |
- for i := 0; i < len(a.Env); i++ {
|
|
| 35 |
- if a.Env[i] != b.Env[i] {
|
|
| 36 |
- return false |
|
| 37 |
- } |
|
| 38 |
- } |
|
| 39 |
- for k, v := range a.Labels {
|
|
| 40 |
- if v != b.Labels[k] {
|
|
| 41 |
- return false |
|
| 42 |
- } |
|
| 43 |
- } |
|
| 44 |
- for k := range a.ExposedPorts {
|
|
| 45 |
- if _, exists := b.ExposedPorts[k]; !exists {
|
|
| 46 |
- return false |
|
| 47 |
- } |
|
| 48 |
- } |
|
| 49 |
- |
|
| 50 |
- for i := 0; i < len(a.Entrypoint); i++ {
|
|
| 51 |
- if a.Entrypoint[i] != b.Entrypoint[i] {
|
|
| 52 |
- return false |
|
| 53 |
- } |
|
| 54 |
- } |
|
| 55 |
- for key := range a.Volumes {
|
|
| 56 |
- if _, exists := b.Volumes[key]; !exists {
|
|
| 57 |
- return false |
|
| 58 |
- } |
|
| 59 |
- } |
|
| 60 |
- return true |
|
| 61 |
-} |
| 62 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,126 +0,0 @@ |
| 1 |
-package runconfig |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "testing" |
|
| 5 |
- |
|
| 6 |
- "github.com/docker/docker/api/types/container" |
|
| 7 |
- "github.com/docker/docker/api/types/strslice" |
|
| 8 |
- "github.com/docker/go-connections/nat" |
|
| 9 |
-) |
|
| 10 |
- |
|
| 11 |
-// Just to make life easier |
|
| 12 |
-func newPortNoError(proto, port string) nat.Port {
|
|
| 13 |
- p, _ := nat.NewPort(proto, port) |
|
| 14 |
- return p |
|
| 15 |
-} |
|
| 16 |
- |
|
| 17 |
-func TestCompare(t *testing.T) {
|
|
| 18 |
- ports1 := make(nat.PortSet) |
|
| 19 |
- ports1[newPortNoError("tcp", "1111")] = struct{}{}
|
|
| 20 |
- ports1[newPortNoError("tcp", "2222")] = struct{}{}
|
|
| 21 |
- ports2 := make(nat.PortSet) |
|
| 22 |
- ports2[newPortNoError("tcp", "3333")] = struct{}{}
|
|
| 23 |
- ports2[newPortNoError("tcp", "4444")] = struct{}{}
|
|
| 24 |
- ports3 := make(nat.PortSet) |
|
| 25 |
- ports3[newPortNoError("tcp", "1111")] = struct{}{}
|
|
| 26 |
- ports3[newPortNoError("tcp", "2222")] = struct{}{}
|
|
| 27 |
- ports3[newPortNoError("tcp", "5555")] = struct{}{}
|
|
| 28 |
- volumes1 := make(map[string]struct{})
|
|
| 29 |
- volumes1["/test1"] = struct{}{}
|
|
| 30 |
- volumes2 := make(map[string]struct{})
|
|
| 31 |
- volumes2["/test2"] = struct{}{}
|
|
| 32 |
- volumes3 := make(map[string]struct{})
|
|
| 33 |
- volumes3["/test1"] = struct{}{}
|
|
| 34 |
- volumes3["/test3"] = struct{}{}
|
|
| 35 |
- envs1 := []string{"ENV1=value1", "ENV2=value2"}
|
|
| 36 |
- envs2 := []string{"ENV1=value1", "ENV3=value3"}
|
|
| 37 |
- entrypoint1 := strslice.StrSlice{"/bin/sh", "-c"}
|
|
| 38 |
- entrypoint2 := strslice.StrSlice{"/bin/sh", "-d"}
|
|
| 39 |
- entrypoint3 := strslice.StrSlice{"/bin/sh", "-c", "echo"}
|
|
| 40 |
- cmd1 := strslice.StrSlice{"/bin/sh", "-c"}
|
|
| 41 |
- cmd2 := strslice.StrSlice{"/bin/sh", "-d"}
|
|
| 42 |
- cmd3 := strslice.StrSlice{"/bin/sh", "-c", "echo"}
|
|
| 43 |
- labels1 := map[string]string{"LABEL1": "value1", "LABEL2": "value2"}
|
|
| 44 |
- labels2 := map[string]string{"LABEL1": "value1", "LABEL2": "value3"}
|
|
| 45 |
- labels3 := map[string]string{"LABEL1": "value1", "LABEL2": "value2", "LABEL3": "value3"}
|
|
| 46 |
- |
|
| 47 |
- sameConfigs := map[*container.Config]*container.Config{
|
|
| 48 |
- // Empty config |
|
| 49 |
- &container.Config{}: {},
|
|
| 50 |
- // Does not compare hostname, domainname & image |
|
| 51 |
- &container.Config{
|
|
| 52 |
- Hostname: "host1", |
|
| 53 |
- Domainname: "domain1", |
|
| 54 |
- Image: "image1", |
|
| 55 |
- User: "user", |
|
| 56 |
- }: {
|
|
| 57 |
- Hostname: "host2", |
|
| 58 |
- Domainname: "domain2", |
|
| 59 |
- Image: "image2", |
|
| 60 |
- User: "user", |
|
| 61 |
- }, |
|
| 62 |
- // only OpenStdin |
|
| 63 |
- &container.Config{OpenStdin: false}: {OpenStdin: false},
|
|
| 64 |
- // only env |
|
| 65 |
- &container.Config{Env: envs1}: {Env: envs1},
|
|
| 66 |
- // only cmd |
|
| 67 |
- &container.Config{Cmd: cmd1}: {Cmd: cmd1},
|
|
| 68 |
- // only labels |
|
| 69 |
- &container.Config{Labels: labels1}: {Labels: labels1},
|
|
| 70 |
- // only exposedPorts |
|
| 71 |
- &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports1},
|
|
| 72 |
- // only entrypoints |
|
| 73 |
- &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint1},
|
|
| 74 |
- // only volumes |
|
| 75 |
- &container.Config{Volumes: volumes1}: {Volumes: volumes1},
|
|
| 76 |
- } |
|
| 77 |
- differentConfigs := map[*container.Config]*container.Config{
|
|
| 78 |
- nil: nil, |
|
| 79 |
- &container.Config{
|
|
| 80 |
- Hostname: "host1", |
|
| 81 |
- Domainname: "domain1", |
|
| 82 |
- Image: "image1", |
|
| 83 |
- User: "user1", |
|
| 84 |
- }: {
|
|
| 85 |
- Hostname: "host1", |
|
| 86 |
- Domainname: "domain1", |
|
| 87 |
- Image: "image1", |
|
| 88 |
- User: "user2", |
|
| 89 |
- }, |
|
| 90 |
- // only OpenStdin |
|
| 91 |
- &container.Config{OpenStdin: false}: {OpenStdin: true},
|
|
| 92 |
- &container.Config{OpenStdin: true}: {OpenStdin: false},
|
|
| 93 |
- // only env |
|
| 94 |
- &container.Config{Env: envs1}: {Env: envs2},
|
|
| 95 |
- // only cmd |
|
| 96 |
- &container.Config{Cmd: cmd1}: {Cmd: cmd2},
|
|
| 97 |
- // not the same number of parts |
|
| 98 |
- &container.Config{Cmd: cmd1}: {Cmd: cmd3},
|
|
| 99 |
- // only labels |
|
| 100 |
- &container.Config{Labels: labels1}: {Labels: labels2},
|
|
| 101 |
- // not the same number of labels |
|
| 102 |
- &container.Config{Labels: labels1}: {Labels: labels3},
|
|
| 103 |
- // only exposedPorts |
|
| 104 |
- &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports2},
|
|
| 105 |
- // not the same number of ports |
|
| 106 |
- &container.Config{ExposedPorts: ports1}: {ExposedPorts: ports3},
|
|
| 107 |
- // only entrypoints |
|
| 108 |
- &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint2},
|
|
| 109 |
- // not the same number of parts |
|
| 110 |
- &container.Config{Entrypoint: entrypoint1}: {Entrypoint: entrypoint3},
|
|
| 111 |
- // only volumes |
|
| 112 |
- &container.Config{Volumes: volumes1}: {Volumes: volumes2},
|
|
| 113 |
- // not the same number of labels |
|
| 114 |
- &container.Config{Volumes: volumes1}: {Volumes: volumes3},
|
|
| 115 |
- } |
|
| 116 |
- for config1, config2 := range sameConfigs {
|
|
| 117 |
- if !Compare(config1, config2) {
|
|
| 118 |
- t.Fatalf("Compare should be true for [%v] and [%v]", config1, config2)
|
|
| 119 |
- } |
|
| 120 |
- } |
|
| 121 |
- for config1, config2 := range differentConfigs {
|
|
| 122 |
- if Compare(config1, config2) {
|
|
| 123 |
- t.Fatalf("Compare should be false for [%v] and [%v]", config1, config2)
|
|
| 124 |
- } |
|
| 125 |
- } |
|
| 126 |
-} |