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  }