github.com/kubernetes-incubator/kube-aws@v0.16.4/test/integration/plugin_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"os"
     5  	"reflect"
     6  	"regexp"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/kubernetes-incubator/kube-aws/core/root"
    12  	"github.com/kubernetes-incubator/kube-aws/core/root/config"
    13  	"github.com/kubernetes-incubator/kube-aws/pkg/api"
    14  	"github.com/kubernetes-incubator/kube-aws/pkg/model"
    15  	"github.com/kubernetes-incubator/kube-aws/plugin"
    16  	"github.com/kubernetes-incubator/kube-aws/test/helper"
    17  )
    18  
    19  func TestPlugin(t *testing.T) {
    20  	kubeAwsSettings := newKubeAwsSettingsFromEnv(t)
    21  
    22  	s3URI, s3URIExists := os.LookupEnv("KUBE_AWS_S3_DIR_URI")
    23  
    24  	if !s3URIExists || s3URI == "" {
    25  		s3URI = "s3://mybucket/mydir"
    26  		t.Logf(`Falling back s3URI to a stub value "%s" for tests of validating stack templates. No assets will actually be uploaded to S3`, s3URI)
    27  	}
    28  
    29  	minimalValidConfigYaml := kubeAwsSettings.minimumValidClusterYamlWithAZ("c")
    30  
    31  	validCases := []struct {
    32  		context       string
    33  		clusterYaml   string
    34  		plugins       []helper.TestPlugin
    35  		assertConfig  []ConfigTester
    36  		assertCluster []ClusterTester
    37  	}{
    38  		{
    39  			context: "WithAddons",
    40  			clusterYaml: minimalValidConfigYaml + `
    41  
    42  
    43  kubeAwsPlugins:
    44    myPlugin:
    45      enabled: true
    46      queue:
    47        name: baz1
    48      oidc:
    49        issuer:
    50          url: "https://login.example.com/"
    51      systemdTemplateValue: "elvis123"
    52  
    53  worker:
    54    nodePools:
    55    - name: pool1
    56      kubeAwsPlugins:
    57        myPlugin:
    58          enabled: true
    59          queue:
    60            name: baz2
    61  `,
    62  			plugins: []helper.TestPlugin{
    63  				helper.TestPlugin{
    64  					Name: "my-plugin",
    65  					Files: map[string]string{
    66  						"assets/controller/baz.txt": "controller-baz",
    67  						"assets/etcd/baz.txt":       "etcd-baz",
    68  						"assets/worker/baz.txt":     "worker-baz",
    69  					},
    70  					Yaml: `
    71  metadata:
    72    name: my-plugin
    73    version: 0.0.1
    74  spec:
    75    cluster:
    76      # This is the defaults for the values passed to templates like:
    77      # * cloudformation.stacks.{controlPlane,nodePool,root}.resources.append and
    78      # * kubernetes.apiserer.flags[].value
    79      #
    80      # The defaults can be overridden from cluster.yaml via:
    81      # * kubeAwsPlugins.pluginName.* and
    82      # * worker.nodePools[].kubeAwsPlugins.pluginName.*
    83      values:
    84        queue:
    85          name: bar
    86        oidc:
    87          issuer:
    88            url: unspecified
    89      cloudformation:
    90        stacks:
    91          controlPlane:
    92            resources:
    93              content: |
    94                {
    95                  "QueueFromMyPlugin": {
    96                    "Type": "AWS::SQS::Queue",
    97                    "Properties": {
    98                      "QueueName": {{quote .Values.queue.name}}
    99                    }
   100                  }
   101                }
   102            tags:
   103              content: |
   104                {
   105                  "Tags": [
   106                    {
   107                      "Key": "control/tag",
   108                      "PropagateAtLaunch": "false",
   109                      "Value": ""
   110                    }
   111                  ]
   112                }
   113            outputs:
   114              content: |
   115                {
   116                  "ControlStack": {
   117                    "Description": "ControlStack",
   118                    "Value": "ControlOutput"
   119                  }
   120                }
   121          nodePool:
   122            resources:
   123              content: |
   124                {
   125                  "QueueFromMyPlugin": {
   126                    "Type": "AWS::SQS::Queue",
   127                    "Properties": {
   128                      "QueueName": {{quote .Values.queue.name}}
   129                    }
   130                  }
   131                }
   132            tags:
   133              content: |
   134                {
   135                  "Tags": [
   136                    {
   137                      "Key": "nodepool/tag",
   138                      "PropagateAtLaunch": "false",
   139                      "Value": ""
   140                    }
   141                  ]
   142                }
   143            outputs:
   144              content: |
   145                {
   146                  "NodeStack": {
   147                    "Description": "NodeStack",
   148                    "Value": "NodeOutput"
   149                  }
   150                }
   151          root:
   152            resources:
   153              content: |
   154                {
   155                  "QueueFromMyPlugin": {
   156                    "Type": "AWS::SQS::Queue",
   157                    "Properties": {
   158                    "QueueName": {{quote .Values.queue.name}}
   159                    }
   160                  }
   161                }
   162          etcd:
   163            resources:
   164              content: |
   165                {
   166                  "QueueFromMyPlugin": {
   167                    "Type": "AWS::SQS::Queue",
   168                    "Properties": {
   169                    "QueueName": {{quote .Values.queue.name}}
   170                    }
   171                  }
   172                }
   173            outputs:
   174              content: |
   175                {
   176                  "EtcdStack": {
   177                    "Description": "EtcdStack",
   178                    "Value": "EtcdOutput"
   179                  }
   180                }
   181          network:
   182            resources:
   183              content: |
   184                {
   185                  "QueueFromMyPlugin": {
   186                    "Type": "AWS::SQS::Queue",
   187                    "Properties": {
   188                    "QueueName": {{quote .Values.queue.name}}
   189                    }
   190                  }
   191                }
   192            outputs:
   193              content: |
   194                {
   195                  "NetworkStack": {
   196                    "Description": "NetworkStack",
   197                    "Value": "NetworkOutput"
   198                  }
   199                }
   200      kubernetes:
   201        controllerManager:
   202          flags:
   203            - name: "secure-port"
   204              value: "11257"
   205        kubelet:
   206          flags:
   207            - name: "healthz-bind-address"
   208              value: "0.0.0.0"
   209        kubeScheduler:
   210          flags:
   211            - name: "secure-port"
   212              value: "11259"
   213        kubeProxy:
   214          config:
   215            metricsBindAddress: "0.0.0.0"
   216        apiserver:
   217          flags:
   218          - name: "oidc-issuer-url"
   219            value: "{{ .Values.oidc.issuer.url}}"
   220          volumes:
   221          - name: "mycreds"
   222            path: "/etc/my/creds"
   223      machine:
   224        roles:
   225          controller:
   226            iam:
   227              policy:
   228                statements:
   229                - actions:
   230                  - "ec2:Describe*"
   231                  effect: "Allow"
   232                  resources:
   233                  - "*"
   234            kubelet:
   235              nodeLabels:
   236                role: controller
   237            systemd:
   238              units:
   239              - name: save-queue-name.service
   240                content: |
   241                  [Unit] {{ .Values.systemdTemplateValue }}
   242            files:
   243            - path: /var/kube-aws/bar.txt
   244              permissions: 0644
   245              content: controller-bar
   246            - path: /var/kube-aws/baz.txt
   247              permissions: 0644
   248              source:
   249                path: assets/controller/baz.txt
   250          etcd:
   251            iam:
   252              policy:
   253                statements:
   254                - actions:
   255                  - "ec2:Describe*"
   256                  effect: "Allow"
   257                  resources:
   258                  - "*"
   259            systemd:
   260              units:
   261              - name: save-queue-name.service
   262                content: |
   263                  [Unit] {{ .Values.systemdTemplateValue }}
   264            files:
   265            - path: /var/kube-aws/bar.txt
   266              permissions: 0644
   267              content: etcd-bar
   268            - path: /var/kube-aws/baz.txt
   269              permissions: 0644
   270              source:
   271                path: assets/etcd/baz.txt
   272          worker:
   273            iam:
   274              policy:
   275                statements:
   276                - actions:
   277                  - "ec2:*"
   278                  effect: "Allow"
   279                  resources:
   280                  - "*"
   281            kubelet:
   282              nodeLabels:
   283                role: worker
   284              featureGates:
   285                Accelerators: "true"
   286            systemd:
   287              units:
   288              - name: save-queue-name.service
   289                content: |
   290                  [Unit] {{ .Values.systemdTemplateValue }}
   291            files:
   292            - path: /var/kube-aws/bar.txt
   293              permissions: 0644
   294              content: worker-bar
   295            - path: /var/kube-aws/baz.txt
   296              permissions: 0644
   297              source:
   298                path: assets/worker/baz.txt
   299  `,
   300  				},
   301  			},
   302  			assertConfig: []ConfigTester{
   303  				func(c *config.Config, t *testing.T) {
   304  					cp := c.PluginConfigs["myPlugin"]
   305  
   306  					if !cp.Enabled {
   307  						t.Errorf("The plugin should have been enabled: %+v", cp)
   308  					}
   309  
   310  					if q, ok := cp.Values["queue"].(map[string]interface{}); ok {
   311  						if m, ok := q["name"].(string); ok {
   312  							if m != "baz1" {
   313  								t.Errorf("The plugin should have queue.name set to \"baz1\", but was set to \"%s\"", m)
   314  							}
   315  						}
   316  					}
   317  
   318  					np := c.NodePools[0].Plugins["myPlugin"]
   319  
   320  					if !np.Enabled {
   321  						t.Errorf("The plugin should have been enabled: %+v", np)
   322  					}
   323  
   324  					if q, ok := np.Values["queue"].(map[string]interface{}); ok {
   325  						if m, ok := q["name"].(string); ok {
   326  							if m != "baz2" {
   327  								t.Errorf("The plugin should have queue.name set to \"baz2\", but was set to \"%s\"", m)
   328  							}
   329  						}
   330  					}
   331  				},
   332  			},
   333  			assertCluster: []ClusterTester{
   334  				func(c *root.Cluster, t *testing.T) {
   335  					cp := c.ControlPlane()
   336  					np := c.NodePools()[0]
   337  					etcd := c.Etcd()
   338  					network := c.Network()
   339  
   340  					{
   341  						e := api.CustomFile{
   342  							Path:        "/var/kube-aws/bar.txt",
   343  							Permissions: 0644,
   344  							Content:     "controller-bar",
   345  						}
   346  						a := cp.Config.Controller.CustomFiles[0]
   347  						if !reflect.DeepEqual(e, a) {
   348  							t.Errorf("Unexpected controller custom file from plugin: expected=%v actual=%v", e, a)
   349  						}
   350  					}
   351  					{
   352  						e := api.CustomFile{
   353  							Path:        "/var/kube-aws/baz.txt",
   354  							Permissions: 0644,
   355  							Content:     "controller-baz",
   356  						}
   357  						a := cp.Config.Controller.CustomFiles[1]
   358  						if !reflect.DeepEqual(e, a) {
   359  							t.Errorf("Unexpected controller custom file from plugin: expected=%v actual=%v", e, a)
   360  						}
   361  					}
   362  					{
   363  						e := api.IAMPolicyStatements{
   364  							api.IAMPolicyStatement{
   365  								Effect:    "Allow",
   366  								Actions:   []string{"ec2:Describe*"},
   367  								Resources: []string{"*"},
   368  							},
   369  						}
   370  						a := cp.Config.Controller.IAMConfig.Policy.Statements
   371  						if !reflect.DeepEqual(e, a) {
   372  							t.Errorf("Unexpected controller iam policy statements from plugin: expected=%v actual=%v", e, a)
   373  						}
   374  					}
   375  
   376  					{
   377  						e := api.CustomFile{
   378  							Path:        "/var/kube-aws/bar.txt",
   379  							Permissions: 0644,
   380  							Content:     "etcd-bar",
   381  						}
   382  						a := etcd.Config.Etcd.CustomFiles[0]
   383  						if !reflect.DeepEqual(e, a) {
   384  							t.Errorf("Unexpected etcd custom file from plugin: expected=%v actual=%v", e, a)
   385  						}
   386  					}
   387  					{
   388  						e := api.CustomFile{
   389  							Path:        "/var/kube-aws/baz.txt",
   390  							Permissions: 0644,
   391  							Content:     "etcd-baz",
   392  						}
   393  						a := etcd.Config.Etcd.CustomFiles[1]
   394  						if !reflect.DeepEqual(e, a) {
   395  							t.Errorf("Unexpected etcd custom file from plugin: expected=%v actual=%v", e, a)
   396  						}
   397  					}
   398  					{
   399  						e := api.IAMPolicyStatements{
   400  							api.IAMPolicyStatement{
   401  								Effect:    "Allow",
   402  								Actions:   []string{"ec2:Describe*"},
   403  								Resources: []string{"*"},
   404  							},
   405  						}
   406  						a := etcd.Config.Etcd.IAMConfig.Policy.Statements
   407  						if !reflect.DeepEqual(e, a) {
   408  							t.Errorf("Unexpected etcd iam policy statements from plugin: expected=%v actual=%v", e, a)
   409  						}
   410  					}
   411  
   412  					{
   413  						e := api.CustomFile{
   414  							Path:        "/var/kube-aws/bar.txt",
   415  							Permissions: 0644,
   416  							Content:     "worker-bar",
   417  						}
   418  						a := np.NodePoolConfig.CustomFiles[0]
   419  						if !reflect.DeepEqual(e, a) {
   420  							t.Errorf("Unexpected worker custom file from plugin: expected=%v actual=%v", e, a)
   421  						}
   422  					}
   423  					{
   424  						e := api.CustomFile{
   425  							Path:        "/var/kube-aws/baz.txt",
   426  							Permissions: 0644,
   427  							Content:     "worker-baz",
   428  						}
   429  						a := np.NodePoolConfig.CustomFiles[1]
   430  						if !reflect.DeepEqual(e, a) {
   431  							t.Errorf("Unexpected worker custom file from plugin: expected=%v actual=%v", e, a)
   432  						}
   433  					}
   434  					{
   435  						e := api.IAMPolicyStatements{
   436  							api.IAMPolicyStatement{
   437  								Effect:    "Allow",
   438  								Actions:   []string{"ec2:*"},
   439  								Resources: []string{"*"},
   440  							},
   441  						}
   442  						a := np.NodePoolConfig.IAMConfig.Policy.Statements
   443  						if diff := cmp.Diff(a, e); diff != "" {
   444  							t.Errorf("Unexpected worker iam policy statements from plugin: %s", diff)
   445  						}
   446  					}
   447  
   448  					// A kube-aws plugin can inject systemd units - which are evaluated as templates
   449  					controllerUserdataS3Part := cp.UserData["Controller"].Parts[api.USERDATA_S3].Asset.Content
   450  					if !strings.Contains(controllerUserdataS3Part, "save-queue-name.service") {
   451  						t.Errorf("Invalid controller userdata: missing systemd unit save-queue-name.service: %v", controllerUserdataS3Part)
   452  					}
   453  					if !strings.Contains(controllerUserdataS3Part, "elvis123") {
   454  						t.Errorf("Invalid controller userdata: systemd unit not templated: %v", controllerUserdataS3Part)
   455  					}
   456  
   457  					etcdUserdataS3Part := etcd.UserData["Etcd"].Parts[api.USERDATA_S3].Asset.Content
   458  					if !strings.Contains(etcdUserdataS3Part, "save-queue-name.service") {
   459  						t.Errorf("Invalid etcd userdata: missing systemd unit save-queue-name.service: %v", etcdUserdataS3Part)
   460  					}
   461  					if !strings.Contains(etcdUserdataS3Part, "elvis123") {
   462  						t.Errorf("Invalid etcd userdata: systemd unit not templated: %v", etcdUserdataS3Part)
   463  					}
   464  
   465  					workerUserdataS3Part := np.UserData["Worker"].Parts[api.USERDATA_S3].Asset.Content
   466  					if !strings.Contains(workerUserdataS3Part, "save-queue-name.service") {
   467  						t.Errorf("Invalid worker userdata: missing systemd unit save-queue-name.service: %v", workerUserdataS3Part)
   468  					}
   469  					if !strings.Contains(workerUserdataS3Part, "elvis123") {
   470  						t.Errorf("Invalid worker userdata: systemd unit not templated: %v", workerUserdataS3Part)
   471  					}
   472  
   473  					// A kube-aws plugin can inject custom cfn stack resources
   474  					controlPlaneStackTemplate, err := cp.RenderStackTemplateAsString()
   475  					if err != nil {
   476  						t.Errorf("failed to render control-plane stack template: %v", err)
   477  					}
   478  					if !strings.Contains(controlPlaneStackTemplate, "QueueFromMyPlugin") {
   479  						t.Errorf("Invalid control-plane stack template: missing resource QueueFromMyPlugin: %v", controlPlaneStackTemplate)
   480  					}
   481  					if !strings.Contains(controlPlaneStackTemplate, `"QueueName":"baz1"`) {
   482  						t.Errorf("Invalid control-plane stack template: missing QueueName baz1: %v", controlPlaneStackTemplate)
   483  					}
   484  					if !strings.Contains(controlPlaneStackTemplate, `"Action":["ec2:Describe*"]`) {
   485  						t.Errorf("Invalid control-plane stack template: missing iam policy statement ec2:Describe*: %v", controlPlaneStackTemplate)
   486  					}
   487  
   488  					// A kube-aws plugin can inject custom cfn stack resources into the etcd stack
   489  					etcdStackTemplate, err := etcd.RenderStackTemplateAsString()
   490  
   491  					if err != nil {
   492  						t.Errorf("failed to render control-plane stack template: %v", err)
   493  					}
   494  					if !strings.Contains(etcdStackTemplate, "QueueFromMyPlugin") {
   495  						t.Errorf("Invalid etcd stack template: missing resource QueueFromMyPlugin: %v", etcdStackTemplate)
   496  					}
   497  					if !strings.Contains(etcdStackTemplate, `"QueueName":"baz1"`) {
   498  						t.Errorf("Invalid etcd stack template: missing QueueName baz1: %v", etcdStackTemplate)
   499  					}
   500  					if !strings.Contains(etcdStackTemplate, `"Action":["ec2:Describe*"]`) {
   501  						t.Errorf("Invalid etcd stack template: missing iam policy statement ec2:Describe*: %v", etcdStackTemplate)
   502  					}
   503  
   504  					// A kube-aws plugin can inject custom cfn stack resources into the network stack
   505  					networkStackTemplate, err := network.RenderStackTemplateAsString()
   506  
   507  					if err != nil {
   508  						t.Errorf("failed to render control-plane stack template: %v", err)
   509  					}
   510  					if !strings.Contains(networkStackTemplate, "QueueFromMyPlugin") {
   511  						t.Errorf("Invalid networks stack template: missing resource QueueFromMyPlugin: %v", networkStackTemplate)
   512  					}
   513  					if !strings.Contains(networkStackTemplate, `"QueueName":"baz1"`) {
   514  						t.Errorf("Invalid network stack template: missing QueueName baz1: %v", networkStackTemplate)
   515  					}
   516  
   517  					rootStackTemplate, err := c.RenderStackTemplateAsString()
   518  					if err != nil {
   519  						t.Errorf("failed to render root stack template: %v", err)
   520  					}
   521  					if !strings.Contains(rootStackTemplate, "QueueFromMyPlugin") {
   522  						t.Errorf("Invalid root stack template: missing resource QueueFromMyPlugin: %v", rootStackTemplate)
   523  					}
   524  					if !strings.Contains(rootStackTemplate, `"QueueName":"baz1"`) {
   525  						t.Errorf("Invalid root stack template: missing QueueName baz1: %v", rootStackTemplate)
   526  					}
   527  
   528  					nodePoolStackTemplate, err := np.RenderStackTemplateAsString()
   529  					if err != nil {
   530  						t.Errorf("failed to render worker node pool stack template: %v", err)
   531  					}
   532  					if !strings.Contains(nodePoolStackTemplate, "QueueFromMyPlugin") {
   533  						t.Errorf("Invalid worker node pool stack template: missing resource QueueFromMyPlugin: %v", nodePoolStackTemplate)
   534  					}
   535  					if !strings.Contains(nodePoolStackTemplate, `"QueueName":"baz2"`) {
   536  						t.Errorf("Invalid worker node pool stack template: missing QueueName baz2: %v", nodePoolStackTemplate)
   537  					}
   538  					if !strings.Contains(nodePoolStackTemplate, `"Action":["ec2:*"]`) {
   539  						t.Errorf("Invalid worker node pool stack template: missing iam policy statement ec2:*: %v", nodePoolStackTemplate)
   540  					}
   541  
   542  					// A kube-aws plugin can inject control plane and node pool tags
   543  					if !strings.Contains(controlPlaneStackTemplate, `"Key":"control/tag"`) {
   544  						t.Errorf("Invalid control-plane stack template: missing tag control/tag: %v", controlPlaneStackTemplate)
   545  					}
   546  					if !strings.Contains(nodePoolStackTemplate, `"Key":"nodepool/tag"`) {
   547  						t.Errorf("Invalid node-pool stack template: missing tag nodepool/tag: %v", nodePoolStackTemplate)
   548  					}
   549  
   550  					// A kube-aws plugin can inject cfn outputs
   551  					if !strings.Contains(controlPlaneStackTemplate, `"Value":"ControlOutput"`) {
   552  						t.Errorf("Invalid control-plane stack template: missing output ControlOutput: %v", controlPlaneStackTemplate)
   553  					}
   554  					if !strings.Contains(nodePoolStackTemplate, `"Value":"NodeOutput"`) {
   555  						t.Errorf("Invalid node-pool stack template: missing output NodeOutput: %v", nodePoolStackTemplate)
   556  					}
   557  					if !strings.Contains(etcdStackTemplate, `"Value":"EtcdOutput"`) {
   558  						t.Errorf("Invalid etcd stack template: missing output EtcdOutput: %v", etcdStackTemplate)
   559  					}
   560  					if !strings.Contains(networkStackTemplate, `"Value":"NetworkOutput"`) {
   561  						t.Errorf("Invalid network stack template: missing output Network: %v", networkStackTemplate)
   562  					}
   563  
   564  					// A kube-aws plugin can inject node labels
   565  					if !strings.Contains(controllerUserdataS3Part, "role=controller") {
   566  						t.Error("missing controller node label: role=controller")
   567  					}
   568  
   569  					if !strings.Contains(workerUserdataS3Part, "role=worker") {
   570  						t.Error("missing worker node label: role=worker")
   571  					}
   572  
   573  					// A kube-aws plugin can activate feature gates
   574  					if match, _ := regexp.MatchString(`Accelerators: true`, workerUserdataS3Part); !match {
   575  						t.Error("missing worker feature gate: Accelerators: true")
   576  					}
   577  
   578  					// A kube-aws plugin can add volume mounts to apiserver pod
   579  					if !strings.Contains(controllerUserdataS3Part, `mountPath: "/etc/my/creds"`) {
   580  						t.Errorf("missing apiserver volume mount: /etc/my/creds")
   581  					}
   582  
   583  					// A kube-aws plugin can add volumes to apiserver pod
   584  					if !strings.Contains(controllerUserdataS3Part, `path: "/etc/my/creds"`) {
   585  						t.Errorf("missing apiserver volume: /etc/my/creds")
   586  					}
   587  
   588  					// A kube-aws plugin can add flags to apiserver
   589  					if !strings.Contains(controllerUserdataS3Part, `--oidc-issuer-url=https://login.example.com/`) {
   590  						t.Errorf("missing apiserver flag: --oidc-issuer-url=https://login.example.com/")
   591  					}
   592  
   593  					// A kube-aws plugin can add flags to the kube-controllermanager
   594  					if !strings.Contains(controllerUserdataS3Part, `--secure-port=11259`) {
   595  						t.Errorf("missing kube-controllermanager flag: --secure-port=11259")
   596  					}
   597  
   598  					// A kube-aws plugin can add flags to the kubescheduler
   599  					if !strings.Contains(controllerUserdataS3Part, `- --secure-port=11259`) {
   600  						t.Errorf("missing kubescheduler flag: --secure-port=11259")
   601  					}
   602  
   603  					// A kube-aws plugin can add flags to the kubelet in the controller
   604  					if !strings.Contains(controllerUserdataS3Part, `--healthz-bind-address=0.0.0.0`) {
   605  						t.Errorf("missing kubelet flag: --healthz-bind-address=0.0.0.0")
   606  					}
   607  
   608  					// A kube-aws plugin can add flags to the kubelet in the workers
   609  					if !strings.Contains(workerUserdataS3Part, `--healthz-bind-address=0.0.0.0`) {
   610  						t.Errorf("missing kubelet flag: --healthz-bind-address=0.0.0.0")
   611  					}
   612  
   613  					// A kube-aws plugin can add flags to the kubeproxy
   614  					if !strings.Contains(controllerUserdataS3Part, `metricsBindAddress: 0.0.0.0`) {
   615  						t.Errorf("missing kubeproxy config item: metricsBindAddress: 0.0.0.0")
   616  					}
   617  				},
   618  			},
   619  		},
   620  	}
   621  
   622  	for _, validCase := range validCases {
   623  		t.Run(validCase.context, func(t *testing.T) {
   624  			helper.WithPlugins(t, validCase.plugins, func() {
   625  				plugins, err := plugin.LoadAll()
   626  				if err != nil {
   627  					t.Errorf("failed to load plugins: %v", err)
   628  					t.FailNow()
   629  				}
   630  				if len(plugins) != len(validCase.plugins) {
   631  					t.Errorf("failed to load plugins: expected %d plugins but loaded %d plugins", len(validCase.plugins), len(plugins))
   632  					t.FailNow()
   633  				}
   634  
   635  				configBytes := validCase.clusterYaml
   636  				providedConfig, err := config.ConfigFromBytes([]byte(configBytes), plugins)
   637  				if err != nil {
   638  					t.Errorf("failed to parse config %s: %+v", configBytes, err)
   639  					t.FailNow()
   640  				}
   641  
   642  				t.Run("AssertConfig", func(t *testing.T) {
   643  					for _, assertion := range validCase.assertConfig {
   644  						assertion(providedConfig, t)
   645  					}
   646  				})
   647  
   648  				helper.WithDummyCredentials(func(dummyAssetsDir string) {
   649  					var stackTemplateOptions = root.NewOptions(false, false)
   650  					stackTemplateOptions.AssetsDir = dummyAssetsDir
   651  					stackTemplateOptions.ControllerTmplFile = "../../builtin/files/userdata/cloud-config-controller"
   652  					stackTemplateOptions.WorkerTmplFile = "../../builtin/files/userdata/cloud-config-worker"
   653  					stackTemplateOptions.EtcdTmplFile = "../../builtin/files/userdata/cloud-config-etcd"
   654  					stackTemplateOptions.RootStackTemplateTmplFile = "../../builtin/files/stack-templates/root.json.tmpl"
   655  					stackTemplateOptions.NodePoolStackTemplateTmplFile = "../../builtin/files/stack-templates/node-pool.json.tmpl"
   656  					stackTemplateOptions.ControlPlaneStackTemplateTmplFile = "../../builtin/files/stack-templates/control-plane.json.tmpl"
   657  					stackTemplateOptions.EtcdStackTemplateTmplFile = "../../builtin/files/stack-templates/etcd.json.tmpl"
   658  					stackTemplateOptions.NetworkStackTemplateTmplFile = "../../builtin/files/stack-templates/network.json.tmpl"
   659  
   660  					cl, err := root.CompileClusterFromConfig(providedConfig, stackTemplateOptions, false)
   661  					if err != nil {
   662  						t.Errorf("failed to create cluster driver : %v", err)
   663  						t.FailNow()
   664  					}
   665  					cl.Context = &model.Context{
   666  						ProvidedEncryptService:  helper.DummyEncryptService{},
   667  						ProvidedCFInterrogator:  helper.DummyCFInterrogator{},
   668  						ProvidedEC2Interrogator: helper.DummyEC2Interrogator{},
   669  						StackTemplateGetter:     helper.DummyStackTemplateGetter{},
   670  					}
   671  
   672  					_, err = cl.EnsureAllAssetsGenerated()
   673  					if err != nil {
   674  						t.Errorf("%v", err)
   675  						t.FailNow()
   676  					}
   677  
   678  					t.Run("AssertCluster", func(t *testing.T) {
   679  						for _, assertion := range validCase.assertCluster {
   680  							assertion(cl, t)
   681  						}
   682  					})
   683  
   684  					t.Run("ValidateTemplates", func(t *testing.T) {
   685  						if err := cl.ValidateTemplates(); err != nil {
   686  							t.Errorf("failed to render stack template: %v", err)
   687  						}
   688  					})
   689  
   690  					if os.Getenv("KUBE_AWS_INTEGRATION_TEST") == "" {
   691  						t.Skipf("`export KUBE_AWS_INTEGRATION_TEST=1` is required to run integration tests. Skipping.")
   692  						t.SkipNow()
   693  					} else {
   694  						t.Run("ValidateStack", func(t *testing.T) {
   695  							if !s3URIExists {
   696  								t.Errorf("failed to obtain value for KUBE_AWS_S3_DIR_URI")
   697  								t.FailNow()
   698  							}
   699  
   700  							report, err := cl.ValidateStack()
   701  
   702  							if err != nil {
   703  								t.Errorf("failed to validate stack: %s %v", report, err)
   704  							}
   705  						})
   706  					}
   707  				})
   708  			})
   709  		})
   710  	}
   711  }