github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/vsphere/validation_test.go (about)

     1  package vsphere
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"net"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/golang/mock/gomock"
    12  	"github.com/sirupsen/logrus/hooks/test"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/vmware/govmomi/session"
    15  	"github.com/vmware/govmomi/simulator"
    16  	"github.com/vmware/govmomi/vapi/rest"
    17  	vapitags "github.com/vmware/govmomi/vapi/tags"
    18  	"github.com/vmware/govmomi/vim25/mo"
    19  	vim25types "github.com/vmware/govmomi/vim25/types"
    20  	"k8s.io/apimachinery/pkg/util/validation/field"
    21  
    22  	"github.com/openshift/installer/pkg/asset/installconfig/vsphere/mock"
    23  	"github.com/openshift/installer/pkg/ipnet"
    24  	"github.com/openshift/installer/pkg/types"
    25  	"github.com/openshift/installer/pkg/types/vsphere"
    26  )
    27  
    28  var (
    29  	validCIDR     = "10.0.0.0/16"
    30  	mu            sync.Mutex
    31  	stopListening = false
    32  )
    33  
    34  const (
    35  	tagTestCreateRegionCategory     = 0x01
    36  	tagTestCreateZoneCategory       = 0x02
    37  	tagTestAttachRegionTags         = 0x04
    38  	tagTestAttachZoneTags           = 0x08
    39  	tagTestNothingCreatedOrAttached = 0x10
    40  )
    41  
    42  const wildcardDNS = "nip.io"
    43  
    44  func validIPIInstallConfig() *types.InstallConfig {
    45  	return &types.InstallConfig{
    46  		Networking: &types.Networking{
    47  			MachineNetwork: []types.MachineNetworkEntry{
    48  				{CIDR: *ipnet.MustParseCIDR(validCIDR)},
    49  			},
    50  		},
    51  		Publish: types.ExternalPublishingStrategy,
    52  		Platform: types.Platform{
    53  			VSphere: &vsphere.Platform{
    54  				APIVIPs:     []string{"192.168.111.0"},
    55  				IngressVIPs: []string{"192.168.111.1"},
    56  			},
    57  		},
    58  	}
    59  }
    60  
    61  func validMultiVCenterPlatform() *vsphere.Platform {
    62  	return &vsphere.Platform{
    63  		VCenters: []vsphere.VCenter{
    64  			{
    65  				Server:   "test-vcenter",
    66  				Port:     443,
    67  				Username: "test_username",
    68  				Password: "test_password",
    69  				Datacenters: []string{
    70  					"DC0",
    71  				},
    72  			},
    73  		},
    74  		FailureDomains: []vsphere.FailureDomain{
    75  			{
    76  				Name:   "test-east-1a",
    77  				Region: "test-region-east",
    78  				Zone:   "test-zone-1a",
    79  				Topology: vsphere.Topology{
    80  					Datacenter:     "DC0",
    81  					ComputeCluster: "/DC0/host/DC0_C0",
    82  					ResourcePool:   "/DC0/host/DC0_C0/Resources/test-resourcepool",
    83  					Folder:         "/DC0/vm",
    84  					Networks: []string{
    85  						"DC0_DVPG0",
    86  					},
    87  					Datastore: "/DC0/datastore/LocalDS_0",
    88  				}},
    89  		},
    90  	}
    91  }
    92  
    93  func teardownTagAttachmentTest(ctx context.Context, tagMgr *vapitags.Manager) error {
    94  	tags, err := tagMgr.ListTags(ctx)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	attachedMos, err := tagMgr.GetAttachedObjectsOnTags(ctx, tags)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	for _, attachedMo := range attachedMos {
   104  		for _, mo := range attachedMo.ObjectIDs {
   105  			err := tagMgr.DetachTag(ctx, attachedMo.TagID, mo)
   106  			if err != nil {
   107  				return err
   108  			}
   109  		}
   110  		err := tagMgr.DeleteTag(ctx, attachedMo.Tag)
   111  		if err != nil {
   112  			return err
   113  		}
   114  	}
   115  
   116  	categories, err := tagMgr.GetCategories(ctx)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	for _, category := range categories {
   121  		cat := category
   122  		err := tagMgr.DeleteCategory(ctx, &cat)
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  func setupTagAttachmentTest(ctx context.Context, restClient *rest.Client, finder Finder, attachmentMask int64) (*vapitags.Manager, error) {
   131  	tagMgr := vapitags.NewManager(restClient)
   132  
   133  	if attachmentMask&tagTestCreateRegionCategory != 0 {
   134  		categoryID, err := tagMgr.CreateCategory(ctx, &vapitags.Category{
   135  			Name:        "openshift-region",
   136  			Description: "region tag category",
   137  		})
   138  		if err != nil {
   139  			return nil, err
   140  		}
   141  
   142  		if attachmentMask&tagTestAttachRegionTags != 0 {
   143  			tagID, err := tagMgr.CreateTag(ctx, &vapitags.Tag{
   144  				Name:       "us-east",
   145  				CategoryID: categoryID,
   146  			})
   147  			if err != nil {
   148  				return nil, err
   149  			}
   150  			datacenters, err := finder.DatacenterList(ctx, "/...")
   151  			if err != nil {
   152  				return nil, err
   153  			}
   154  			for _, datacenter := range datacenters {
   155  				err = tagMgr.AttachTag(ctx, tagID, datacenter)
   156  				if err != nil {
   157  					return nil, err
   158  				}
   159  			}
   160  		}
   161  	}
   162  	if attachmentMask&tagTestCreateZoneCategory != 0 {
   163  		categoryID, err := tagMgr.CreateCategory(ctx, &vapitags.Category{
   164  			Name:        "openshift-zone",
   165  			Description: "zone tag category",
   166  		})
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		if attachmentMask&tagTestAttachZoneTags != 0 {
   171  			tagID, err := tagMgr.CreateTag(ctx, &vapitags.Tag{
   172  				Name:       "us-east-1a",
   173  				CategoryID: categoryID,
   174  			})
   175  			if err != nil {
   176  				return nil, err
   177  			}
   178  			clusters, err := finder.ClusterComputeResourceList(ctx, "/...")
   179  			if err != nil {
   180  				return nil, err
   181  			}
   182  			for _, cluster := range clusters {
   183  				err = tagMgr.AttachTag(ctx, tagID, cluster)
   184  				if err != nil {
   185  					return nil, err
   186  				}
   187  			}
   188  		}
   189  	}
   190  
   191  	return tagMgr, nil
   192  }
   193  
   194  // simulatorHelper starts the govmomi simulator
   195  // returning the simulator.Server so that we can defer closing later and
   196  // shutdown the simulator to change versions.
   197  func simulatorHelper(t *testing.T, setVersionToSupported bool) (*validationContext, *simulator.Server, *rest.Client, error) {
   198  	t.Helper()
   199  
   200  	server, err := mock.StartSimulator(setVersionToSupported)
   201  	if err != nil {
   202  		return nil, nil, nil, err
   203  	}
   204  
   205  	ctrl := gomock.NewController(t)
   206  	defer ctrl.Finish()
   207  
   208  	ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Minute)
   209  	defer cancel()
   210  
   211  	finder, err := mock.GetFinder(server)
   212  	if err != nil {
   213  		return nil, nil, nil, err
   214  	}
   215  
   216  	client, _, err := mock.GetClient(server)
   217  	if err != nil {
   218  		return nil, nil, nil, err
   219  	}
   220  
   221  	restClient := rest.NewClient(client)
   222  
   223  	defer restClient.CloseIdleConnections()
   224  	err = restClient.Login(context.TODO(), simulator.DefaultLogin)
   225  	if err != nil {
   226  		return nil, nil, nil, err
   227  	}
   228  
   229  	vmFolder, err := finder.Folder(ctx, "/DC0/vm")
   230  	if err != nil {
   231  		return nil, nil, nil, err
   232  	}
   233  
   234  	myFolder, err := vmFolder.CreateFolder(ctx, "my-folder")
   235  	if err != nil {
   236  		return nil, nil, nil, err
   237  	}
   238  	var folder mo.Folder
   239  
   240  	err = myFolder.Properties(ctx, myFolder.Reference(), nil, &folder)
   241  	if err != nil {
   242  		return nil, nil, nil, err
   243  	}
   244  
   245  	resourcePools, err := finder.ResourcePoolList(ctx, "/DC0/host/DC0_C0")
   246  	if err != nil {
   247  		return nil, nil, nil, err
   248  	}
   249  	_, err = resourcePools[0].Create(ctx, "test-resourcepool", vim25types.DefaultResourceConfigSpec())
   250  	if err != nil {
   251  		return nil, nil, nil, err
   252  	}
   253  
   254  	sessionMgr := session.NewManager(client)
   255  	userSession, err := sessionMgr.UserSession(ctx)
   256  	if err != nil {
   257  		return nil, nil, nil, err
   258  	}
   259  	username := userSession.UserName
   260  	validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, t, ctrl, finder, username, nil, nil, nil)
   261  	if err != nil {
   262  		return nil, nil, nil, err
   263  	}
   264  
   265  	return &validationContext{
   266  		AuthManager: validPermissionsAuthManagerClient,
   267  		Finder:      finder,
   268  		Client:      client,
   269  	}, server, restClient, nil
   270  }
   271  
   272  func TestValidateFailureDomains(t *testing.T) {
   273  	validationCtx, server, restClient, err := simulatorHelper(t, true)
   274  	if err != nil {
   275  		t.Error(err)
   276  		return
   277  	}
   278  	defer server.Close()
   279  
   280  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   281  	defer cancel()
   282  
   283  	tests := []struct {
   284  		name             string
   285  		installConfig    *types.InstallConfig
   286  		validationMethod func(*validationContext, *vsphere.FailureDomain, bool) field.ErrorList
   287  		failureDomain    *vsphere.FailureDomain
   288  		tagTestMask      int64
   289  		checkTags        bool
   290  		expectErr        string
   291  	}{
   292  		{
   293  			name:             "multi-zone validation",
   294  			failureDomain:    &validMultiVCenterPlatform().FailureDomains[0],
   295  			validationMethod: validateFailureDomain,
   296  		}, {
   297  			name: "multi-zone validation - invalid datacenter",
   298  			failureDomain: func() *vsphere.FailureDomain {
   299  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   300  				failureDomain.Topology.Datacenter = "invalid-dc"
   301  				return failureDomain
   302  			}(),
   303  			validationMethod: validateFailureDomain,
   304  			checkTags:        false,
   305  			expectErr:        `[platform.vsphere.failureDomains.topology.datacenter: Invalid value: "invalid-dc": datacenter 'invalid-dc' not found, platform.vsphere.failureDomains.topology.datastore: Invalid value: "invalid-dc": unable to find datacenter invalid-dc: datacenter 'invalid-dc' not found, platform.vsphere.failureDomains.topology: Invalid value: "invalid-dc": datacenter './invalid-dc' not found]`,
   306  		},
   307  		{
   308  			name: "multi-zone validation - invalid cluster",
   309  			failureDomain: func() *vsphere.FailureDomain {
   310  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   311  				failureDomain.Topology.ComputeCluster = "/DC0/host/invalid-cluster"
   312  				return failureDomain
   313  			}(),
   314  			validationMethod: validateFailureDomain,
   315  			checkTags:        false,
   316  			expectErr:        `\[platform.vsphere.failureDomains.topology.computeCluster: Invalid value: "/DC0/host/invalid-cluster": cluster '/DC0/host/invalid-cluster' not found, platform.vsphere.failureDomains.topology: Invalid value: "DC0_DVPG0": could not find vSphere cluster at /DC0/host/invalid-cluster: cluster '/DC0/host/invalid-cluster' not found\]`,
   317  		},
   318  		{
   319  			name: "multi-zone validation - missing cluster",
   320  			failureDomain: func() *vsphere.FailureDomain {
   321  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   322  				failureDomain.Topology.ComputeCluster = ""
   323  				return failureDomain
   324  			}(),
   325  			validationMethod: validateFailureDomain,
   326  			checkTags:        false,
   327  			expectErr:        `[platform.vsphere: Internal error: please specify a datacenter, platform.vsphere.failureDomains.topology.computeCluster: Required value: must specify the cluster]`,
   328  		},
   329  		{
   330  			name: "multi-zone validation - missing datastore",
   331  			failureDomain: func() *vsphere.FailureDomain {
   332  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   333  				failureDomain.Topology.Datastore = ""
   334  				return failureDomain
   335  			}(),
   336  			validationMethod: validateFailureDomain,
   337  			checkTags:        false,
   338  			expectErr:        `^platform.vsphere.failureDomains.topology.datastore: Required value: must specify the datastore$`,
   339  		},
   340  		{
   341  			name: "multi-zone validation - invalid resource pool",
   342  			failureDomain: func() *vsphere.FailureDomain {
   343  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   344  				failureDomain.Topology.ResourcePool = "/DC0/host/DC0_C0/Resources/invalid-resourcepool"
   345  				return failureDomain
   346  			}(),
   347  			validationMethod: validateFailureDomain,
   348  			checkTags:        false,
   349  			expectErr:        `^platform.vsphere.failureDomains.topology.resourcePool: Invalid value: "/DC0/host/DC0_C0/Resources/invalid-resourcepool": resource pool '/DC0/host/DC0_C0/Resources/invalid-resourcepool' not found$`,
   350  		},
   351  		{
   352  			name: "multi-zone validation - invalid network",
   353  			failureDomain: func() *vsphere.FailureDomain {
   354  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   355  				failureDomain.Topology.Networks = []string{
   356  					"invalid-network",
   357  				}
   358  				return failureDomain
   359  			}(),
   360  			validationMethod: validateFailureDomain,
   361  			checkTags:        false,
   362  			expectErr:        `^platform.vsphere.failureDomains.topology: Invalid value: "invalid-network": unable to find network provided$`,
   363  		}, {
   364  			name: "multi-zone validation - create missing folder",
   365  			failureDomain: func() *vsphere.FailureDomain {
   366  				failureDomain := &validMultiVCenterPlatform().FailureDomains[0]
   367  				failureDomain.Topology.Folder = "/DC0/vm/create-missing-folder"
   368  				return failureDomain
   369  			}(),
   370  			validationMethod: validateFailureDomain,
   371  			checkTags:        false,
   372  			expectErr:        ``,
   373  		}, {
   374  			name:             "multi-zone tag categories present and tags attached",
   375  			validationMethod: validateFailureDomain,
   376  			failureDomain:    &validMultiVCenterPlatform().FailureDomains[0],
   377  			checkTags:        true,
   378  			tagTestMask: tagTestCreateZoneCategory |
   379  				tagTestCreateRegionCategory |
   380  				tagTestAttachRegionTags |
   381  				tagTestAttachZoneTags,
   382  		}, {
   383  			name:             "multi-zone tag categories, missing zone tag attachment",
   384  			validationMethod: validateFailureDomain,
   385  			failureDomain:    &validMultiVCenterPlatform().FailureDomains[0],
   386  			checkTags:        true,
   387  			tagTestMask: tagTestCreateZoneCategory |
   388  				tagTestCreateRegionCategory |
   389  				tagTestAttachRegionTags,
   390  			expectErr: "platform.vsphere.failureDomains.topology.computeCluster: Internal error: tag associated with tag category openshift-zone not attached to this resource or ancestor",
   391  		}, {
   392  			name:             "multi-zone tag categories, missing zone and region tag categories",
   393  			validationMethod: validateFailureDomain,
   394  			failureDomain:    &validMultiVCenterPlatform().FailureDomains[0],
   395  			checkTags:        true,
   396  			tagTestMask:      tagTestNothingCreatedOrAttached,
   397  			expectErr:        "platform.vsphere: Internal error: tag categories openshift-zone and openshift-region must be created",
   398  		},
   399  	}
   400  
   401  	for _, test := range tests {
   402  		t.Run(test.name, func(t *testing.T) {
   403  			var err error
   404  			if test.validationMethod != nil {
   405  				var tagMgr *vapitags.Manager
   406  
   407  				if test.tagTestMask != 0 {
   408  					tagMgr, err = setupTagAttachmentTest(ctx, restClient, validationCtx.Finder, test.tagTestMask)
   409  					if err != nil {
   410  						assert.NoError(t, err)
   411  					}
   412  					validationCtx.zoneTagCategoryID = ""
   413  					validationCtx.regionTagCategoryID = ""
   414  					validationCtx.TagManager = tagMgr
   415  				}
   416  
   417  				err = test.validationMethod(validationCtx, test.failureDomain, test.checkTags).ToAggregate()
   418  				if test.tagTestMask != 0 {
   419  					err := teardownTagAttachmentTest(ctx, tagMgr)
   420  					if err != nil {
   421  						assert.NoError(t, err)
   422  					}
   423  					validationCtx.zoneTagCategoryID = ""
   424  					validationCtx.regionTagCategoryID = ""
   425  					validationCtx.TagManager = nil
   426  				}
   427  			} else {
   428  				err = errors.New("no test method defined")
   429  			}
   430  			if test.expectErr == "" {
   431  				assert.NoError(t, err)
   432  			} else {
   433  				assert.Regexp(t, test.expectErr, err)
   434  			}
   435  		})
   436  	}
   437  }
   438  
   439  func Test_validateVCenterVersion(t *testing.T) {
   440  	tests := []struct {
   441  		name                  string
   442  		setVersionToSupported bool
   443  		fldPath               *field.Path
   444  		expectErr             string
   445  	}{
   446  		{
   447  			name:                  "valid vcenter version",
   448  			setVersionToSupported: true,
   449  			fldPath:               field.NewPath("platform").Child("vsphere").Child("vcenters"),
   450  			expectErr:             ``,
   451  		},
   452  		{
   453  			name:                  "unsupported vcenter version",
   454  			setVersionToSupported: false,
   455  			fldPath:               field.NewPath("platform").Child("vsphere").Child("vcenters"),
   456  			expectErr:             `platform.vsphere.vcenters: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. Current vCenter version: 6.5.0, build: 5973321`,
   457  		},
   458  	}
   459  
   460  	for _, test := range tests {
   461  		t.Run(test.name, func(t *testing.T) {
   462  			validationCtx, server, _, err := simulatorHelper(t, test.setVersionToSupported)
   463  
   464  			if err != nil {
   465  				t.Error(err)
   466  			}
   467  			defer server.Close()
   468  
   469  			err = validateVCenterVersion(validationCtx, test.fldPath).ToAggregate()
   470  
   471  			if test.expectErr != "" {
   472  				assert.Regexp(t, test.expectErr, err)
   473  			} else if err != nil {
   474  				assert.NoError(t, err)
   475  			}
   476  		})
   477  	}
   478  }
   479  
   480  func Test_validateESXiVersion(t *testing.T) {
   481  	vSphereFldPath := field.NewPath("platform").Child("vsphere")
   482  	computeClusterFldPath := vSphereFldPath.Child("failureDomains").Child("topology").Child("computeCluster")
   483  	platform := validMultiVCenterPlatform()
   484  
   485  	tests := []struct {
   486  		name                  string
   487  		setVersionToSupported bool
   488  		computeClusterPath    string
   489  		expectErr             string
   490  	}{
   491  		{
   492  			name:                  "valid esxi version",
   493  			setVersionToSupported: true,
   494  			computeClusterPath:    platform.FailureDomains[0].Topology.ComputeCluster,
   495  			expectErr:             ``,
   496  		},
   497  		{
   498  			name:                  "unsupported esxi version",
   499  			setVersionToSupported: false,
   500  			computeClusterPath:    platform.FailureDomains[0].Topology.ComputeCluster,
   501  			expectErr:             `[platform.vsphere.failureDomains.topology.computeCluster: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: DC0_C0_H0 is version: 6.5.0 and build: 5969303, platform.vsphere.failureDomains.topology.computeCluster: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: DC0_C0_H1 is version: 6.5.0 and build: 5969303, platform.vsphere.failureDomains.topology.computeCluster: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: DC0_C0_H2 is version: 6.5.0 and build: 5969303]`,
   502  		},
   503  		{
   504  			name:                  "computeCluster not found",
   505  			setVersionToSupported: true,
   506  			computeClusterPath:    "/DC0/host/invalid-cluster",
   507  			expectErr:             `platform.vsphere.failureDomains.topology.computeCluster: Invalid value: "/DC0/host/invalid-cluster": cluster '/DC0/host/invalid-cluster' not found`,
   508  		},
   509  	}
   510  
   511  	for _, test := range tests {
   512  		t.Run(test.name, func(t *testing.T) {
   513  			validationCtx, server, _, err := simulatorHelper(t, test.setVersionToSupported)
   514  
   515  			if err != nil {
   516  				t.Error(err)
   517  			}
   518  			defer server.Close()
   519  
   520  			err = validateESXiVersion(validationCtx, test.computeClusterPath, vSphereFldPath, computeClusterFldPath).ToAggregate()
   521  
   522  			if test.expectErr != "" {
   523  				assert.Regexp(t, test.expectErr, err)
   524  			} else if err != nil {
   525  				t.Error(err)
   526  			}
   527  		})
   528  	}
   529  }
   530  
   531  func Test_ensureDNS(t *testing.T) {
   532  	platformFieldPath := field.NewPath("platform")
   533  
   534  	resolver := &net.Resolver{
   535  		PreferGo: true,
   536  		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
   537  			d := net.Dialer{
   538  				Timeout: time.Millisecond * time.Duration(10000),
   539  			}
   540  			return d.DialContext(ctx, network, "1.1.1.1:53")
   541  		},
   542  	}
   543  
   544  	tests := []struct {
   545  		name          string
   546  		installConfig *types.InstallConfig
   547  		expectErr     string
   548  	}{
   549  		{
   550  			name: "valid dns",
   551  			installConfig: func() *types.InstallConfig {
   552  				installConfig := validIPIInstallConfig()
   553  				installConfig.ObjectMeta.Name = "0a000803"
   554  				installConfig.BaseDomain = wildcardDNS
   555  
   556  				return installConfig
   557  			}(),
   558  			expectErr: ``,
   559  		},
   560  	}
   561  	for _, test := range tests {
   562  		t.Run(test.name, func(t *testing.T) {
   563  			err := ensureDNS(test.installConfig, platformFieldPath, resolver).ToAggregate()
   564  			if test.expectErr != "" {
   565  				assert.Regexp(t, test.expectErr, err)
   566  			} else if err != nil {
   567  				t.Error(err)
   568  			}
   569  		})
   570  	}
   571  }
   572  
   573  // lbHelper creates a listening port of 6443 on loopback (127.0.0.1)
   574  // If stopListening is true the loop is broken and the port closes.
   575  func lbHelper(t *testing.T) {
   576  	t.Helper()
   577  	mu.Lock()
   578  	stopListening = false
   579  	mu.Unlock()
   580  	addr, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:6443")
   581  	if err != nil {
   582  		t.Error(err)
   583  	}
   584  
   585  	listen, err := net.ListenTCP("tcp4", addr)
   586  	if err != nil {
   587  		t.Error(err)
   588  	}
   589  	for {
   590  		mu.Lock()
   591  		if stopListening {
   592  			err = listen.Close()
   593  			if err != nil {
   594  				t.Logf("closing tcp listener: %s", err.Error())
   595  			}
   596  			break
   597  		}
   598  		mu.Unlock()
   599  
   600  		_, err = listen.Accept()
   601  
   602  		if err != nil {
   603  			t.Error(err)
   604  		}
   605  	}
   606  	mu.Unlock()
   607  }
   608  
   609  // Test_ensureLoadBalancer uses lbHelper to create an open
   610  // on 127.0.0.1:6443. Also uses nip.io as the base domain
   611  // and cluster name as 7f000001 which equals 127.0.0.1
   612  // Examples for testing logrus output from here:
   613  // https://maxchadwick.xyz/blog/testing-log-output-in-go-logrus
   614  func Test_ensureLoadBalancer(t *testing.T) {
   615  	go lbHelper(t)
   616  	logger, hook := test.NewNullLogger()
   617  	localLogger = logger
   618  
   619  	tests := []struct {
   620  		name          string
   621  		installConfig *types.InstallConfig
   622  		stopTCPListen bool
   623  		expectLevel   string
   624  		expectWarn    string
   625  	}{
   626  		{
   627  			name: "valid lb",
   628  			installConfig: func() *types.InstallConfig {
   629  				installConfig := validIPIInstallConfig()
   630  				installConfig.ObjectMeta.Name = "7f000001"
   631  				installConfig.BaseDomain = wildcardDNS
   632  				installConfig.VSphere.APIVIPs = []string{}
   633  				installConfig.VSphere.IngressVIPs = []string{}
   634  
   635  				return installConfig
   636  			}(),
   637  			stopTCPListen: false,
   638  			expectLevel:   ``,
   639  			expectWarn:    ``,
   640  		},
   641  		{
   642  			name: "warn lb",
   643  			installConfig: func() *types.InstallConfig {
   644  				installConfig := validIPIInstallConfig()
   645  				installConfig.ObjectMeta.Name = "7f000002"
   646  				installConfig.BaseDomain = wildcardDNS
   647  				installConfig.VSphere.APIVIPs = []string{}
   648  				installConfig.VSphere.IngressVIPs = []string{}
   649  
   650  				return installConfig
   651  			}(),
   652  			stopTCPListen: true,
   653  			expectLevel:   `warning`,
   654  			expectWarn:    `Installation may fail, load balancer not available: dial tcp 127.0.0.2:6443: connect: connection refused`,
   655  		},
   656  	}
   657  	for _, test := range tests {
   658  		t.Run(test.name, func(t *testing.T) {
   659  			if test.stopTCPListen {
   660  				mu.Lock()
   661  				stopListening = true
   662  				mu.Unlock()
   663  			}
   664  			mu.Lock()
   665  			ensureLoadBalancer(test.installConfig)
   666  			mu.Unlock()
   667  			if test.expectWarn != "" {
   668  				// there should be only one entry
   669  				entries := hook.AllEntries()
   670  				assert.NotEmpty(t, entries)
   671  				for _, e := range entries {
   672  					assert.Equal(t, test.expectLevel, e.Level.String())
   673  					assert.Regexp(t, test.expectWarn, e.Message)
   674  				}
   675  			}
   676  			hook.Reset()
   677  		})
   678  	}
   679  }
   680  
   681  func Test_compareCurrentToTemplate(t *testing.T) {
   682  	logger, hook := test.NewNullLogger()
   683  	localLogger = logger
   684  
   685  	tests := []struct {
   686  		name                   string
   687  		rhcosReleaseVersion    string
   688  		templateProductVersion string
   689  		expectErr              string
   690  		expectWarn             string
   691  	}{
   692  		{
   693  			name:                   "same version",
   694  			rhcosReleaseVersion:    "412.86.202303211731-0",
   695  			templateProductVersion: "412.86.202303211731-0",
   696  			expectErr:              ``,
   697  			expectWarn:             ``,
   698  		},
   699  		{
   700  			name:                   "template newer than current rhcos",
   701  			rhcosReleaseVersion:    "412.86.202303211731-0",
   702  			templateProductVersion: "414.92.202304252144-0",
   703  			expectErr:              `rhcos version: 414.92.202304252144-0 is too many revisions ahead current version: 412.86.202303211731-0`,
   704  			expectWarn:             ``,
   705  		},
   706  		{
   707  			name:                   "rhcos one release newer than template",
   708  			rhcosReleaseVersion:    "413.92.202304252144-0",
   709  			templateProductVersion: "412.86.202303211731-0",
   710  			expectErr:              ``,
   711  			expectWarn:             `rhcos version: 412.86.202303211731-0 is behind current version: 413.92.202304252144-0, installation may fail`,
   712  		},
   713  		{
   714  			name:                   "rhcos two release newer than template",
   715  			rhcosReleaseVersion:    "414.92.202304252144-0",
   716  			templateProductVersion: "412.86.202303211731-0",
   717  			expectErr:              `rhcos version: 412.86.202303211731-0 is too many revisions behind current version: 414.92.202304252144-0`,
   718  			expectWarn:             ``,
   719  		},
   720  	}
   721  	for _, tc := range tests {
   722  		t.Run(tc.name, func(t *testing.T) {
   723  			err := compareCurrentToTemplate(tc.templateProductVersion, tc.rhcosReleaseVersion)
   724  
   725  			if tc.expectWarn != "" {
   726  				// there should be only one entry
   727  				entries := hook.AllEntries()
   728  				assert.NotEmpty(t, entries)
   729  				for _, e := range entries {
   730  					assert.Regexp(t, tc.expectWarn, e.Message)
   731  				}
   732  			}
   733  
   734  			if tc.expectErr != "" {
   735  				assert.Regexp(t, tc.expectErr, err)
   736  			} else if err != nil {
   737  				t.Error(err)
   738  			}
   739  			hook.Reset()
   740  		})
   741  	}
   742  }