github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/cache/retrieve_test.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package cache 18 19 import ( 20 "context" 21 "errors" 22 "io" 23 "io/ioutil" 24 "testing" 25 26 "github.com/docker/docker/api/types" 27 specs "github.com/opencontainers/image-spec/specs-go/v1" 28 29 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build" 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext" 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 36 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/tag" 37 "github.com/GoogleContainerTools/skaffold/testutil" 38 ) 39 40 func depLister(files map[string][]string) DependencyLister { 41 return func(_ context.Context, artifact *latest.Artifact) ([]string, error) { 42 list, found := files[artifact.ImageName] 43 if !found { 44 return nil, errors.New("unknown artifact") 45 } 46 return list, nil 47 } 48 } 49 50 type mockArtifactStore map[string]string 51 52 func (m mockArtifactStore) GetImageTag(imageName string) (string, bool) { return m[imageName], true } 53 func (m mockArtifactStore) Record(a *latest.Artifact, tag string) { m[a.ImageName] = tag } 54 func (m mockArtifactStore) GetArtifacts([]*latest.Artifact) ([]graph.Artifact, error) { 55 return nil, nil 56 } 57 58 type mockBuilder struct { 59 built []*latest.Artifact 60 push bool 61 dockerDaemon docker.LocalDaemon 62 store build.ArtifactStore 63 } 64 65 func (b *mockBuilder) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, artifacts []*latest.Artifact, _ platform.Resolver) ([]graph.Artifact, error) { 66 var built []graph.Artifact 67 68 for _, artifact := range artifacts { 69 b.built = append(b.built, artifact) 70 tag := tags[artifact.ImageName] 71 opts := docker.BuildOptions{Tag: tag, Mode: config.RunModes.Dev} 72 _, err := b.dockerDaemon.Build(ctx, out, artifact.Workspace, artifact.ImageName, artifact.DockerArtifact, opts) 73 if err != nil { 74 return nil, err 75 } 76 77 if b.push { 78 digest, err := b.dockerDaemon.Push(ctx, out, tag) 79 if err != nil { 80 return nil, err 81 } 82 83 built = append(built, graph.Artifact{ 84 ImageName: artifact.ImageName, 85 Tag: build.TagWithDigest(tag, digest), 86 }) 87 } else { 88 built = append(built, graph.Artifact{ 89 ImageName: artifact.ImageName, 90 Tag: tag, 91 }) 92 b.store.Record(artifact, tag) 93 } 94 } 95 96 return built, nil 97 } 98 99 type stubAuth struct{} 100 101 func (t stubAuth) GetAuthConfig(string) (types.AuthConfig, error) { 102 return types.AuthConfig{}, nil 103 } 104 func (t stubAuth) GetAllAuthConfigs(context.Context) (map[string]types.AuthConfig, error) { 105 return nil, nil 106 } 107 108 func TestCacheBuildLocal(t *testing.T) { 109 testutil.Run(t, "", func(t *testutil.T) { 110 tmpDir := t.NewTempDir(). 111 Write("dep1", "content1"). 112 Write("dep2", "content2"). 113 Write("dep3", "content3"). 114 Chdir() 115 116 tags := map[string]string{ 117 "artifact1": "artifact1:tag1", 118 "artifact2": "artifact2:tag2", 119 } 120 artifacts := []*latest.Artifact{ 121 {ImageName: "artifact1", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{}}}, 122 {ImageName: "artifact2", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{}}}, 123 } 124 deps := depLister(map[string][]string{ 125 "artifact1": {"dep1", "dep2"}, 126 "artifact2": {"dep3"}, 127 }) 128 129 // Mock Docker 130 t.Override(&docker.DefaultAuthHelper, stubAuth{}) 131 dockerDaemon := fakeLocalDaemon(&testutil.FakeAPIClient{}) 132 t.Override(&docker.NewAPIClient, func(context.Context, docker.Config) (docker.LocalDaemon, error) { 133 return dockerDaemon, nil 134 }) 135 136 // Mock args builder 137 t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) { 138 return args, nil 139 }) 140 141 // Create cache 142 cfg := &mockConfig{ 143 pipeline: latest.Pipeline{Build: latest.BuildConfig{BuildType: latest.BuildType{LocalBuild: &latest.LocalBuild{TryImportMissing: false}}}}, 144 cacheFile: tmpDir.Path("cache"), 145 } 146 store := make(mockArtifactStore) 147 artifactCache, err := NewCache(context.Background(), cfg, func(imageName string) (bool, error) { return true, nil }, deps, graph.ToArtifactGraph(artifacts), store) 148 t.CheckNoError(err) 149 150 // First build: Need to build both artifacts 151 builder := &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store} 152 bRes, err := artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 153 154 t.CheckNoError(err) 155 t.CheckDeepEqual(2, len(builder.built)) 156 t.CheckDeepEqual(2, len(bRes)) 157 158 // Second build: both artifacts are read from cache 159 // Artifacts should always be returned in their original order 160 builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store} 161 bRes, err = artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 162 163 t.CheckNoError(err) 164 t.CheckEmpty(builder.built) 165 t.CheckDeepEqual(2, len(bRes)) 166 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 167 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 168 169 // Third build: change first artifact's dependency 170 // Artifacts should always be returned in their original order 171 tmpDir.Write("dep1", "new content") 172 builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store} 173 bRes, err = artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 174 175 t.CheckNoError(err) 176 t.CheckDeepEqual(1, len(builder.built)) 177 t.CheckDeepEqual(2, len(bRes)) 178 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 179 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 180 181 // Fourth build: change second artifact's dependency 182 // Artifacts should always be returned in their original order 183 tmpDir.Write("dep3", "new content") 184 builder = &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: store} 185 bRes, err = artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 186 187 t.CheckNoError(err) 188 t.CheckDeepEqual(1, len(builder.built)) 189 t.CheckDeepEqual(2, len(bRes)) 190 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 191 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 192 }) 193 } 194 195 func TestCacheBuildRemote(t *testing.T) { 196 testutil.Run(t, "", func(t *testutil.T) { 197 tmpDir := t.NewTempDir(). 198 Write("dep1", "content1"). 199 Write("dep2", "content2"). 200 Write("dep3", "content3"). 201 Chdir() 202 203 tags := map[string]string{ 204 "artifact1": "artifact1:tag1", 205 "artifact2": "artifact2:tag2", 206 } 207 artifacts := []*latest.Artifact{ 208 {ImageName: "artifact1", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{}}}, 209 {ImageName: "artifact2", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{}}}, 210 } 211 deps := depLister(map[string][]string{ 212 "artifact1": {"dep1", "dep2"}, 213 "artifact2": {"dep3"}, 214 }) 215 216 // Mock Docker 217 dockerDaemon := fakeLocalDaemon(&testutil.FakeAPIClient{}) 218 t.Override(&docker.NewAPIClient, func(context.Context, docker.Config) (docker.LocalDaemon, error) { 219 return dockerDaemon, nil 220 }) 221 t.Override(&docker.DefaultAuthHelper, stubAuth{}) 222 t.Override(&docker.RemoteDigest, func(ref string, _ docker.Config, _ []specs.Platform) (string, error) { 223 switch ref { 224 case "artifact1:tag1": 225 return "sha256:51ae7fa00c92525c319404a3a6d400e52ff9372c5a39cb415e0486fe425f3165", nil 226 case "artifact2:tag2": 227 return "sha256:35bdf2619f59e6f2372a92cb5486f4a0bf9b86e0e89ee0672864db6ed9c51539", nil 228 default: 229 return "", errors.New("unknown remote tag") 230 } 231 }) 232 233 // Mock args builder 234 t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) { 235 return args, nil 236 }) 237 238 // Create cache 239 cfg := &mockConfig{ 240 pipeline: latest.Pipeline{Build: latest.BuildConfig{BuildType: latest.BuildType{LocalBuild: &latest.LocalBuild{TryImportMissing: false}}}}, 241 cacheFile: tmpDir.Path("cache"), 242 } 243 artifactCache, err := NewCache(context.Background(), cfg, func(imageName string) (bool, error) { return false, nil }, deps, graph.ToArtifactGraph(artifacts), make(mockArtifactStore)) 244 t.CheckNoError(err) 245 246 // First build: Need to build both artifacts 247 builder := &mockBuilder{dockerDaemon: dockerDaemon, push: true} 248 bRes, err := artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 249 250 t.CheckNoError(err) 251 t.CheckDeepEqual(2, len(builder.built)) 252 t.CheckDeepEqual(2, len(bRes)) 253 // Artifacts should always be returned in their original order 254 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 255 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 256 257 // Second build: both artifacts are read from cache 258 builder = &mockBuilder{dockerDaemon: dockerDaemon, push: true} 259 bRes, err = artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 260 261 t.CheckNoError(err) 262 t.CheckEmpty(builder.built) 263 t.CheckDeepEqual(2, len(bRes)) 264 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 265 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 266 267 // Third build: change one artifact's dependencies 268 tmpDir.Write("dep1", "new content") 269 builder = &mockBuilder{dockerDaemon: dockerDaemon, push: true} 270 bRes, err = artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 271 272 t.CheckNoError(err) 273 t.CheckDeepEqual(1, len(builder.built)) 274 t.CheckDeepEqual(2, len(bRes)) 275 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 276 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 277 }) 278 } 279 280 func TestCacheFindMissing(t *testing.T) { 281 testutil.Run(t, "", func(t *testutil.T) { 282 tmpDir := t.NewTempDir(). 283 Write("dep1", "content1"). 284 Write("dep2", "content2"). 285 Write("dep3", "content3"). 286 Chdir() 287 288 tags := map[string]string{ 289 "artifact1": "artifact1:tag1", 290 "artifact2": "artifact2:tag2", 291 } 292 artifacts := []*latest.Artifact{ 293 {ImageName: "artifact1", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{}}}, 294 {ImageName: "artifact2", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{}}}, 295 } 296 deps := depLister(map[string][]string{ 297 "artifact1": {"dep1", "dep2"}, 298 "artifact2": {"dep3"}, 299 }) 300 301 // Mock Docker 302 dockerDaemon := fakeLocalDaemon(&testutil.FakeAPIClient{}) 303 t.Override(&docker.NewAPIClient, func(context.Context, docker.Config) (docker.LocalDaemon, error) { 304 return dockerDaemon, nil 305 }) 306 t.Override(&docker.DefaultAuthHelper, stubAuth{}) 307 t.Override(&docker.RemoteDigest, func(ref string, _ docker.Config, _ []specs.Platform) (string, error) { 308 switch ref { 309 case "artifact1:tag1": 310 return "sha256:51ae7fa00c92525c319404a3a6d400e52ff9372c5a39cb415e0486fe425f3165", nil 311 case "artifact2:tag2": 312 return "sha256:35bdf2619f59e6f2372a92cb5486f4a0bf9b86e0e89ee0672864db6ed9c51539", nil 313 default: 314 return "", errors.New("unknown remote tag") 315 } 316 }) 317 318 // Mock args builder 319 t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) { 320 return args, nil 321 }) 322 323 // Create cache 324 cfg := &mockConfig{ 325 pipeline: latest.Pipeline{Build: latest.BuildConfig{BuildType: latest.BuildType{LocalBuild: &latest.LocalBuild{TryImportMissing: true}}}}, 326 cacheFile: tmpDir.Path("cache"), 327 } 328 artifactCache, err := NewCache(context.Background(), cfg, func(imageName string) (bool, error) { return false, nil }, deps, graph.ToArtifactGraph(artifacts), make(mockArtifactStore)) 329 t.CheckNoError(err) 330 331 // Because the artifacts are in the docker registry, we expect them to be imported correctly. 332 builder := &mockBuilder{dockerDaemon: dockerDaemon, push: false, store: make(mockArtifactStore)} 333 bRes, err := artifactCache.Build(context.Background(), ioutil.Discard, tags, artifacts, platform.Resolver{}, builder.Build) 334 335 t.CheckNoError(err) 336 t.CheckDeepEqual(0, len(builder.built)) 337 t.CheckDeepEqual(2, len(bRes)) 338 // Artifacts should always be returned in their original order 339 t.CheckDeepEqual("artifact1", bRes[0].ImageName) 340 t.CheckDeepEqual("artifact2", bRes[1].ImageName) 341 }) 342 } 343 344 type mockConfig struct { 345 runcontext.RunContext // Embedded to provide the default values. 346 cacheFile string 347 mode config.RunMode 348 pipeline latest.Pipeline 349 } 350 351 func (c *mockConfig) CacheArtifacts() bool { return true } 352 func (c *mockConfig) CacheFile() string { return c.cacheFile } 353 func (c *mockConfig) Mode() config.RunMode { return c.mode } 354 func (c *mockConfig) PipelineForImage(string) (latest.Pipeline, bool) { return c.pipeline, true }