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 }