github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/specs/v2_test.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package specs_test
     5  
     6  import (
     7  	"encoding/base64"
     8  
     9  	jc "github.com/juju/testing/checkers"
    10  	gc "gopkg.in/check.v1"
    11  	admissionregistration "k8s.io/api/admissionregistration/v1beta1"
    12  	core "k8s.io/api/core/v1"
    13  	networkingv1beta1 "k8s.io/api/networking/v1beta1"
    14  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    17  	"k8s.io/apimachinery/pkg/util/intstr"
    18  	"k8s.io/utils/pointer"
    19  
    20  	k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs"
    21  	"github.com/juju/juju/caas/specs"
    22  	"github.com/juju/juju/testing"
    23  )
    24  
    25  type v2SpecsSuite struct {
    26  	testing.BaseSuite
    27  }
    28  
    29  var _ = gc.Suite(&v2SpecsSuite{})
    30  
    31  var version2Header = `
    32  version: 2
    33  `[1:]
    34  
    35  func (s *v2SpecsSuite) TestParse(c *gc.C) {
    36  
    37  	specStrBase := version2Header + `
    38  containers:
    39    - name: gitlab
    40      image: gitlab/latest
    41      imagePullPolicy: Always
    42      command:
    43        - sh
    44        - -c
    45        - |
    46          set -ex
    47          echo "do some stuff here for gitlab container"
    48      args: ["doIt", "--debug"]
    49      workingDir: "/path/to/here"
    50      ports:
    51        - containerPort: 80
    52          name: fred
    53          protocol: TCP
    54        - containerPort: 443
    55          name: mary
    56      kubernetes:
    57        securityContext:
    58          runAsNonRoot: true
    59          privileged: true
    60        livenessProbe:
    61          initialDelaySeconds: 10
    62          httpGet:
    63            path: /ping
    64            port: 8080
    65        readinessProbe:
    66          initialDelaySeconds: 10
    67          httpGet:
    68            path: /pingReady
    69            port: www
    70        startupProbe:
    71          httpGet:
    72            path: /healthz
    73            port: liveness-port
    74          failureThreshold: 30
    75          periodSeconds: 10
    76      config:
    77        attr: foo=bar; name["fred"]="blogs";
    78        foo: bar
    79        brackets: '["hello", "world"]'
    80        restricted: 'yes'
    81        switch: on
    82        special: p@ssword's
    83        my-resource-limit:
    84          resource:
    85            container-name: container1
    86            resource: requests.cpu
    87            divisor: 1m
    88      files:
    89        - name: configuration
    90          mountPath: /var/lib/foo
    91          files:
    92            file1: |
    93              [config]
    94              foo: bar
    95            file: |
    96              [config]
    97              foo: bar
    98    - name: gitlab-helper
    99      image: gitlab-helper/latest
   100      ports:
   101      - containerPort: 8080
   102        protocol: TCP
   103    - name: secret-image-user
   104      imageDetails:
   105          imagePath: staging.registry.org/testing/testing-image@sha256:deed-beef
   106          username: docker-registry
   107          password: hunter2
   108    - name: just-image-details
   109      imageDetails:
   110          imagePath: testing/no-secrets-needed@sha256:deed-beef
   111    - name: gitlab-init
   112      image: gitlab-init/latest
   113      imagePullPolicy: Always
   114      init: true
   115      command:
   116        - sh
   117        - -c
   118        - |
   119          set -ex
   120          echo "do some stuff here for gitlab-init container"
   121      args: ["doIt", "--debug"]
   122      workingDir: "/path/to/here"
   123      ports:
   124      - containerPort: 80
   125        name: fred
   126        protocol: TCP
   127      - containerPort: 443
   128        name: mary
   129      config:
   130        brackets: '["hello", "world"]'
   131        foo: bar
   132        restricted: 'yes'
   133        switch: on
   134        special: p@ssword's
   135  configMaps:
   136    mydata:
   137      foo: bar
   138      hello: world
   139  service:
   140    annotations:
   141      foo: bar
   142    scalePolicy: serial
   143    updateStrategy:
   144      type: Recreate
   145      rollingUpdate:
   146        maxUnavailable: 10%
   147        maxSurge: 25%
   148  serviceAccount:
   149    automountServiceAccountToken: true
   150    global: true
   151    rules:
   152    - apiGroups: [""]
   153      resources: ["pods"]
   154      verbs: ["get", "watch", "list"]
   155  kubernetesResources:
   156    serviceAccounts:
   157    - name: k8sServiceAccount1
   158      automountServiceAccountToken: true
   159      global: true
   160      rules:
   161      - apiGroups: [""]
   162        resources: ["pods"]
   163        verbs: ["get", "watch", "list"]
   164      - nonResourceURLs: ["/healthz", "/healthz/*"] # '*' in a nonResourceURL is a suffix glob match
   165        verbs: ["get", "post"]
   166      - apiGroups: ["rbac.authorization.k8s.io"]
   167        resources: ["clusterroles"]
   168        verbs: ["bind"]
   169        resourceNames: ["admin","edit","view"]
   170    pod:
   171      annotations:
   172        foo: baz
   173      labels:
   174        foo: bax
   175      restartPolicy: OnFailure
   176      activeDeadlineSeconds: 10
   177      terminationGracePeriodSeconds: 20
   178      securityContext:
   179        runAsNonRoot: true
   180        supplementalGroups: [1,2]
   181      readinessGates:
   182        - conditionType: PodScheduled
   183      dnsPolicy: ClusterFirstWithHostNet
   184      hostNetwork: true
   185      hostPID: true
   186      priorityClassName: system-cluster-critical
   187      priority: 2000000000
   188    secrets:
   189      - name: build-robot-secret
   190        type: Opaque
   191        stringData:
   192            config.yaml: |-
   193                apiUrl: "https://my.api.com/api/v1"
   194                username: fred
   195                password: shhhh
   196      - name: another-build-robot-secret
   197        type: Opaque
   198        data:
   199            username: YWRtaW4=
   200            password: MWYyZDFlMmU2N2Rm
   201    customResourceDefinitions:
   202      tfjobs.kubeflow.org:
   203        group: kubeflow.org
   204        scope: Cluster
   205        names:
   206          kind: TFJob
   207          singular: tfjob
   208          plural: tfjobs
   209        version: v1
   210        versions:
   211        - name: v1
   212          served: true
   213          storage: true
   214        - name: v1beta2
   215          served: true
   216          storage: false
   217        conversion:
   218          strategy: None
   219        preserveUnknownFields: false
   220        additionalPrinterColumns:
   221        - name: Worker
   222          type: integer
   223          description: Worker attribute.
   224          jsonPath: .spec.tfReplicaSpecs.Worker
   225        validation:
   226          openAPIV3Schema:
   227            properties:
   228              spec:
   229                properties:
   230                  tfReplicaSpecs:
   231                    properties:
   232                      Worker:
   233                        properties:
   234                          replicas:
   235                            type: integer
   236                            minimum: 1
   237                      PS:
   238                        properties:
   239                          replicas:
   240                            type: integer
   241                            minimum: 1
   242                      Chief:
   243                        properties:
   244                          replicas:
   245                            type: integer
   246                            minimum: 1
   247                            maximum: 1
   248    customResources:
   249      tfjobs.kubeflow.org:
   250        - apiVersion: "kubeflow.org/v1"
   251          kind: "TFJob"
   252          metadata:
   253            name: "dist-mnist-for-e2e-test"
   254          spec:
   255            tfReplicaSpecs:
   256              PS:
   257                replicas: 2
   258                restartPolicy: Never
   259                template:
   260                  spec:
   261                    containers:
   262                      - name: tensorflow
   263                        image: kubeflow/tf-dist-mnist-test:1.0
   264              Worker:
   265                replicas: 4
   266                restartPolicy: Never
   267                template:
   268                  spec:
   269                    containers:
   270                      - name: tensorflow
   271                        image: kubeflow/tf-dist-mnist-test:1.0
   272    ingressResources:
   273      - name: test-ingress
   274        labels:
   275          foo: bar
   276        annotations:
   277          nginx.ingress.kubernetes.io/rewrite-target: /
   278        spec:
   279          rules:
   280          - http:
   281              paths:
   282              - path: /testpath
   283                backend:
   284                  serviceName: test
   285                  servicePort: 80
   286    mutatingWebhookConfigurations:
   287      example-mutatingwebhookconfiguration:
   288        - name: "example.mutatingwebhookconfiguration.com"
   289          failurePolicy: Ignore
   290          clientConfig:
   291            service:
   292              name: apple-service
   293              namespace: apples
   294              path: /apple
   295            caBundle: "YXBwbGVz"
   296          namespaceSelector:
   297            matchExpressions:
   298            - key: production
   299              operator: DoesNotExist
   300          rules:
   301          - apiGroups:
   302            - ""
   303            apiVersions:
   304            - v1
   305            operations:
   306            - CREATE
   307            - UPDATE
   308            resources:
   309            - pods
   310    validatingWebhookConfigurations:
   311      pod-policy.example.com:
   312        - name: "pod-policy.example.com"
   313          rules:
   314          - apiGroups:   [""]
   315            apiVersions: ["v1"]
   316            operations:  ["CREATE"]
   317            resources:   ["pods"]
   318            scope:       "Namespaced"
   319          clientConfig:
   320            service:
   321              namespace: "example-namespace"
   322              name: "example-service"
   323            caBundle: "YXBwbGVz"
   324          admissionReviewVersions: ["v1", "v1beta1"]
   325          sideEffects: None
   326          timeoutSeconds: 5
   327  `[1:]
   328  
   329  	expectedFileContent := `
   330  [config]
   331  foo: bar
   332  `[1:]
   333  
   334  	sa1 := &specs.PrimeServiceAccountSpecV3{
   335  		ServiceAccountSpecV3: specs.ServiceAccountSpecV3{
   336  			AutomountServiceAccountToken: pointer.BoolPtr(true),
   337  			Roles: []specs.Role{
   338  				{
   339  					Global: true,
   340  					Rules: []specs.PolicyRule{
   341  						{
   342  							APIGroups: []string{""},
   343  							Resources: []string{"pods"},
   344  							Verbs:     []string{"get", "watch", "list"},
   345  						},
   346  					},
   347  				},
   348  			},
   349  		},
   350  	}
   351  
   352  	getExpectedPodSpecBase := func() *specs.PodSpec {
   353  		pSpecs := &specs.PodSpec{ServiceAccount: sa1}
   354  		pSpecs.Service = &specs.ServiceSpec{
   355  			Annotations: map[string]string{"foo": "bar"},
   356  			ScalePolicy: "serial",
   357  			UpdateStrategy: &specs.UpdateStrategy{
   358  				Type: "Recreate",
   359  				RollingUpdate: &specs.RollingUpdateSpec{
   360  					MaxUnavailable: &specs.IntOrString{Type: specs.String, StrVal: "10%"},
   361  					MaxSurge:       &specs.IntOrString{Type: specs.String, StrVal: "25%"},
   362  				},
   363  			},
   364  		}
   365  		pSpecs.ConfigMaps = map[string]specs.ConfigMap{
   366  			"mydata": {
   367  				"foo":   "bar",
   368  				"hello": "world",
   369  			},
   370  		}
   371  		// always parse to latest version.
   372  		pSpecs.Version = specs.CurrentVersion
   373  
   374  		pSpecs.Containers = []specs.ContainerSpec{
   375  			{
   376  				Name:            "gitlab",
   377  				Image:           "gitlab/latest",
   378  				ImagePullPolicy: "Always",
   379  				Command: []string{"sh", "-c", `
   380  set -ex
   381  echo "do some stuff here for gitlab container"
   382  `[1:]},
   383  				Args:       []string{"doIt", "--debug"},
   384  				WorkingDir: "/path/to/here",
   385  				Ports: []specs.ContainerPort{
   386  					{ContainerPort: 80, Protocol: "TCP", Name: "fred"},
   387  					{ContainerPort: 443, Name: "mary"},
   388  				},
   389  				EnvConfig: map[string]interface{}{
   390  					"attr":       `foo=bar; name["fred"]="blogs";`,
   391  					"foo":        "bar",
   392  					"restricted": "yes",
   393  					"switch":     true,
   394  					"brackets":   `["hello", "world"]`,
   395  					"special":    "p@ssword's",
   396  					"my-resource-limit": map[string]interface{}{
   397  						"resource": map[string]interface{}{
   398  							"container-name": "container1",
   399  							"resource":       "requests.cpu",
   400  							"divisor":        "1m",
   401  						},
   402  					},
   403  				},
   404  				VolumeConfig: []specs.FileSet{
   405  					{
   406  						Name:      "configuration",
   407  						MountPath: "/var/lib/foo",
   408  						VolumeSource: specs.VolumeSource{
   409  							Files: []specs.File{
   410  								{Path: "file", Content: expectedFileContent},
   411  								{Path: "file1", Content: expectedFileContent},
   412  							},
   413  						},
   414  					},
   415  				},
   416  				ProviderContainer: &k8sspecs.K8sContainerSpec{
   417  					SecurityContext: &core.SecurityContext{
   418  						RunAsNonRoot: pointer.BoolPtr(true),
   419  						Privileged:   pointer.BoolPtr(true),
   420  					},
   421  					LivenessProbe: &core.Probe{
   422  						InitialDelaySeconds: 10,
   423  						ProbeHandler: core.ProbeHandler{
   424  							HTTPGet: &core.HTTPGetAction{
   425  								Path: "/ping",
   426  								Port: intstr.IntOrString{IntVal: 8080},
   427  							},
   428  						},
   429  					},
   430  					ReadinessProbe: &core.Probe{
   431  						InitialDelaySeconds: 10,
   432  						ProbeHandler: core.ProbeHandler{
   433  							HTTPGet: &core.HTTPGetAction{
   434  								Path: "/pingReady",
   435  								Port: intstr.IntOrString{StrVal: "www", Type: 1},
   436  							},
   437  						},
   438  					},
   439  					StartupProbe: &core.Probe{
   440  						PeriodSeconds:    10,
   441  						FailureThreshold: 30,
   442  						ProbeHandler: core.ProbeHandler{
   443  							HTTPGet: &core.HTTPGetAction{
   444  								Path: "/healthz",
   445  								Port: intstr.IntOrString{
   446  									Type:   intstr.String,
   447  									StrVal: "liveness-port",
   448  								},
   449  							},
   450  						},
   451  					},
   452  				},
   453  			}, {
   454  				Name:  "gitlab-helper",
   455  				Image: "gitlab-helper/latest",
   456  				Ports: []specs.ContainerPort{
   457  					{ContainerPort: 8080, Protocol: "TCP"},
   458  				},
   459  			}, {
   460  				Name: "secret-image-user",
   461  				ImageDetails: specs.ImageDetails{
   462  					ImagePath: "staging.registry.org/testing/testing-image@sha256:deed-beef",
   463  					Username:  "docker-registry",
   464  					Password:  "hunter2",
   465  				},
   466  			}, {
   467  				Name: "just-image-details",
   468  				ImageDetails: specs.ImageDetails{
   469  					ImagePath: "testing/no-secrets-needed@sha256:deed-beef",
   470  				},
   471  			},
   472  			{
   473  				Name:            "gitlab-init",
   474  				Image:           "gitlab-init/latest",
   475  				ImagePullPolicy: "Always",
   476  				Init:            true,
   477  				Command: []string{"sh", "-c", `
   478  set -ex
   479  echo "do some stuff here for gitlab-init container"
   480  `[1:]},
   481  				Args:       []string{"doIt", "--debug"},
   482  				WorkingDir: "/path/to/here",
   483  				Ports: []specs.ContainerPort{
   484  					{ContainerPort: 80, Protocol: "TCP", Name: "fred"},
   485  					{ContainerPort: 443, Name: "mary"},
   486  				},
   487  				EnvConfig: map[string]interface{}{
   488  					"foo":        "bar",
   489  					"restricted": "yes",
   490  					"switch":     true,
   491  					"brackets":   `["hello", "world"]`,
   492  					"special":    "p@ssword's",
   493  				},
   494  			},
   495  		}
   496  
   497  		rbacResources := k8sspecs.K8sRBACResources{
   498  			ServiceAccounts: []k8sspecs.K8sServiceAccountSpec{
   499  				{
   500  					Name: "k8sServiceAccount1",
   501  					ServiceAccountSpecV3: specs.ServiceAccountSpecV3{
   502  						AutomountServiceAccountToken: pointer.BoolPtr(true),
   503  						Roles: []specs.Role{
   504  							{
   505  								Global: true,
   506  								Rules: []specs.PolicyRule{
   507  									{
   508  										APIGroups: []string{""},
   509  										Resources: []string{"pods"},
   510  										Verbs:     []string{"get", "watch", "list"},
   511  									},
   512  									{
   513  										NonResourceURLs: []string{"/healthz", "/healthz/*"},
   514  										Verbs:           []string{"get", "post"},
   515  									},
   516  									{
   517  										APIGroups:     []string{"rbac.authorization.k8s.io"},
   518  										Resources:     []string{"clusterroles"},
   519  										Verbs:         []string{"bind"},
   520  										ResourceNames: []string{"admin", "edit", "view"},
   521  									},
   522  								},
   523  							},
   524  						},
   525  					},
   526  				},
   527  			},
   528  		}
   529  
   530  		ingress1Rule1 := networkingv1beta1.IngressRule{
   531  			IngressRuleValue: networkingv1beta1.IngressRuleValue{
   532  				HTTP: &networkingv1beta1.HTTPIngressRuleValue{
   533  					Paths: []networkingv1beta1.HTTPIngressPath{
   534  						{
   535  							Path: "/testpath",
   536  							Backend: networkingv1beta1.IngressBackend{
   537  								ServiceName: "test",
   538  								ServicePort: intstr.IntOrString{IntVal: 80},
   539  							},
   540  						},
   541  					},
   542  				},
   543  			},
   544  		}
   545  		ingress1 := k8sspecs.K8sIngress{
   546  			Meta: k8sspecs.Meta{
   547  				Name: "test-ingress",
   548  				Labels: map[string]string{
   549  					"foo": "bar",
   550  				},
   551  				Annotations: map[string]string{
   552  					"nginx.ingress.kubernetes.io/rewrite-target": "/",
   553  				},
   554  			},
   555  			Spec: k8sspecs.K8sIngressSpec{
   556  				Version: k8sspecs.K8sIngressV1Beta1,
   557  				SpecV1Beta1: networkingv1beta1.IngressSpec{
   558  					Rules: []networkingv1beta1.IngressRule{ingress1Rule1},
   559  				},
   560  			},
   561  		}
   562  
   563  		webhookRule1 := admissionregistration.Rule{
   564  			APIGroups:   []string{""},
   565  			APIVersions: []string{"v1"},
   566  			Resources:   []string{"pods"},
   567  		}
   568  		webhookRuleWithOperations1 := admissionregistration.RuleWithOperations{
   569  			Operations: []admissionregistration.OperationType{
   570  				admissionregistration.Create,
   571  				admissionregistration.Update,
   572  			},
   573  		}
   574  		webhookRuleWithOperations1.Rule = webhookRule1
   575  		CABundle1, err := base64.StdEncoding.DecodeString("YXBwbGVz")
   576  		c.Assert(err, jc.ErrorIsNil)
   577  		webhookFailurePolicy1 := admissionregistration.Ignore
   578  		webhook1 := admissionregistration.MutatingWebhook{
   579  			Name:          "example.mutatingwebhookconfiguration.com",
   580  			FailurePolicy: &webhookFailurePolicy1,
   581  			ClientConfig: admissionregistration.WebhookClientConfig{
   582  				Service: &admissionregistration.ServiceReference{
   583  					Name:      "apple-service",
   584  					Namespace: "apples",
   585  					Path:      pointer.StringPtr("/apple"),
   586  				},
   587  				CABundle: CABundle1,
   588  			},
   589  			NamespaceSelector: &metav1.LabelSelector{
   590  				MatchExpressions: []metav1.LabelSelectorRequirement{
   591  					{Key: "production", Operator: metav1.LabelSelectorOpDoesNotExist},
   592  				},
   593  			},
   594  			Rules: []admissionregistration.RuleWithOperations{webhookRuleWithOperations1},
   595  		}
   596  
   597  		scope := admissionregistration.NamespacedScope
   598  		webhookRule2 := admissionregistration.Rule{
   599  			APIGroups:   []string{""},
   600  			APIVersions: []string{"v1"},
   601  			Resources:   []string{"pods"},
   602  			Scope:       &scope,
   603  		}
   604  		webhookRuleWithOperations2 := admissionregistration.RuleWithOperations{
   605  			Operations: []admissionregistration.OperationType{
   606  				admissionregistration.Create,
   607  			},
   608  		}
   609  		webhookRuleWithOperations2.Rule = webhookRule2
   610  		sideEffects := admissionregistration.SideEffectClassNone
   611  		webhook2 := admissionregistration.ValidatingWebhook{
   612  			Name:  "pod-policy.example.com",
   613  			Rules: []admissionregistration.RuleWithOperations{webhookRuleWithOperations2},
   614  			ClientConfig: admissionregistration.WebhookClientConfig{
   615  				Service: &admissionregistration.ServiceReference{
   616  					Name:      "example-service",
   617  					Namespace: "example-namespace",
   618  				},
   619  				CABundle: CABundle1,
   620  			},
   621  			AdmissionReviewVersions: []string{"v1", "v1beta1"},
   622  			SideEffects:             &sideEffects,
   623  			TimeoutSeconds:          pointer.Int32Ptr(5),
   624  		}
   625  
   626  		pSpecs.ProviderPod = &k8sspecs.K8sPodSpec{
   627  			KubernetesResources: &k8sspecs.KubernetesResources{
   628  				K8sRBACResources: rbacResources,
   629  				Pod: &k8sspecs.PodSpec{
   630  					Labels:                        map[string]string{"foo": "bax"},
   631  					Annotations:                   map[string]string{"foo": "baz"},
   632  					ActiveDeadlineSeconds:         pointer.Int64Ptr(10),
   633  					RestartPolicy:                 core.RestartPolicyOnFailure,
   634  					TerminationGracePeriodSeconds: pointer.Int64Ptr(20),
   635  					SecurityContext: &core.PodSecurityContext{
   636  						RunAsNonRoot:       pointer.BoolPtr(true),
   637  						SupplementalGroups: []int64{1, 2},
   638  					},
   639  					ReadinessGates: []core.PodReadinessGate{
   640  						{ConditionType: core.PodScheduled},
   641  					},
   642  					DNSPolicy:         "ClusterFirstWithHostNet",
   643  					HostNetwork:       true,
   644  					HostPID:           true,
   645  					PriorityClassName: "system-cluster-critical",
   646  					Priority:          pointer.Int32Ptr(2000000000),
   647  				},
   648  				Secrets: []k8sspecs.K8sSecret{
   649  					{
   650  						Name: "build-robot-secret",
   651  						Type: core.SecretTypeOpaque,
   652  						StringData: map[string]string{
   653  							"config.yaml": `
   654  apiUrl: "https://my.api.com/api/v1"
   655  username: fred
   656  password: shhhh`[1:],
   657  						},
   658  					},
   659  					{
   660  						Name: "another-build-robot-secret",
   661  						Type: core.SecretTypeOpaque,
   662  						Data: map[string]string{
   663  							"username": "YWRtaW4=",
   664  							"password": "MWYyZDFlMmU2N2Rm",
   665  						},
   666  					},
   667  				},
   668  				CustomResourceDefinitions: []k8sspecs.K8sCustomResourceDefinition{
   669  					{
   670  						Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"},
   671  						Spec: k8sspecs.K8sCustomResourceDefinitionSpec{
   672  							Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1,
   673  							SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   674  								Group:   "kubeflow.org",
   675  								Version: "v1",
   676  								Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
   677  									{Name: "v1", Served: true, Storage: true},
   678  									{Name: "v1beta2", Served: true, Storage: false},
   679  								},
   680  								Scope:                 "Cluster",
   681  								PreserveUnknownFields: pointer.BoolPtr(false),
   682  								Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   683  									Kind:     "TFJob",
   684  									Plural:   "tfjobs",
   685  									Singular: "tfjob",
   686  								},
   687  								Conversion: &apiextensionsv1beta1.CustomResourceConversion{
   688  									Strategy: apiextensionsv1beta1.NoneConverter,
   689  								},
   690  								AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{
   691  									{
   692  										Name:        "Worker",
   693  										Type:        "integer",
   694  										Description: "Worker attribute.",
   695  										JSONPath:    ".spec.tfReplicaSpecs.Worker",
   696  									},
   697  								},
   698  								Validation: &apiextensionsv1beta1.CustomResourceValidation{
   699  									OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   700  										Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   701  											"spec": {
   702  												Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   703  													"tfReplicaSpecs": {
   704  														Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   705  															"PS": {
   706  																Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   707  																	"replicas": {
   708  																		Type: "integer", Minimum: pointer.Float64Ptr(1),
   709  																	},
   710  																},
   711  															},
   712  															"Chief": {
   713  																Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   714  																	"replicas": {
   715  																		Type:    "integer",
   716  																		Minimum: pointer.Float64Ptr(1),
   717  																		Maximum: pointer.Float64Ptr(1),
   718  																	},
   719  																},
   720  															},
   721  															"Worker": {
   722  																Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   723  																	"replicas": {
   724  																		Type:    "integer",
   725  																		Minimum: pointer.Float64Ptr(1),
   726  																	},
   727  																},
   728  															},
   729  														},
   730  													},
   731  												},
   732  											},
   733  										},
   734  									},
   735  								},
   736  							},
   737  						},
   738  					},
   739  				},
   740  				CustomResources: map[string][]unstructured.Unstructured{
   741  					"tfjobs.kubeflow.org": {
   742  						{
   743  							Object: map[string]interface{}{
   744  								"apiVersion": "kubeflow.org/v1",
   745  								"metadata": map[string]interface{}{
   746  									"name": "dist-mnist-for-e2e-test",
   747  								},
   748  								"kind": "TFJob",
   749  								"spec": map[string]interface{}{
   750  									"tfReplicaSpecs": map[string]interface{}{
   751  										"PS": map[string]interface{}{
   752  											"replicas":      int64(2),
   753  											"restartPolicy": "Never",
   754  											"template": map[string]interface{}{
   755  												"spec": map[string]interface{}{
   756  													"containers": []interface{}{
   757  														map[string]interface{}{
   758  															"name":  "tensorflow",
   759  															"image": "kubeflow/tf-dist-mnist-test:1.0",
   760  														},
   761  													},
   762  												},
   763  											},
   764  										},
   765  										"Worker": map[string]interface{}{
   766  											"replicas":      int64(4),
   767  											"restartPolicy": "Never",
   768  											"template": map[string]interface{}{
   769  												"spec": map[string]interface{}{
   770  													"containers": []interface{}{
   771  														map[string]interface{}{
   772  															"name":  "tensorflow",
   773  															"image": "kubeflow/tf-dist-mnist-test:1.0",
   774  														},
   775  													},
   776  												},
   777  											},
   778  										},
   779  									},
   780  								},
   781  							},
   782  						},
   783  					},
   784  				},
   785  				IngressResources: []k8sspecs.K8sIngress{ingress1},
   786  				MutatingWebhookConfigurations: []k8sspecs.K8sMutatingWebhook{
   787  					{
   788  						Meta: k8sspecs.Meta{Name: "example-mutatingwebhookconfiguration"},
   789  						Webhooks: []k8sspecs.K8sMutatingWebhookSpec{
   790  							{
   791  								Version:     k8sspecs.K8sWebhookV1Beta1,
   792  								SpecV1Beta1: webhook1,
   793  							},
   794  						},
   795  					},
   796  				},
   797  				ValidatingWebhookConfigurations: []k8sspecs.K8sValidatingWebhook{
   798  					{
   799  						Meta: k8sspecs.Meta{Name: "pod-policy.example.com"},
   800  						Webhooks: []k8sspecs.K8sValidatingWebhookSpec{
   801  							{
   802  								Version:     k8sspecs.K8sWebhookV1Beta1,
   803  								SpecV1Beta1: webhook2,
   804  							},
   805  						},
   806  					},
   807  				},
   808  			},
   809  		}
   810  		return pSpecs
   811  	}
   812  
   813  	spec, err := k8sspecs.ParsePodSpec(specStrBase)
   814  	c.Assert(err, jc.ErrorIsNil)
   815  	c.Assert(spec, jc.DeepEquals, getExpectedPodSpecBase())
   816  }
   817  
   818  func (s *v2SpecsSuite) TestValidateMissingContainers(c *gc.C) {
   819  
   820  	specStr := version2Header + `
   821  containers:
   822  `[1:]
   823  
   824  	_, err := k8sspecs.ParsePodSpec(specStr)
   825  	c.Assert(err, gc.ErrorMatches, "require at least one container spec")
   826  }
   827  
   828  func (s *v2SpecsSuite) TestValidateMissingName(c *gc.C) {
   829  
   830  	specStr := version2Header + `
   831  containers:
   832    - image: gitlab/latest
   833  `[1:]
   834  
   835  	_, err := k8sspecs.ParsePodSpec(specStr)
   836  	c.Assert(err, gc.ErrorMatches, "spec name is missing")
   837  }
   838  
   839  func (s *v2SpecsSuite) TestValidateMissingImage(c *gc.C) {
   840  
   841  	specStr := version2Header + `
   842  containers:
   843    - name: gitlab
   844  `[1:]
   845  
   846  	_, err := k8sspecs.ParsePodSpec(specStr)
   847  	c.Assert(err, gc.ErrorMatches, "spec image details is missing")
   848  }
   849  
   850  func (s *v2SpecsSuite) TestValidateFileSetPath(c *gc.C) {
   851  
   852  	specStr := version2Header + `
   853  containers:
   854    - name: gitlab
   855      image: gitlab/latest
   856      files:
   857        - files:
   858            file1: |-
   859              [config]
   860              foo: bar
   861  `[1:]
   862  
   863  	_, err := k8sspecs.ParsePodSpec(specStr)
   864  	c.Assert(err, gc.ErrorMatches, `file set name is missing`)
   865  }
   866  
   867  func (s *v2SpecsSuite) TestValidateMissingMountPath(c *gc.C) {
   868  
   869  	specStr := version2Header + `
   870  containers:
   871    - name: gitlab
   872      image: gitlab/latest
   873      files:
   874        - name: configuration
   875          files:
   876            file1: |-
   877              [config]
   878              foo: bar
   879  `[1:]
   880  
   881  	_, err := k8sspecs.ParsePodSpec(specStr)
   882  	c.Assert(err, gc.ErrorMatches, `mount path is missing for file set "configuration"`)
   883  }
   884  
   885  func (s *v2SpecsSuite) TestValidateServiceAccountShouldBeOmittedForEmptyValue(c *gc.C) {
   886  	specStr := version2Header + `
   887  containers:
   888    - name: gitlab-helper
   889      image: gitlab-helper/latest
   890      ports:
   891      - containerPort: 8080
   892        protocol: TCP
   893  serviceAccount:
   894    automountServiceAccountToken: true
   895  `[1:]
   896  
   897  	_, err := k8sspecs.ParsePodSpec(specStr)
   898  	c.Assert(err, gc.ErrorMatches, `invalid primary service account: rules is required`)
   899  }
   900  
   901  func (s *v2SpecsSuite) TestValidateCustomResourceDefinitions(c *gc.C) {
   902  	specStr := version2Header + `
   903  containers:
   904    - name: gitlab-helper
   905      image: gitlab-helper/latest
   906      ports:
   907      - containerPort: 8080
   908        protocol: TCP
   909  kubernetesResources:
   910    customResourceDefinitions:
   911      tfjobs.kubeflow.org:
   912        group: kubeflow.org
   913        version: v1alpha2
   914        scope: invalid-scope
   915        names:
   916          plural: "tfjobs"
   917          singular: "tfjob"
   918          kind: TFJob
   919        validation:
   920          openAPIV3Schema:
   921            properties:
   922              tfReplicaSpecs:
   923                properties:
   924                  Worker:
   925                    properties:
   926                      replicas:
   927                        type: integer
   928                        minimum: 1
   929                  PS:
   930                    properties:
   931                      replicas:
   932                        type: integer
   933                        minimum: 1
   934                  Chief:
   935                    properties:
   936                      replicas:
   937                        type: integer
   938                        minimum: 1
   939                        maximum: 1
   940  `[1:]
   941  
   942  	_, err := k8sspecs.ParsePodSpec(specStr)
   943  	c.Assert(err, gc.ErrorMatches, `custom resource definition "tfjobs.kubeflow.org" scope "invalid-scope" is not supported, please use "Namespaced" or "Cluster" scope`)
   944  }
   945  
   946  func (s *v2SpecsSuite) TestValidateMutatingWebhookConfigurations(c *gc.C) {
   947  	specStr := version2Header + `
   948  containers:
   949    - name: gitlab-helper
   950      image: gitlab-helper/latest
   951      ports:
   952      - containerPort: 8080
   953        protocol: TCP
   954  kubernetesResources:
   955    mutatingWebhookConfigurations:
   956      example-mutatingwebhookconfiguration:
   957  `[1:]
   958  
   959  	_, err := k8sspecs.ParsePodSpec(specStr)
   960  	c.Assert(err, gc.ErrorMatches, `empty webhooks "example-mutatingwebhookconfiguration" not valid`)
   961  }
   962  
   963  func (s *v2SpecsSuite) TestValidateValidatingWebhookConfigurations(c *gc.C) {
   964  	specStr := version2Header + `
   965  containers:
   966    - name: gitlab-helper
   967      image: gitlab-helper/latest
   968      ports:
   969      - containerPort: 8080
   970        protocol: TCP
   971  kubernetesResources:
   972    validatingWebhookConfigurations:
   973      example-validatingwebhookconfiguration:
   974  `[1:]
   975  
   976  	_, err := k8sspecs.ParsePodSpec(specStr)
   977  	c.Assert(err, gc.ErrorMatches, `empty webhooks "example-validatingwebhookconfiguration" not valid`)
   978  }
   979  
   980  func (s *v2SpecsSuite) TestValidateIngressResources(c *gc.C) {
   981  	specStr := version2Header + `
   982  containers:
   983    - name: gitlab-helper
   984      image: gitlab-helper/latest
   985      ports:
   986      - containerPort: 8080
   987        protocol: TCP
   988  kubernetesResources:
   989    ingressResources:
   990      - labels:
   991          foo: bar
   992        annotations:
   993          nginx.ingress.kubernetes.io/rewrite-target: /
   994        spec:
   995          rules:
   996          - http:
   997              paths:
   998              - path: /testpath
   999                backend:
  1000                  serviceName: test
  1001                  servicePort: 80
  1002  `[1:]
  1003  
  1004  	_, err := k8sspecs.ParsePodSpec(specStr)
  1005  	c.Assert(err, gc.ErrorMatches, `name is missing`)
  1006  
  1007  	specStr = version3Header + `
  1008  containers:
  1009    - name: gitlab-helper
  1010      image: gitlab-helper/latest
  1011      ports:
  1012      - containerPort: 8080
  1013        protocol: TCP
  1014  kubernetesResources:
  1015    ingressResources:
  1016      - name: test-ingress
  1017        labels:
  1018          /foo: bar
  1019        annotations:
  1020          nginx.ingress.kubernetes.io/rewrite-target: /
  1021        spec:
  1022          rules:
  1023          - http:
  1024              paths:
  1025              - path: /testpath
  1026                backend:
  1027                  serviceName: test
  1028                  servicePort: 80
  1029  `[1:]
  1030  
  1031  	_, err = k8sspecs.ParsePodSpec(specStr)
  1032  	c.Assert(err, gc.ErrorMatches, `label key "/foo": prefix part must be non-empty not valid`)
  1033  }
  1034  
  1035  func (s *v2SpecsSuite) TestUnknownFieldError(c *gc.C) {
  1036  	specStr := version2Header + `
  1037  containers:
  1038    - name: gitlab-helper
  1039      image: gitlab-helper/latest
  1040      ports:
  1041      - containerPort: 8080
  1042        protocol: TCP
  1043  bar: a-bad-guy
  1044  `[1:]
  1045  
  1046  	_, err := k8sspecs.ParsePodSpec(specStr)
  1047  	c.Assert(err, gc.ErrorMatches, `json: unknown field "bar"`)
  1048  }