github.com/GoogleContainerTools/skaffold/v2@v2.13.2/integration/multiplatform_test.go (about) 1 /* 2 Copyright 2022 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 integration 18 19 import ( 20 "fmt" 21 "os" 22 "strings" 23 "testing" 24 25 "github.com/google/uuid" 26 v1 "github.com/opencontainers/image-spec/specs-go/v1" 27 k8sv1 "k8s.io/api/core/v1" 28 29 "github.com/GoogleContainerTools/skaffold/v2/integration/skaffold" 30 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker" 31 "github.com/GoogleContainerTools/skaffold/v2/testutil" 32 ) 33 34 const ( 35 defaultRepo = "us-central1-docker.pkg.dev/k8s-skaffold/testing" 36 hybridClusterName = "integration-tests-hybrid" 37 armClusterName = "integration-tests-arm" 38 ) 39 40 func TestMultiPlatformWithRun(t *testing.T) { 41 isRunningInHybridCluster := os.Getenv("GKE_CLUSTER_NAME") == hybridClusterName 42 type image struct { 43 name string 44 pod string 45 } 46 47 tests := []struct { 48 description string 49 dir string 50 images []image 51 tag string 52 expectedPlatforms []v1.Platform 53 }{ 54 { 55 description: "Run with multiplatform linux/arm64 and linux/amd64", 56 dir: "examples/cross-platform-builds", 57 images: []image{{name: "skaffold-example", pod: "getting-started"}}, 58 tag: "multiplatform-integration-test", 59 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 60 }, 61 { 62 description: "Run with multiplatform linux/arm64 and linux/amd64 in a multi config project", 63 dir: "testdata/multi-config-pods", 64 images: []image{ 65 {name: "multi-config-module1", pod: "module1"}, 66 {name: "multi-config-module2", pod: "module2"}, 67 }, 68 tag: "multiplatform-integration-test", 69 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 70 }, 71 } 72 73 for _, test := range tests { 74 t.Run(test.description, func(t *testing.T) { 75 MarkIntegrationTest(t, NeedsGcp) 76 platforms := platformsCliValue(test.expectedPlatforms) 77 ns, client := SetupNamespace(t) 78 tag := fmt.Sprintf("%s-%s", test.tag, uuid.New().String()) 79 args := []string{"--platform", platforms, "--default-repo", defaultRepo, "--tag", tag, "--cache-artifacts=false"} 80 expectedPlatforms := expectedPlatformsForRunningCluster(test.expectedPlatforms) 81 82 skaffold.Run(args...).InDir(test.dir).InNs(ns.Name).RunOrFail(t) 83 defer skaffold.Delete().InDir(test.dir).InNs(ns.Name).RunOrFail(t) 84 85 for _, image := range test.images { 86 checkRemoteImagePlatforms(t, fmt.Sprintf("%s/%s:%s", defaultRepo, image.name, tag), expectedPlatforms) 87 88 if isRunningInHybridCluster { 89 pod := client.GetPod(image.pod) 90 checkNodeAffinity(t, test.expectedPlatforms, pod) 91 } 92 } 93 }) 94 } 95 } 96 97 func TestMultiplatformWithDevAndDebug(t *testing.T) { 98 const platformsExpectedInNodeAffinity = 1 99 const platformsExpectedInCreatedImage = 1 100 isRunningInHybridCluster := os.Getenv("GKE_CLUSTER_NAME") == hybridClusterName 101 102 type image struct { 103 name string 104 pod string 105 } 106 107 tests := []struct { 108 description string 109 dir string 110 images []image 111 tag string 112 command func(args ...string) *skaffold.RunBuilder 113 expectedPlatforms []v1.Platform 114 }{ 115 { 116 description: "Debug with multiplatform linux/arm64 and linux/amd64", 117 dir: "examples/cross-platform-builds", 118 images: []image{{name: "skaffold-example", pod: "getting-started"}}, 119 tag: "multiplatform-integration-test", 120 command: skaffold.Debug, 121 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 122 }, 123 { 124 description: "Debug with multiplatform linux/arm64 and linux/amd64 in a multi config project", 125 dir: "testdata/multi-config-pods", 126 images: []image{ 127 {name: "multi-config-module1", pod: "module1"}, 128 {name: "multi-config-module2", pod: "module2"}, 129 }, 130 tag: "multiplatform-integration-test", 131 command: skaffold.Debug, 132 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 133 }, 134 { 135 description: "Dev with multiplatform linux/arm64 and linux/amd64", 136 dir: "examples/cross-platform-builds", 137 images: []image{{name: "skaffold-example", pod: "getting-started"}}, 138 tag: "multiplatform-integration-test", 139 command: skaffold.Dev, 140 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 141 }, 142 { 143 description: "Dev with multiplatform linux/arm64 and linux/amd64 in a multi config project", 144 dir: "testdata/multi-config-pods", 145 images: []image{ 146 {name: "multi-config-module1", pod: "module1"}, 147 {name: "multi-config-module2", pod: "module2"}, 148 }, 149 tag: "multiplatform-integration-test", 150 command: skaffold.Dev, 151 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 152 }, 153 } 154 155 for _, test := range tests { 156 t.Run(test.description, func(t *testing.T) { 157 MarkIntegrationTest(t, NeedsGcp) 158 platforms := platformsCliValue(test.expectedPlatforms) 159 tag := fmt.Sprintf("%s-%s", test.tag, uuid.New().String()) 160 ns, client := SetupNamespace(t) 161 args := []string{"--platform", platforms, "--default-repo", defaultRepo, "--tag", tag, "--cache-artifacts=false"} 162 expectedPlatforms := expectedPlatformsForRunningCluster(test.expectedPlatforms) 163 164 test.command(args...).InDir(test.dir).InNs(ns.Name).RunBackground(t) 165 defer skaffold.Delete().InDir(test.dir).InNs(ns.Name).Run(t) 166 167 for _, image := range test.images { 168 client.WaitForPodsReady(image.pod) 169 createdImagePlatforms, err := docker.GetPlatforms(fmt.Sprintf("%s/%s:%s", defaultRepo, image.name, tag)) 170 failNowIfError(t, err) 171 172 if len(createdImagePlatforms) != platformsExpectedInCreatedImage { 173 t.Fatalf("there are more platforms in created Image than expected, found %v, expected %v", len(createdImagePlatforms), platformsExpectedInCreatedImage) 174 } 175 176 checkIfAPlatformMatch(t, expectedPlatforms, createdImagePlatforms[0]) 177 178 if isRunningInHybridCluster { 179 pod := client.GetPod(image.pod) 180 failIfNodeAffinityNotSet(t, pod) 181 nodeAffinityPlatforms := getPlatformsFromNodeAffinity(pod) 182 platformsInNodeAffinity := len(nodeAffinityPlatforms) 183 184 if platformsInNodeAffinity != platformsExpectedInNodeAffinity { 185 t.Fatalf("there are more platforms in NodeAffinity than expected, found %v, expected %v", platformsInNodeAffinity, platformsExpectedInNodeAffinity) 186 } 187 188 checkIfAPlatformMatch(t, expectedPlatforms, nodeAffinityPlatforms[0]) 189 } 190 } 191 }) 192 } 193 } 194 195 func TestMultiplatformWithDeploy(t *testing.T) { 196 isRunningInHybridCluster := os.Getenv("GKE_CLUSTER_NAME") == hybridClusterName 197 type image struct { 198 name string 199 pod string 200 } 201 202 tests := []struct { 203 description string 204 dir string 205 images []image 206 tag string 207 expectedPlatforms []v1.Platform 208 }{ 209 { 210 description: "Deploy with multiplatform linux/arm64 and linux/amd64", 211 dir: "examples/cross-platform-builds", 212 images: []image{{name: "skaffold-example", pod: "getting-started"}}, 213 tag: "multiplatform-integration-test", 214 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 215 }, 216 { 217 description: "Deploy with multiplatform linux/arm64 and linux/amd64 in a multi config project", 218 dir: "testdata/multi-config-pods", 219 images: []image{ 220 {name: "multi-config-module1", pod: "module1"}, 221 {name: "multi-config-module2", pod: "module2"}, 222 }, 223 tag: "multiplatform-integration-test", 224 expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}}, 225 }, 226 } 227 228 for _, test := range tests { 229 t.Run(test.description, func(t *testing.T) { 230 MarkIntegrationTest(t, NeedsGcp) 231 tmpfile := testutil.TempFile(t, "", []byte{}) 232 tag := fmt.Sprintf("%s-%s", test.tag, uuid.New().String()) 233 platforms := platformsCliValue(test.expectedPlatforms) 234 argsBuild := []string{"--platform", platforms, "--default-repo", defaultRepo, "--tag", tag, "--cache-artifacts=false", "--file-output", tmpfile} 235 argsDeploy := []string{"--build-artifacts", tmpfile, "--default-repo", defaultRepo, "--enable-platform-node-affinity=true"} 236 237 skaffold.Build(argsBuild...).InDir(test.dir).RunOrFail(t) 238 ns, client := SetupNamespace(t) 239 skaffold.Deploy(argsDeploy...).InDir(test.dir).InNs(ns.Name).RunOrFail(t) 240 defer skaffold.Delete().InDir(test.dir).InNs(ns.Name).RunOrFail(t) 241 242 for _, image := range test.images { 243 checkRemoteImagePlatforms(t, fmt.Sprintf("%s/%s:%s", defaultRepo, image.name, tag), test.expectedPlatforms) 244 245 if isRunningInHybridCluster { 246 pod := client.GetPod(image.pod) 247 checkNodeAffinity(t, test.expectedPlatforms, pod) 248 } 249 } 250 }) 251 } 252 } 253 254 func checkNodeAffinity(t *testing.T, expectedPlatforms []v1.Platform, pod *k8sv1.Pod) { 255 failIfNodeAffinityNotSet(t, pod) 256 nodeAffinityPlatforms := getPlatformsFromNodeAffinity(pod) 257 checkPlatformsEqual(t, nodeAffinityPlatforms, expectedPlatforms) 258 } 259 260 func failIfNodeAffinityNotSet(t *testing.T, pod *k8sv1.Pod) { 261 if pod.Spec.Affinity == nil { 262 t.Fatalf("Affinity not defined in spec") 263 } 264 265 if pod.Spec.Affinity.NodeAffinity == nil { 266 t.Fatalf("NodeAffinity not defined in spec") 267 } 268 269 if pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil { 270 t.Fatalf("RequiredDuringSchedulingIgnoredDuringExecution not defined in spec") 271 } 272 273 if pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms == nil { 274 t.Fatalf("NodeSelectorTerms not defined in spec") 275 } 276 } 277 278 func getPlatformsFromNodeAffinity(pod *k8sv1.Pod) []v1.Platform { 279 var platforms []v1.Platform 280 nodeAffinityPlatforms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms 281 282 for _, np := range nodeAffinityPlatforms { 283 os, arch := "", "" 284 for _, me := range np.MatchExpressions { 285 if me.Key == "kubernetes.io/os" { 286 os = strings.Join(me.Values, "") 287 } 288 289 if me.Key == "kubernetes.io/arch" { 290 arch = strings.Join(me.Values, "") 291 } 292 } 293 294 platforms = append(platforms, v1.Platform{OS: os, Architecture: arch}) 295 } 296 297 return platforms 298 } 299 300 func platformsCliValue(platforms []v1.Platform) string { 301 var platformsCliValue []string 302 for _, platform := range platforms { 303 platformsCliValue = append(platformsCliValue, fmt.Sprintf("%s/%s", platform.OS, platform.Architecture)) 304 } 305 306 return strings.Join(platformsCliValue, ",") 307 } 308 309 func expectedPlatformsForRunningCluster(platforms []v1.Platform) []v1.Platform { 310 switch clusterName := os.Getenv("GKE_CLUSTER_NAME"); clusterName { 311 case hybridClusterName: 312 return platforms 313 case armClusterName: 314 return []v1.Platform{{OS: "linux", Architecture: "arm64"}} 315 default: 316 return []v1.Platform{{OS: "linux", Architecture: "amd64"}} 317 } 318 } 319 320 func checkIfAPlatformMatch(t *testing.T, platforms []v1.Platform, expectedPlatform v1.Platform) { 321 const expectedMatchedPlatforms = 1 322 matchedPlatforms := 0 323 nodeAffinityPlatformValue := expectedPlatform.OS + "/" + expectedPlatform.Architecture 324 325 for _, platform := range platforms { 326 expectedPlatformValue := platform.OS + "/" + platform.Architecture 327 328 if nodeAffinityPlatformValue == expectedPlatformValue { 329 matchedPlatforms++ 330 } 331 } 332 333 if matchedPlatforms != expectedMatchedPlatforms { 334 t.Fatalf("Number of matched platforms should be %v", expectedMatchedPlatforms) 335 } 336 }