github.com/kotalco/kotal@v0.3.0/apis/ethereum/v1alpha1/node_validation_webhook_test.go (about)

     1  package v1alpha1
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/kotalco/kotal/apis/shared"
     7  	. "github.com/onsi/ginkgo/v2"
     8  	. "github.com/onsi/gomega"
     9  	"k8s.io/apimachinery/pkg/api/errors"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/util/validation/field"
    12  )
    13  
    14  var _ = Describe("Ethereum node validation", func() {
    15  
    16  	var (
    17  		networkID       uint = 77777
    18  		fixedDifficulty uint = 1500
    19  		coinbase             = shared.EthereumAddress("0xd2c21213027cbf4d46c16b55fa98e5252b048706")
    20  	)
    21  
    22  	createCases := []struct {
    23  		Title  string
    24  		Node   *Node
    25  		Errors field.ErrorList
    26  	}{
    27  		{
    28  			Title: "node #2",
    29  			Node: &Node{
    30  				ObjectMeta: metav1.ObjectMeta{
    31  					Name: "node-1",
    32  				},
    33  				Spec: NodeSpec{
    34  					Genesis: &Genesis{
    35  						ChainID: 444,
    36  					},
    37  					Client:  BesuClient,
    38  					Network: GoerliNetwork,
    39  				},
    40  			},
    41  			Errors: field.ErrorList{
    42  				{
    43  					Type:     field.ErrorTypeInvalid,
    44  					Field:    "spec.network",
    45  					BadValue: GoerliNetwork,
    46  					Detail:   "must be none if spec.genesis is specified",
    47  				},
    48  			},
    49  		},
    50  		{
    51  			Title: "node #3",
    52  			Node: &Node{
    53  				ObjectMeta: metav1.ObjectMeta{
    54  					Name: "node-1",
    55  				},
    56  				Spec: NodeSpec{
    57  					Client: BesuClient,
    58  				},
    59  			},
    60  			Errors: field.ErrorList{
    61  				{
    62  					Type:     field.ErrorTypeInvalid,
    63  					Field:    "spec.genesis",
    64  					BadValue: "",
    65  					Detail:   "must be specified if spec.network is none",
    66  				},
    67  			},
    68  		},
    69  		{
    70  			Title: "node #10",
    71  			Node: &Node{
    72  				ObjectMeta: metav1.ObjectMeta{
    73  					Name: "node-1",
    74  				},
    75  				Spec: NodeSpec{
    76  					Genesis: &Genesis{
    77  						ChainID: 55555,
    78  					},
    79  					Miner:  true,
    80  					Client: BesuClient,
    81  				},
    82  			},
    83  			Errors: field.ErrorList{
    84  				{
    85  					Type:     field.ErrorTypeInvalid,
    86  					Field:    "spec.coinbase",
    87  					BadValue: "",
    88  					Detail:   "must provide coinbase if miner is true",
    89  				},
    90  			},
    91  		},
    92  		{
    93  			Title: "node #10",
    94  			Node: &Node{
    95  				ObjectMeta: metav1.ObjectMeta{
    96  					Name: "node-1",
    97  				},
    98  				Spec: NodeSpec{
    99  					Client:  BesuClient,
   100  					Network: GoerliNetwork,
   101  					Engine:  true,
   102  				},
   103  			},
   104  			Errors: field.ErrorList{
   105  				{
   106  					Type:     field.ErrorTypeInvalid,
   107  					Field:    "spec.jwtSecretName",
   108  					BadValue: "",
   109  					Detail:   "must provide jwtSecretName if engine is true",
   110  				},
   111  			},
   112  		},
   113  		{
   114  			Title: "node #11",
   115  			Node: &Node{
   116  				ObjectMeta: metav1.ObjectMeta{
   117  					Name: "node-1",
   118  				},
   119  				Spec: NodeSpec{
   120  					Genesis: &Genesis{
   121  						ChainID: 55555,
   122  						IBFT2:   &IBFT2{},
   123  					},
   124  					Coinbase: shared.EthereumAddress("0x676aEda2E67D24eb304cFf75A5190824831E3399"),
   125  					Client:   BesuClient,
   126  				},
   127  			},
   128  			Errors: field.ErrorList{
   129  				{
   130  					Type:     field.ErrorTypeInvalid,
   131  					Field:    "spec.miner",
   132  					BadValue: false,
   133  					Detail:   "must set miner to true if coinbase is provided",
   134  				},
   135  			},
   136  		},
   137  		{
   138  			Title: "node #16",
   139  			Node: &Node{
   140  				ObjectMeta: metav1.ObjectMeta{
   141  					Name: "node-1",
   142  				},
   143  				Spec: NodeSpec{
   144  					Genesis: &Genesis{
   145  						ChainID:   55555,
   146  						NetworkID: networkID,
   147  						Ethash:    &Ethash{},
   148  					},
   149  					Client:   GethClient,
   150  					Miner:    true,
   151  					Coinbase: coinbase,
   152  				},
   153  			},
   154  			Errors: field.ErrorList{
   155  				{
   156  					Type:     field.ErrorTypeInvalid,
   157  					Field:    "spec.import",
   158  					BadValue: "",
   159  					Detail:   "must import coinbase account",
   160  				},
   161  			},
   162  		},
   163  		{
   164  			Title: "node #18",
   165  			Node: &Node{
   166  				ObjectMeta: metav1.ObjectMeta{
   167  					Name: "node-1",
   168  				},
   169  				Spec: NodeSpec{
   170  					Genesis: &Genesis{
   171  						ChainID:   55555,
   172  						NetworkID: networkID,
   173  						Ethash:    &Ethash{},
   174  					},
   175  					Client:   BesuClient,
   176  					Miner:    true,
   177  					Coinbase: coinbase,
   178  					Import: &ImportedAccount{
   179  						PrivateKeySecretName: "my-account-privatekey",
   180  						PasswordSecretName:   "my-account-password",
   181  					},
   182  				},
   183  			},
   184  			Errors: field.ErrorList{
   185  				{
   186  					Type:     field.ErrorTypeInvalid,
   187  					Field:    "spec.client",
   188  					BadValue: "besu",
   189  					Detail:   "client doesn't support importing accounts",
   190  				},
   191  			},
   192  		},
   193  		{
   194  			Title: "node #19",
   195  			Node: &Node{
   196  				ObjectMeta: metav1.ObjectMeta{
   197  					Name: "node-1",
   198  				},
   199  				Spec: NodeSpec{
   200  					Genesis: &Genesis{
   201  						NetworkID: networkID,
   202  						ChainID:   55555,
   203  						IBFT2:     &IBFT2{},
   204  					},
   205  					Client: GethClient,
   206  				},
   207  			},
   208  			Errors: field.ErrorList{
   209  				{
   210  					Type:     field.ErrorTypeInvalid,
   211  					Field:    "spec.client",
   212  					BadValue: "geth",
   213  					Detail:   "client doesn't support ibft2 consensus",
   214  				},
   215  			},
   216  		},
   217  		{
   218  			Title: "node #20",
   219  			Node: &Node{
   220  				ObjectMeta: metav1.ObjectMeta{
   221  					Name: "node-1",
   222  				},
   223  				Spec: NodeSpec{
   224  					Genesis: &Genesis{
   225  						ChainID:   55555,
   226  						NetworkID: networkID,
   227  						Clique:    &Clique{},
   228  					},
   229  					Client:   GethClient,
   230  					RPC:      true,
   231  					Miner:    true,
   232  					Coinbase: coinbase,
   233  					Import: &ImportedAccount{
   234  						PrivateKeySecretName: "my-account-privatekey",
   235  						PasswordSecretName:   "my-account-password",
   236  					},
   237  				},
   238  			},
   239  			Errors: field.ErrorList{
   240  				{
   241  					Type:     field.ErrorTypeInvalid,
   242  					Field:    "spec.rpc",
   243  					BadValue: true,
   244  					Detail:   "must be false if import is provided",
   245  				},
   246  			},
   247  		},
   248  		{
   249  			Title: "node #21",
   250  			Node: &Node{
   251  				ObjectMeta: metav1.ObjectMeta{
   252  					Name: "node-1",
   253  				},
   254  				Spec: NodeSpec{
   255  					Genesis: &Genesis{
   256  						ChainID:   55555,
   257  						NetworkID: networkID,
   258  						Clique:    &Clique{},
   259  					},
   260  					Client:   GethClient,
   261  					WS:       true,
   262  					Miner:    true,
   263  					Coinbase: coinbase,
   264  					Import: &ImportedAccount{
   265  						PrivateKeySecretName: "my-account-privatekey",
   266  						PasswordSecretName:   "my-account-password",
   267  					},
   268  				},
   269  			},
   270  			Errors: field.ErrorList{
   271  				{
   272  					Type:     field.ErrorTypeInvalid,
   273  					Field:    "spec.ws",
   274  					BadValue: true,
   275  					Detail:   "must be false if import is provided",
   276  				},
   277  			},
   278  		},
   279  		{
   280  			Title: "node #22",
   281  			Node: &Node{
   282  				ObjectMeta: metav1.ObjectMeta{
   283  					Name: "node-1",
   284  				},
   285  				Spec: NodeSpec{
   286  					Genesis: &Genesis{
   287  						ChainID:   55555,
   288  						NetworkID: networkID,
   289  						Clique:    &Clique{},
   290  					},
   291  					Client:   GethClient,
   292  					GraphQL:  true,
   293  					Miner:    true,
   294  					Coinbase: coinbase,
   295  					Import: &ImportedAccount{
   296  						PrivateKeySecretName: "my-account-privatekey",
   297  						PasswordSecretName:   "my-account-password",
   298  					},
   299  				},
   300  			},
   301  			Errors: field.ErrorList{
   302  				{
   303  					Type:     field.ErrorTypeInvalid,
   304  					Field:    "spec.graphql",
   305  					BadValue: true,
   306  					Detail:   "must be false if import is provided",
   307  				},
   308  			},
   309  		},
   310  		{
   311  			Title: "node #23",
   312  			Node: &Node{
   313  				ObjectMeta: metav1.ObjectMeta{
   314  					Name: "node-1",
   315  				},
   316  				Spec: NodeSpec{
   317  					Genesis: &Genesis{
   318  						ChainID:   55555,
   319  						NetworkID: networkID,
   320  						Ethash: &Ethash{
   321  							FixedDifficulty: &fixedDifficulty,
   322  						},
   323  					},
   324  					Client: GethClient,
   325  				},
   326  			},
   327  			Errors: field.ErrorList{
   328  				{
   329  					Type:     field.ErrorTypeInvalid,
   330  					Field:    "spec.client",
   331  					BadValue: "geth",
   332  					Detail:   "client doesn't support fixed difficulty pow networks",
   333  				},
   334  			},
   335  		},
   336  		{
   337  			Title: "node #24",
   338  			Node: &Node{
   339  				ObjectMeta: metav1.ObjectMeta{
   340  					Name: "node-1",
   341  				},
   342  				Spec: NodeSpec{
   343  					Client:   BesuClient,
   344  					Network:  GoerliNetwork,
   345  					SyncMode: LightSynchronization,
   346  				},
   347  			},
   348  			Errors: field.ErrorList{
   349  				{
   350  					Type:     field.ErrorTypeInvalid,
   351  					Field:    "spec.syncMode",
   352  					BadValue: LightSynchronization,
   353  					Detail:   "not supported by client besu",
   354  				},
   355  			},
   356  		},
   357  		{
   358  			Title: "node #24",
   359  			Node: &Node{
   360  				ObjectMeta: metav1.ObjectMeta{
   361  					Name: "node-1",
   362  				},
   363  				Spec: NodeSpec{
   364  					Client:   NethermindClient,
   365  					Network:  GoerliNetwork,
   366  					SyncMode: SnapSynchronization,
   367  				},
   368  			},
   369  			Errors: field.ErrorList{
   370  				{
   371  					Type:     field.ErrorTypeInvalid,
   372  					Field:    "spec.syncMode",
   373  					BadValue: SnapSynchronization,
   374  					Detail:   "not supported by client nethermind",
   375  				},
   376  			},
   377  		},
   378  		{
   379  			Title: "node #25",
   380  			Node: &Node{
   381  				ObjectMeta: metav1.ObjectMeta{
   382  					Name: "node-1",
   383  				},
   384  				Spec: NodeSpec{
   385  					Client:  BesuClient,
   386  					Network: GoerliNetwork,
   387  					Resources: shared.Resources{
   388  						CPU:      "2",
   389  						CPULimit: "1",
   390  					},
   391  				},
   392  			},
   393  			Errors: field.ErrorList{
   394  				{
   395  					Type:     field.ErrorTypeInvalid,
   396  					Field:    "spec.resources.cpuLimit",
   397  					BadValue: "1",
   398  					Detail:   "must be greater than or equal to cpu 2",
   399  				},
   400  			},
   401  		},
   402  		{
   403  			Title: "node #26",
   404  			Node: &Node{
   405  				ObjectMeta: metav1.ObjectMeta{
   406  					Name: "node-1",
   407  				},
   408  				Spec: NodeSpec{
   409  					Client:  BesuClient,
   410  					Network: GoerliNetwork,
   411  					Resources: shared.Resources{
   412  						CPU:         "1",
   413  						CPULimit:    "2",
   414  						Memory:      "2Gi",
   415  						MemoryLimit: "1Gi",
   416  					},
   417  				},
   418  			},
   419  			Errors: field.ErrorList{
   420  				{
   421  					Type:     field.ErrorTypeInvalid,
   422  					Field:    "spec.resources.memoryLimit",
   423  					BadValue: "1Gi",
   424  					Detail:   "must be greater than memory 2Gi",
   425  				},
   426  			},
   427  		},
   428  		{
   429  			Title: "node #28",
   430  			Node: &Node{
   431  				ObjectMeta: metav1.ObjectMeta{
   432  					Name: "node-1",
   433  				},
   434  				Spec: NodeSpec{
   435  					Client:  GethClient,
   436  					Network: GoerliNetwork,
   437  					Logging: shared.FatalLogs,
   438  				},
   439  			},
   440  			Errors: field.ErrorList{
   441  				{
   442  					Type:     field.ErrorTypeInvalid,
   443  					Field:    "spec.logging",
   444  					BadValue: shared.FatalLogs,
   445  					Detail:   "not supported by client geth",
   446  				},
   447  			},
   448  		},
   449  		{
   450  			Title: "node #29",
   451  			Node: &Node{
   452  				ObjectMeta: metav1.ObjectMeta{
   453  					Name: "node-1",
   454  				},
   455  				Spec: NodeSpec{
   456  					Client:  GethClient,
   457  					Network: GoerliNetwork,
   458  					Logging: shared.TraceLogs,
   459  				},
   460  			},
   461  			Errors: field.ErrorList{
   462  				{
   463  					Type:     field.ErrorTypeInvalid,
   464  					Field:    "spec.logging",
   465  					BadValue: shared.TraceLogs,
   466  					Detail:   "not supported by client geth",
   467  				},
   468  			},
   469  		},
   470  		{
   471  			Title: "node #37",
   472  			Node: &Node{
   473  				ObjectMeta: metav1.ObjectMeta{
   474  					Name: "node-1",
   475  				},
   476  				Spec: NodeSpec{
   477  					Client:  GethClient,
   478  					Network: GoerliNetwork,
   479  					GraphQL: true,
   480  				},
   481  			},
   482  			Errors: field.ErrorList{
   483  				{
   484  					Type:     field.ErrorTypeInvalid,
   485  					Field:    "spec.rpc",
   486  					BadValue: false,
   487  					Detail:   "must enable rpc if client is geth and graphql is enabled",
   488  				},
   489  			},
   490  		},
   491  		{
   492  			Title: "node #38",
   493  			Node: &Node{
   494  				ObjectMeta: metav1.ObjectMeta{
   495  					Name: "node-1",
   496  				},
   497  				Spec: NodeSpec{
   498  					Client:  NethermindClient,
   499  					Network: GoerliNetwork,
   500  					Hosts:   []string{"kotal.com"},
   501  				},
   502  			},
   503  			Errors: field.ErrorList{
   504  				{
   505  					Type:     field.ErrorTypeInvalid,
   506  					Field:    "spec.client",
   507  					BadValue: NethermindClient,
   508  					Detail:   "client doesn't support hosts whitelisting",
   509  				},
   510  			},
   511  		},
   512  		{
   513  			Title: "node #39",
   514  			Node: &Node{
   515  				ObjectMeta: metav1.ObjectMeta{
   516  					Name: "node-1",
   517  				},
   518  				Spec: NodeSpec{
   519  					Client:      NethermindClient,
   520  					Network:     GoerliNetwork,
   521  					CORSDomains: []string{"kotal.com"},
   522  				},
   523  			},
   524  			Errors: field.ErrorList{
   525  				{
   526  					Type:     field.ErrorTypeInvalid,
   527  					Field:    "spec.client",
   528  					BadValue: NethermindClient,
   529  					Detail:   "client doesn't support CORS domains",
   530  				},
   531  			},
   532  		},
   533  	}
   534  
   535  	// TODO: move .resources validation to shared resources package
   536  	updateCases := []struct {
   537  		Title   string
   538  		OldNode *Node
   539  		NewNode *Node
   540  		Errors  field.ErrorList
   541  	}{
   542  		{
   543  			Title: "node #1",
   544  			OldNode: &Node{
   545  				ObjectMeta: metav1.ObjectMeta{
   546  					Name: "node-1",
   547  				},
   548  				Spec: NodeSpec{
   549  					Client:  BesuClient,
   550  					Network: GoerliNetwork,
   551  				},
   552  			},
   553  			NewNode: &Node{
   554  				ObjectMeta: metav1.ObjectMeta{
   555  					Name: "node-1",
   556  				},
   557  				Spec: NodeSpec{
   558  					Client:  BesuClient,
   559  					Network: MainNetwork,
   560  				},
   561  			},
   562  			Errors: field.ErrorList{
   563  				{
   564  					Type:     field.ErrorTypeInvalid,
   565  					Field:    "spec.network",
   566  					BadValue: MainNetwork,
   567  					Detail:   "field is immutable",
   568  				},
   569  			},
   570  		},
   571  		{
   572  			Title: "node #2",
   573  			OldNode: &Node{
   574  				ObjectMeta: metav1.ObjectMeta{
   575  					Name: "node-2",
   576  				},
   577  				Spec: NodeSpec{
   578  					Client:  BesuClient,
   579  					Network: GoerliNetwork,
   580  					Resources: shared.Resources{
   581  						Storage: "20Gi",
   582  					},
   583  				},
   584  			},
   585  			NewNode: &Node{
   586  				ObjectMeta: metav1.ObjectMeta{
   587  					Name: "node-2",
   588  				},
   589  				Spec: NodeSpec{
   590  					Client:  BesuClient,
   591  					Network: GoerliNetwork,
   592  					Resources: shared.Resources{
   593  						Storage: "10Gi",
   594  					},
   595  				},
   596  			},
   597  			Errors: field.ErrorList{
   598  				{
   599  					Type:     field.ErrorTypeInvalid,
   600  					Field:    "spec.resources.storage",
   601  					BadValue: "10Gi",
   602  					Detail:   "must be greater than or equal to old storage 20Gi",
   603  				},
   604  			},
   605  		},
   606  		{
   607  			Title: "node #3",
   608  			OldNode: &Node{
   609  				ObjectMeta: metav1.ObjectMeta{
   610  					Name: "node-3",
   611  				},
   612  				Spec: NodeSpec{
   613  					Client:  BesuClient,
   614  					Network: GoerliNetwork,
   615  					Resources: shared.Resources{
   616  						CPU:      "1",
   617  						CPULimit: "2",
   618  					},
   619  				},
   620  			},
   621  			NewNode: &Node{
   622  				ObjectMeta: metav1.ObjectMeta{
   623  					Name: "node-3",
   624  				},
   625  				Spec: NodeSpec{
   626  					Client:  BesuClient,
   627  					Network: GoerliNetwork,
   628  					Resources: shared.Resources{
   629  						CPU:      "2",
   630  						CPULimit: "1",
   631  					},
   632  				},
   633  			},
   634  			Errors: field.ErrorList{
   635  				{
   636  					Type:     field.ErrorTypeInvalid,
   637  					Field:    "spec.resources.cpuLimit",
   638  					BadValue: "1",
   639  					Detail:   "must be greater than or equal to cpu 2",
   640  				},
   641  			},
   642  		},
   643  		{
   644  			Title: "node #4",
   645  			OldNode: &Node{
   646  				ObjectMeta: metav1.ObjectMeta{
   647  					Name: "node-4",
   648  				},
   649  				Spec: NodeSpec{
   650  					Client:  BesuClient,
   651  					Network: GoerliNetwork,
   652  					Resources: shared.Resources{
   653  						Memory:      "1Gi",
   654  						MemoryLimit: "2Gi",
   655  					},
   656  				},
   657  			},
   658  			NewNode: &Node{
   659  				ObjectMeta: metav1.ObjectMeta{
   660  					Name: "node-4",
   661  				},
   662  				Spec: NodeSpec{
   663  					Client:  BesuClient,
   664  					Network: GoerliNetwork,
   665  					Resources: shared.Resources{
   666  						Memory:      "1Gi",
   667  						MemoryLimit: "1Gi",
   668  					},
   669  				},
   670  			},
   671  			Errors: field.ErrorList{
   672  				{
   673  					Type:     field.ErrorTypeInvalid,
   674  					Field:    "spec.resources.memoryLimit",
   675  					BadValue: "1Gi",
   676  					Detail:   "must be greater than memory 1Gi",
   677  				},
   678  			},
   679  		},
   680  		{
   681  			Title: "node #5",
   682  			OldNode: &Node{
   683  				ObjectMeta: metav1.ObjectMeta{
   684  					Name: "node-5",
   685  				},
   686  				Spec: NodeSpec{
   687  					Client:  BesuClient,
   688  					Network: GoerliNetwork,
   689  				},
   690  			},
   691  			NewNode: &Node{
   692  				ObjectMeta: metav1.ObjectMeta{
   693  					Name: "node-5",
   694  				},
   695  				Spec: NodeSpec{
   696  					Client:  GethClient,
   697  					Network: GoerliNetwork,
   698  				},
   699  			},
   700  			Errors: field.ErrorList{
   701  				{
   702  					Type:     field.ErrorTypeInvalid,
   703  					Field:    "spec.client",
   704  					BadValue: GethClient,
   705  					Detail:   "field is immutable",
   706  				},
   707  			},
   708  		},
   709  	}
   710  
   711  	Context("While creating node", func() {
   712  		for _, c := range createCases {
   713  			func() {
   714  				cc := c
   715  				It(fmt.Sprintf("Should validate %s", cc.Title), func() {
   716  					cc.Node.Default()
   717  					_, err := cc.Node.ValidateCreate()
   718  
   719  					errStatus := err.(*errors.StatusError)
   720  
   721  					causes := shared.ErrorsToCauses(cc.Errors)
   722  
   723  					Expect(errStatus.ErrStatus.Details.Causes).To(ContainElements(causes))
   724  				})
   725  			}()
   726  		}
   727  	})
   728  
   729  	Context("While updating node", func() {
   730  		for _, c := range updateCases {
   731  			func() {
   732  				cc := c
   733  				It(fmt.Sprintf("Should validate %s", cc.Title), func() {
   734  					cc.OldNode.Default()
   735  					cc.NewNode.Default()
   736  					_, err := cc.NewNode.ValidateUpdate(cc.OldNode)
   737  
   738  					errStatus := err.(*errors.StatusError)
   739  
   740  					causes := shared.ErrorsToCauses(cc.Errors)
   741  
   742  					Expect(errStatus.ErrStatus.Details.Causes).To(ContainElements(causes))
   743  				})
   744  			}()
   745  		}
   746  	})
   747  
   748  })