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

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider_test
     5  
     6  import (
     7  	"context"
     8  	"time"
     9  
    10  	"github.com/juju/errors"
    11  	jc "github.com/juju/testing/checkers"
    12  	"go.uber.org/mock/gomock"
    13  	gc "gopkg.in/check.v1"
    14  	appsv1 "k8s.io/api/apps/v1"
    15  	core "k8s.io/api/core/v1"
    16  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    17  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    18  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    20  	"k8s.io/apimachinery/pkg/runtime/schema"
    21  	k8sversion "k8s.io/apimachinery/pkg/version"
    22  	"k8s.io/utils/pointer"
    23  
    24  	"github.com/juju/juju/caas"
    25  	"github.com/juju/juju/caas/kubernetes/provider"
    26  	"github.com/juju/juju/caas/kubernetes/provider/mocks"
    27  	k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs"
    28  	"github.com/juju/juju/core/config"
    29  	"github.com/juju/juju/core/resources"
    30  	"github.com/juju/juju/core/status"
    31  	"github.com/juju/juju/testing"
    32  )
    33  
    34  func (s *K8sBrokerSuite) assertCustomerResourceDefinitions(c *gc.C, crds []k8sspecs.K8sCustomResourceDefinition, assertCalls ...any) {
    35  
    36  	basicPodSpec := getBasicPodspec()
    37  	basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{
    38  		KubernetesResources: &k8sspecs.KubernetesResources{
    39  			CustomResourceDefinitions: crds,
    40  		},
    41  	}
    42  	workloadSpec, err := provider.PrepareWorkloadSpec(
    43  		"app-name", "app-name", basicPodSpec, resources.DockerImageDetails{RegistryPath: "operator/image-path"},
    44  	)
    45  	c.Assert(err, jc.ErrorIsNil)
    46  	podSpec := provider.Pod(workloadSpec).PodSpec
    47  
    48  	numUnits := int32(2)
    49  	statefulSetArg := &appsv1.StatefulSet{
    50  		ObjectMeta: v1.ObjectMeta{
    51  			Name:   "app-name",
    52  			Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"},
    53  			Annotations: map[string]string{
    54  				"app.juju.is/uuid":               "appuuid",
    55  				"controller.juju.is/id":          testing.ControllerTag.Id(),
    56  				"charm.juju.is/modified-version": "0",
    57  			},
    58  		},
    59  		Spec: appsv1.StatefulSetSpec{
    60  			Replicas: &numUnits,
    61  			Selector: &v1.LabelSelector{
    62  				MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"},
    63  			},
    64  			RevisionHistoryLimit: pointer.Int32Ptr(0),
    65  			Template: core.PodTemplateSpec{
    66  				ObjectMeta: v1.ObjectMeta{
    67  					Labels: map[string]string{"app.kubernetes.io/name": "app-name"},
    68  					Annotations: map[string]string{
    69  						"apparmor.security.beta.kubernetes.io/pod": "runtime/default",
    70  						"seccomp.security.beta.kubernetes.io/pod":  "docker/default",
    71  						"controller.juju.is/id":                    testing.ControllerTag.Id(),
    72  						"charm.juju.is/modified-version":           "0",
    73  					},
    74  				},
    75  				Spec: podSpec,
    76  			},
    77  			PodManagementPolicy: appsv1.ParallelPodManagement,
    78  			ServiceName:         "app-name-endpoints",
    79  		},
    80  	}
    81  
    82  	serviceArg := *basicServiceArg
    83  	serviceArg.Spec.Type = core.ServiceTypeClusterIP
    84  
    85  	assertCalls = append(
    86  		[]any{
    87  			s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}).
    88  				Return(nil, s.k8sNotFoundError()),
    89  		},
    90  		assertCalls...,
    91  	)
    92  
    93  	ociImageSecret := s.getOCIImageSecret(c, nil)
    94  	assertCalls = append(assertCalls, []any{
    95  		s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}).
    96  			Return(ociImageSecret, nil),
    97  		s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}).
    98  			Return(nil, s.k8sNotFoundError()),
    99  		s.mockServices.EXPECT().Update(gomock.Any(), &serviceArg, v1.UpdateOptions{}).
   100  			Return(nil, s.k8sNotFoundError()),
   101  		s.mockServices.EXPECT().Create(gomock.Any(), &serviceArg, v1.CreateOptions{}).
   102  			Return(nil, nil),
   103  		s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}).
   104  			Return(nil, s.k8sNotFoundError()),
   105  		s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}).
   106  			Return(nil, s.k8sNotFoundError()),
   107  		s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}).
   108  			Return(nil, nil),
   109  		s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}).
   110  			Return(statefulSetArg, nil),
   111  		s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}).
   112  			Return(nil, nil),
   113  	}...)
   114  	gomock.InOrder(assertCalls...)
   115  
   116  	params := &caas.ServiceParams{
   117  		PodSpec: basicPodSpec,
   118  		Deployment: caas.DeploymentParams{
   119  			DeploymentType: caas.DeploymentStateful,
   120  		},
   121  		ImageDetails: resources.DockerImageDetails{RegistryPath: "operator/image-path"},
   122  		ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()},
   123  	}
   124  	err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, e string, _ map[string]interface{}) error {
   125  		c.Logf("EnsureService error -> %q", e)
   126  		return nil
   127  	}, params, 2, config.ConfigAttributes{
   128  		"kubernetes-service-loadbalancer-ip": "10.0.0.1",
   129  		"kubernetes-service-externalname":    "ext-name",
   130  	})
   131  	c.Assert(err, jc.ErrorIsNil)
   132  }
   133  
   134  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsCreateV1beta1(c *gc.C) {
   135  	ctrl := s.setupController(c)
   136  	defer ctrl.Finish()
   137  
   138  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
   139  		Major: "1", Minor: "21",
   140  	}, nil)
   141  
   142  	crds := []k8sspecs.K8sCustomResourceDefinition{
   143  		{
   144  			Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"},
   145  			Spec: k8sspecs.K8sCustomResourceDefinitionSpec{
   146  				Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1,
   147  				SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   148  					Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   149  						Kind:     "TFJob",
   150  						Singular: "tfjob",
   151  						Plural:   "tfjobs",
   152  					},
   153  					Version: "v1alpha2",
   154  					Group:   "kubeflow.org",
   155  					Scope:   "Namespaced",
   156  					Validation: &apiextensionsv1beta1.CustomResourceValidation{
   157  						OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   158  							Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   159  								"tfReplicaSpecs": {
   160  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   161  										"Worker": {
   162  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   163  												"replicas": {
   164  													Type:    "integer",
   165  													Minimum: pointer.Float64Ptr(1),
   166  												},
   167  											},
   168  										},
   169  										"PS": {
   170  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   171  												"replicas": {
   172  													Type: "integer", Minimum: pointer.Float64Ptr(1),
   173  												},
   174  											},
   175  										},
   176  										"Chief": {
   177  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   178  												"replicas": {
   179  													Type:    "integer",
   180  													Minimum: pointer.Float64Ptr(1),
   181  													Maximum: pointer.Float64Ptr(1),
   182  												},
   183  											},
   184  										},
   185  									},
   186  								},
   187  							},
   188  						},
   189  					},
   190  				},
   191  			},
   192  		},
   193  	}
   194  
   195  	crd := &apiextensionsv1beta1.CustomResourceDefinition{
   196  		ObjectMeta: v1.ObjectMeta{
   197  			Name:        "tfjobs.kubeflow.org",
   198  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
   199  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
   200  		},
   201  		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   202  			Group:   "kubeflow.org",
   203  			Version: "v1alpha2",
   204  			Scope:   "Namespaced",
   205  			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   206  				Plural:   "tfjobs",
   207  				Kind:     "TFJob",
   208  				Singular: "tfjob",
   209  			},
   210  			Validation: &apiextensionsv1beta1.CustomResourceValidation{
   211  				OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   212  					Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   213  						"tfReplicaSpecs": {
   214  							Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   215  								"Worker": {
   216  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   217  										"replicas": {
   218  											Type:    "integer",
   219  											Minimum: pointer.Float64Ptr(1),
   220  										},
   221  									},
   222  								},
   223  								"PS": {
   224  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   225  										"replicas": {
   226  											Type: "integer", Minimum: pointer.Float64Ptr(1),
   227  										},
   228  									},
   229  								},
   230  								"Chief": {
   231  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   232  										"replicas": {
   233  											Type:    "integer",
   234  											Minimum: pointer.Float64Ptr(1),
   235  											Maximum: pointer.Float64Ptr(1),
   236  										},
   237  									},
   238  								},
   239  							},
   240  						},
   241  					},
   242  				},
   243  			},
   244  		},
   245  	}
   246  
   247  	s.assertCustomerResourceDefinitions(
   248  		c, crds,
   249  		s.mockCustomResourceDefinitionV1Beta1.EXPECT().Create(gomock.Any(), crd, v1.CreateOptions{}).Return(crd, nil),
   250  	)
   251  }
   252  
   253  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsCreateV1beta1Upgrade(c *gc.C) {
   254  	ctrl := s.setupController(c)
   255  	defer ctrl.Finish()
   256  
   257  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
   258  		Major: "1", Minor: "22",
   259  	}, nil)
   260  
   261  	crds := []k8sspecs.K8sCustomResourceDefinition{
   262  		{
   263  			Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"},
   264  			Spec: k8sspecs.K8sCustomResourceDefinitionSpec{
   265  				Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1,
   266  				SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   267  					Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   268  						Kind:     "TFJob",
   269  						Singular: "tfjob",
   270  						Plural:   "tfjobs",
   271  					},
   272  					Version: "v1alpha2",
   273  					Group:   "kubeflow.org",
   274  					Scope:   "Namespaced",
   275  					Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
   276  						{
   277  							Name:    "v1alpha2",
   278  							Served:  true,
   279  							Storage: true,
   280  						},
   281  					},
   282  					Validation: &apiextensionsv1beta1.CustomResourceValidation{
   283  						OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   284  							Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   285  								"tfReplicaSpecs": {
   286  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   287  										"Worker": {
   288  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   289  												"replicas": {
   290  													Type:    "integer",
   291  													Minimum: pointer.Float64Ptr(1),
   292  												},
   293  											},
   294  										},
   295  										"PS": {
   296  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   297  												"replicas": {
   298  													Type: "integer", Minimum: pointer.Float64Ptr(1),
   299  												},
   300  											},
   301  										},
   302  										"Chief": {
   303  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   304  												"replicas": {
   305  													Type:    "integer",
   306  													Minimum: pointer.Float64Ptr(1),
   307  													Maximum: pointer.Float64Ptr(1),
   308  												},
   309  											},
   310  										},
   311  									},
   312  								},
   313  							},
   314  						},
   315  					},
   316  				},
   317  			},
   318  		},
   319  	}
   320  
   321  	crd2 := &apiextensionsv1.CustomResourceDefinition{
   322  		ObjectMeta: v1.ObjectMeta{
   323  			Name:        "tfjobs.kubeflow.org",
   324  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
   325  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
   326  		},
   327  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   328  			Scope: apiextensionsv1.NamespaceScoped,
   329  			Group: "kubeflow.org",
   330  			Names: apiextensionsv1.CustomResourceDefinitionNames{
   331  				Plural:   "tfjobs",
   332  				Kind:     "TFJob",
   333  				Singular: "tfjob",
   334  			},
   335  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   336  				{
   337  					Name:    "v1alpha2",
   338  					Served:  true,
   339  					Storage: true,
   340  					Schema: &apiextensionsv1.CustomResourceValidation{
   341  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   342  							Properties: map[string]apiextensionsv1.JSONSchemaProps{
   343  								"tfReplicaSpecs": {
   344  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
   345  										"Worker": {
   346  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
   347  												"replicas": {
   348  													Type:    "integer",
   349  													Minimum: pointer.Float64Ptr(1),
   350  												},
   351  											},
   352  										},
   353  										"PS": {
   354  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
   355  												"replicas": {
   356  													Type: "integer", Minimum: pointer.Float64Ptr(1),
   357  												},
   358  											},
   359  										},
   360  										"Chief": {
   361  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
   362  												"replicas": {
   363  													Type:    "integer",
   364  													Minimum: pointer.Float64Ptr(1),
   365  													Maximum: pointer.Float64Ptr(1),
   366  												},
   367  											},
   368  										},
   369  									},
   370  								},
   371  							},
   372  						},
   373  					},
   374  					AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{},
   375  				},
   376  			},
   377  		},
   378  	}
   379  
   380  	s.assertCustomerResourceDefinitions(
   381  		c, crds,
   382  		s.mockCustomResourceDefinitionV1.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(arg0 context.Context, arg1 *apiextensionsv1.CustomResourceDefinition, arg2 v1.CreateOptions) {
   383  			// For some reason, gomock can't compare this but jc.DeepEquals has no problem.
   384  			c.Check(arg1, jc.DeepEquals, crd2)
   385  		}).Return(crd2, nil),
   386  	)
   387  }
   388  
   389  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsUpdateV1beta1(c *gc.C) {
   390  	ctrl := s.setupController(c)
   391  	defer ctrl.Finish()
   392  
   393  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
   394  		Major: "1", Minor: "21",
   395  	}, nil)
   396  
   397  	crds := []k8sspecs.K8sCustomResourceDefinition{
   398  		{
   399  			Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"},
   400  			Spec: k8sspecs.K8sCustomResourceDefinitionSpec{
   401  				Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1,
   402  				SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   403  					Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   404  						Kind:     "TFJob",
   405  						Singular: "tfjob",
   406  						Plural:   "tfjobs",
   407  					},
   408  					Version: "v1alpha2",
   409  					Group:   "kubeflow.org",
   410  					Scope:   "Namespaced",
   411  					Validation: &apiextensionsv1beta1.CustomResourceValidation{
   412  						OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   413  							Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   414  								"tfReplicaSpecs": {
   415  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   416  										"Worker": {
   417  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   418  												"replicas": {
   419  													Type:    "integer",
   420  													Minimum: pointer.Float64Ptr(1),
   421  												},
   422  											},
   423  										},
   424  										"PS": {
   425  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   426  												"replicas": {
   427  													Type: "integer", Minimum: pointer.Float64Ptr(1),
   428  												},
   429  											},
   430  										},
   431  										"Chief": {
   432  											Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   433  												"replicas": {
   434  													Type:    "integer",
   435  													Minimum: pointer.Float64Ptr(1),
   436  													Maximum: pointer.Float64Ptr(1),
   437  												},
   438  											},
   439  										},
   440  									},
   441  								},
   442  							},
   443  						},
   444  					},
   445  				},
   446  			},
   447  		},
   448  	}
   449  
   450  	crd := &apiextensionsv1beta1.CustomResourceDefinition{
   451  		ObjectMeta: v1.ObjectMeta{
   452  			Name:        "tfjobs.kubeflow.org",
   453  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
   454  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
   455  		},
   456  		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   457  			Group:   "kubeflow.org",
   458  			Version: "v1alpha2",
   459  			Scope:   "Namespaced",
   460  			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   461  				Plural:   "tfjobs",
   462  				Kind:     "TFJob",
   463  				Singular: "tfjob",
   464  			},
   465  			Validation: &apiextensionsv1beta1.CustomResourceValidation{
   466  				OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   467  					Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   468  						"tfReplicaSpecs": {
   469  							Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   470  								"Worker": {
   471  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   472  										"replicas": {
   473  											Type:    "integer",
   474  											Minimum: pointer.Float64Ptr(1),
   475  										},
   476  									},
   477  								},
   478  								"PS": {
   479  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   480  										"replicas": {
   481  											Type: "integer", Minimum: pointer.Float64Ptr(1),
   482  										},
   483  									},
   484  								},
   485  								"Chief": {
   486  									Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   487  										"replicas": {
   488  											Type:    "integer",
   489  											Minimum: pointer.Float64Ptr(1),
   490  											Maximum: pointer.Float64Ptr(1),
   491  										},
   492  									},
   493  								},
   494  							},
   495  						},
   496  					},
   497  				},
   498  			},
   499  		},
   500  	}
   501  
   502  	s.assertCustomerResourceDefinitions(
   503  		c, crds,
   504  		s.mockCustomResourceDefinitionV1Beta1.EXPECT().Create(gomock.Any(), crd, v1.CreateOptions{}).Return(crd, s.k8sAlreadyExistsError()),
   505  		s.mockCustomResourceDefinitionV1Beta1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Return(crd, nil),
   506  		s.mockCustomResourceDefinitionV1Beta1.EXPECT().Update(gomock.Any(), crd, v1.UpdateOptions{}).Return(crd, nil),
   507  	)
   508  }
   509  
   510  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsCreateV1(c *gc.C) {
   511  	ctrl := s.setupController(c)
   512  	defer ctrl.Finish()
   513  
   514  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
   515  		Major: "1", Minor: "22",
   516  	}, nil)
   517  
   518  	crds := []k8sspecs.K8sCustomResourceDefinition{
   519  		{
   520  			Meta: k8sspecs.Meta{
   521  				Name: "certificates.networking.internal.knative.dev",
   522  			},
   523  			Spec: k8sspecs.K8sCustomResourceDefinitionSpec{
   524  				Version: k8sspecs.K8sCustomResourceDefinitionV1,
   525  				SpecV1: apiextensionsv1.CustomResourceDefinitionSpec{
   526  					Scope: apiextensionsv1.NamespaceScoped,
   527  					Group: "networking.internal.knative.dev",
   528  					Names: apiextensionsv1.CustomResourceDefinitionNames{
   529  						Kind:     "Certificate",
   530  						Plural:   "certificates",
   531  						Singular: "certificate",
   532  						Categories: []string{
   533  							"knative-internal",
   534  							"networking",
   535  						},
   536  						ShortNames: []string{
   537  							"kcert",
   538  						},
   539  					},
   540  					Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   541  						{
   542  							Name:    "v1alpha1",
   543  							Served:  true,
   544  							Storage: true,
   545  							Subresources: &apiextensionsv1.CustomResourceSubresources{
   546  								Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   547  							},
   548  							Schema: &apiextensionsv1.CustomResourceValidation{
   549  								OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   550  									Type:                   "object",
   551  									XPreserveUnknownFields: pointer.BoolPtr(true),
   552  								},
   553  							},
   554  							AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{
   555  								{
   556  									Name:     "Ready",
   557  									Type:     "string",
   558  									JSONPath: ".status.conditions[?(@.type==\"Ready\")].status",
   559  								},
   560  								{
   561  									Name:     "Reason",
   562  									Type:     "string",
   563  									JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason",
   564  								},
   565  							},
   566  						},
   567  					},
   568  				},
   569  			},
   570  		},
   571  	}
   572  
   573  	crd := &apiextensionsv1.CustomResourceDefinition{
   574  		ObjectMeta: v1.ObjectMeta{
   575  			Name:        "certificates.networking.internal.knative.dev",
   576  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
   577  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
   578  		},
   579  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   580  			Scope: apiextensionsv1.NamespaceScoped,
   581  			Group: "networking.internal.knative.dev",
   582  			Names: apiextensionsv1.CustomResourceDefinitionNames{
   583  				Kind:     "Certificate",
   584  				Plural:   "certificates",
   585  				Singular: "certificate",
   586  				Categories: []string{
   587  					"knative-internal",
   588  					"networking",
   589  				},
   590  				ShortNames: []string{
   591  					"kcert",
   592  				},
   593  			},
   594  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   595  				{
   596  					Name:    "v1alpha1",
   597  					Served:  true,
   598  					Storage: true,
   599  					Subresources: &apiextensionsv1.CustomResourceSubresources{
   600  						Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   601  					},
   602  					Schema: &apiextensionsv1.CustomResourceValidation{
   603  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   604  							Type:                   "object",
   605  							XPreserveUnknownFields: pointer.BoolPtr(true),
   606  						},
   607  					},
   608  					AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{
   609  						{
   610  							Name:     "Ready",
   611  							Type:     "string",
   612  							JSONPath: ".status.conditions[?(@.type==\"Ready\")].status",
   613  						},
   614  						{
   615  							Name:     "Reason",
   616  							Type:     "string",
   617  							JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason",
   618  						},
   619  					},
   620  				},
   621  			},
   622  		},
   623  	}
   624  
   625  	s.assertCustomerResourceDefinitions(
   626  		c, crds,
   627  		s.mockCustomResourceDefinitionV1.EXPECT().Create(gomock.Any(), crd, gomock.Any()).Return(crd, nil),
   628  	)
   629  }
   630  
   631  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsUpdateV1(c *gc.C) {
   632  	ctrl := s.setupController(c)
   633  	defer ctrl.Finish()
   634  
   635  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
   636  		Major: "1", Minor: "22",
   637  	}, nil)
   638  
   639  	crds := []k8sspecs.K8sCustomResourceDefinition{
   640  		{
   641  			Meta: k8sspecs.Meta{
   642  				Name: "certificates.networking.internal.knative.dev",
   643  			},
   644  			Spec: k8sspecs.K8sCustomResourceDefinitionSpec{
   645  				Version: k8sspecs.K8sCustomResourceDefinitionV1,
   646  				SpecV1: apiextensionsv1.CustomResourceDefinitionSpec{
   647  					Scope: apiextensionsv1.NamespaceScoped,
   648  					Group: "networking.internal.knative.dev",
   649  					Names: apiextensionsv1.CustomResourceDefinitionNames{
   650  						Kind:     "Certificate",
   651  						Plural:   "certificates",
   652  						Singular: "certificate",
   653  						Categories: []string{
   654  							"knative-internal",
   655  							"networking",
   656  						},
   657  						ShortNames: []string{
   658  							"kcert",
   659  						},
   660  					},
   661  					Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   662  						{
   663  							Name:    "v1alpha1",
   664  							Served:  true,
   665  							Storage: true,
   666  							Subresources: &apiextensionsv1.CustomResourceSubresources{
   667  								Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   668  							},
   669  							Schema: &apiextensionsv1.CustomResourceValidation{
   670  								OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   671  									Type:                   "object",
   672  									XPreserveUnknownFields: pointer.BoolPtr(true),
   673  								},
   674  							},
   675  							AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{
   676  								{
   677  									Name:     "Ready",
   678  									Type:     "string",
   679  									JSONPath: ".status.conditions[?(@.type==\"Ready\")].status",
   680  								},
   681  								{
   682  									Name:     "Reason",
   683  									Type:     "string",
   684  									JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason",
   685  								},
   686  							},
   687  						},
   688  					},
   689  				},
   690  			},
   691  		},
   692  	}
   693  
   694  	crd := &apiextensionsv1.CustomResourceDefinition{
   695  		ObjectMeta: v1.ObjectMeta{
   696  			Name:        "certificates.networking.internal.knative.dev",
   697  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
   698  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
   699  		},
   700  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   701  			Scope: apiextensionsv1.NamespaceScoped,
   702  			Group: "networking.internal.knative.dev",
   703  			Names: apiextensionsv1.CustomResourceDefinitionNames{
   704  				Kind:     "Certificate",
   705  				Plural:   "certificates",
   706  				Singular: "certificate",
   707  				Categories: []string{
   708  					"knative-internal",
   709  					"networking",
   710  				},
   711  				ShortNames: []string{
   712  					"kcert",
   713  				},
   714  			},
   715  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   716  				{
   717  					Name:    "v1alpha1",
   718  					Served:  true,
   719  					Storage: true,
   720  					Subresources: &apiextensionsv1.CustomResourceSubresources{
   721  						Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   722  					},
   723  					Schema: &apiextensionsv1.CustomResourceValidation{
   724  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   725  							Type:                   "object",
   726  							XPreserveUnknownFields: pointer.BoolPtr(true),
   727  						},
   728  					},
   729  					AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{
   730  						{
   731  							Name:     "Ready",
   732  							Type:     "string",
   733  							JSONPath: ".status.conditions[?(@.type==\"Ready\")].status",
   734  						},
   735  						{
   736  							Name:     "Reason",
   737  							Type:     "string",
   738  							JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason",
   739  						},
   740  					},
   741  				},
   742  			},
   743  		},
   744  	}
   745  
   746  	s.assertCustomerResourceDefinitions(
   747  		c, crds,
   748  		s.mockCustomResourceDefinitionV1.EXPECT().Create(gomock.Any(), crd, gomock.Any()).Return(crd, s.k8sAlreadyExistsError()),
   749  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "certificates.networking.internal.knative.dev", v1.GetOptions{}).Return(crd, nil),
   750  		s.mockCustomResourceDefinitionV1.EXPECT().Update(gomock.Any(), crd, gomock.Any()).Return(crd, nil),
   751  	)
   752  }
   753  
   754  func (s *K8sBrokerSuite) assertCustomerResources(c *gc.C, crs map[string][]unstructured.Unstructured, adjustClock func(), assertCalls ...any) {
   755  
   756  	basicPodSpec := getBasicPodspec()
   757  	basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{
   758  		KubernetesResources: &k8sspecs.KubernetesResources{
   759  			CustomResources: crs,
   760  		},
   761  	}
   762  	workloadSpec, err := provider.PrepareWorkloadSpec(
   763  		"app-name", "app-name", basicPodSpec, resources.DockerImageDetails{RegistryPath: "operator/image-path"},
   764  	)
   765  	c.Assert(err, jc.ErrorIsNil)
   766  	podSpec := provider.Pod(workloadSpec).PodSpec
   767  
   768  	numUnits := int32(2)
   769  	statefulSetArg := &appsv1.StatefulSet{
   770  		ObjectMeta: v1.ObjectMeta{
   771  			Name:   "app-name",
   772  			Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"},
   773  			Annotations: map[string]string{
   774  				"app.juju.is/uuid":               "appuuid",
   775  				"controller.juju.is/id":          testing.ControllerTag.Id(),
   776  				"charm.juju.is/modified-version": "0",
   777  			},
   778  		},
   779  		Spec: appsv1.StatefulSetSpec{
   780  			Replicas: &numUnits,
   781  			Selector: &v1.LabelSelector{
   782  				MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"},
   783  			},
   784  			RevisionHistoryLimit: pointer.Int32Ptr(0),
   785  			Template: core.PodTemplateSpec{
   786  				ObjectMeta: v1.ObjectMeta{
   787  					Labels: map[string]string{"app.kubernetes.io/name": "app-name"},
   788  					Annotations: map[string]string{
   789  						"apparmor.security.beta.kubernetes.io/pod": "runtime/default",
   790  						"seccomp.security.beta.kubernetes.io/pod":  "docker/default",
   791  						"controller.juju.is/id":                    testing.ControllerTag.Id(),
   792  						"charm.juju.is/modified-version":           "0",
   793  					},
   794  				},
   795  				Spec: podSpec,
   796  			},
   797  			PodManagementPolicy: appsv1.ParallelPodManagement,
   798  			ServiceName:         "app-name-endpoints",
   799  		},
   800  	}
   801  
   802  	serviceArg := *basicServiceArg
   803  	serviceArg.Spec.Type = core.ServiceTypeClusterIP
   804  
   805  	assertCalls = append(
   806  		[]any{
   807  			s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}).
   808  				Return(nil, s.k8sNotFoundError()),
   809  		},
   810  		assertCalls...,
   811  	)
   812  
   813  	ociImageSecret := s.getOCIImageSecret(c, nil)
   814  	assertCalls = append(assertCalls, []any{
   815  		s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}).
   816  			Return(ociImageSecret, nil),
   817  		s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}).
   818  			Return(nil, s.k8sNotFoundError()),
   819  		s.mockServices.EXPECT().Update(gomock.Any(), &serviceArg, v1.UpdateOptions{}).
   820  			Return(nil, s.k8sNotFoundError()),
   821  		s.mockServices.EXPECT().Create(gomock.Any(), &serviceArg, v1.CreateOptions{}).
   822  			Return(nil, nil),
   823  		s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}).
   824  			Return(nil, s.k8sNotFoundError()),
   825  		s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}).
   826  			Return(nil, s.k8sNotFoundError()),
   827  		s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}).
   828  			Return(nil, nil),
   829  		s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}).
   830  			Return(statefulSetArg, nil),
   831  		s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}).
   832  			Return(nil, nil),
   833  	}...)
   834  	gomock.InOrder(assertCalls...)
   835  
   836  	errChan := make(chan error)
   837  	go func() {
   838  		params := &caas.ServiceParams{
   839  			PodSpec: basicPodSpec,
   840  			Deployment: caas.DeploymentParams{
   841  				DeploymentType: caas.DeploymentStateful,
   842  			},
   843  			ImageDetails: resources.DockerImageDetails{RegistryPath: "operator/image-path"},
   844  			ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()},
   845  		}
   846  		errChan <- s.broker.EnsureService("app-name",
   847  			func(_ string, _ status.Status, e string, _ map[string]interface{}) error {
   848  				c.Logf("EnsureService error -> %q", e)
   849  				return nil
   850  			},
   851  			params, 2, config.ConfigAttributes{
   852  				"kubernetes-service-loadbalancer-ip": "10.0.0.1",
   853  				"kubernetes-service-externalname":    "ext-name",
   854  			})
   855  
   856  	}()
   857  
   858  	adjustClock()
   859  
   860  	select {
   861  	case err := <-errChan:
   862  		c.Assert(err, jc.ErrorIsNil)
   863  	case <-time.After(testing.LongWait):
   864  		c.Fatalf("timed out waiting for EnsureService return")
   865  	}
   866  }
   867  
   868  func getCR1() unstructured.Unstructured {
   869  	return unstructured.Unstructured{
   870  		Object: map[string]interface{}{
   871  			"apiVersion": "kubeflow.org/v1",
   872  			"metadata": map[string]interface{}{
   873  				"name": "dist-mnist-for-e2e-test-1",
   874  			},
   875  			"kind": "TFJob",
   876  			"spec": map[string]interface{}{
   877  				"tfReplicaSpecs": map[string]interface{}{
   878  					"PS": map[string]interface{}{
   879  						"replicas":      int64(1),
   880  						"restartPolicy": "Never",
   881  						"template": map[string]interface{}{
   882  							"spec": map[string]interface{}{
   883  								"containers": []interface{}{
   884  									map[string]interface{}{
   885  										"name":  "tensorflow",
   886  										"image": "kubeflow/tf-dist-mnist-test:1.0",
   887  									},
   888  								},
   889  							},
   890  						},
   891  					},
   892  					"Worker": map[string]interface{}{
   893  						"replicas":      int64(1),
   894  						"restartPolicy": "Never",
   895  						"template": map[string]interface{}{
   896  							"spec": map[string]interface{}{
   897  								"containers": []interface{}{
   898  									map[string]interface{}{
   899  										"name":  "tensorflow",
   900  										"image": "kubeflow/tf-dist-mnist-test:1.0",
   901  									},
   902  								},
   903  							},
   904  						},
   905  					},
   906  				},
   907  			},
   908  		},
   909  	}
   910  }
   911  
   912  func getCR2() unstructured.Unstructured {
   913  	return unstructured.Unstructured{
   914  		Object: map[string]interface{}{
   915  			"apiVersion": "kubeflow.org/v1beta2",
   916  			"metadata": map[string]interface{}{
   917  				"name": "dist-mnist-for-e2e-test-2",
   918  			},
   919  			"kind": "TFJob",
   920  			"spec": map[string]interface{}{
   921  				"tfReplicaSpecs": map[string]interface{}{
   922  					"PS": map[string]interface{}{
   923  						"replicas":      int64(2),
   924  						"restartPolicy": "Never",
   925  						"template": map[string]interface{}{
   926  							"spec": map[string]interface{}{
   927  								"containers": []interface{}{
   928  									map[string]interface{}{
   929  										"name":  "tensorflow",
   930  										"image": "kubeflow/tf-dist-mnist-test:1.0",
   931  									},
   932  								},
   933  							},
   934  						},
   935  					},
   936  					"Worker": map[string]interface{}{
   937  						"replicas":      int64(2),
   938  						"restartPolicy": "Never",
   939  						"template": map[string]interface{}{
   940  							"spec": map[string]interface{}{
   941  								"containers": []interface{}{
   942  									map[string]interface{}{
   943  										"name":  "tensorflow",
   944  										"image": "kubeflow/tf-dist-mnist-test:1.0",
   945  									},
   946  								},
   947  							},
   948  						},
   949  					},
   950  				},
   951  			},
   952  		},
   953  	}
   954  }
   955  
   956  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourcesCreate(c *gc.C) {
   957  	ctrl := s.setupController(c)
   958  	defer ctrl.Finish()
   959  
   960  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
   961  		Major: "1", Minor: "22",
   962  	}, nil)
   963  
   964  	crRaw1 := getCR1()
   965  	crRaw2 := getCR2()
   966  
   967  	cr1 := getCR1()
   968  	cr1.SetLabels(map[string]string{"juju-app": "app-name"})
   969  	cr1.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"})
   970  	cr1.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()})
   971  	cr2 := getCR2()
   972  	cr2.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"})
   973  	cr2.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()})
   974  
   975  	crs := map[string][]unstructured.Unstructured{
   976  		"tfjobs.kubeflow.org": {
   977  			crRaw1, crRaw2,
   978  		},
   979  	}
   980  
   981  	crd := &apiextensionsv1.CustomResourceDefinition{
   982  		ObjectMeta: v1.ObjectMeta{
   983  			Name:        "tfjobs.kubeflow.org",
   984  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
   985  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
   986  		},
   987  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   988  			Group: "kubeflow.org",
   989  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   990  				{
   991  					Name:    "v1",
   992  					Served:  true,
   993  					Storage: true,
   994  					Schema: &apiextensionsv1.CustomResourceValidation{
   995  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   996  							Properties: map[string]apiextensionsv1.JSONSchemaProps{
   997  								"spec": {
   998  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
   999  										"tfReplicaSpecs": {
  1000  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1001  												"PS": {
  1002  													Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1003  														"replicas": {
  1004  															Type: "integer", Minimum: pointer.Float64Ptr(1),
  1005  														},
  1006  													},
  1007  												},
  1008  												"Chief": {
  1009  													Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1010  														"replicas": {
  1011  															Type:    "integer",
  1012  															Minimum: pointer.Float64Ptr(1),
  1013  															Maximum: pointer.Float64Ptr(1),
  1014  														},
  1015  													},
  1016  												},
  1017  												"Worker": {
  1018  													Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1019  														"replicas": {
  1020  															Type:    "integer",
  1021  															Minimum: pointer.Float64Ptr(1),
  1022  														},
  1023  													},
  1024  												},
  1025  											},
  1026  										},
  1027  									},
  1028  								},
  1029  							},
  1030  						},
  1031  					},
  1032  				},
  1033  				{Name: "v1beta2", Served: true, Storage: false},
  1034  			},
  1035  			Scope: "Namespaced",
  1036  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1037  				Kind:     "TFJob",
  1038  				Plural:   "tfjobs",
  1039  				Singular: "tfjob",
  1040  			},
  1041  		},
  1042  	}
  1043  
  1044  	s.assertCustomerResources(
  1045  		c, crs,
  1046  		func() {
  1047  			// CRD is ready in 1st time checking.
  1048  		},
  1049  		// waits CRD stablised.
  1050  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Return(crd, nil),
  1051  		s.mockDynamicClient.EXPECT().Resource(
  1052  			schema.GroupVersionResource{
  1053  				Group:    crd.Spec.Group,
  1054  				Version:  "v1",
  1055  				Resource: crd.Spec.Names.Plural,
  1056  			},
  1057  		).Return(s.mockNamespaceableResourceClient),
  1058  		s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Return(&unstructured.UnstructuredList{}, nil),
  1059  
  1060  		// ensuring cr1.
  1061  		s.mockDynamicClient.EXPECT().Resource(
  1062  			schema.GroupVersionResource{
  1063  				Group:    crd.Spec.Group,
  1064  				Version:  "v1",
  1065  				Resource: crd.Spec.Names.Plural,
  1066  			},
  1067  		).Return(s.mockNamespaceableResourceClient),
  1068  		s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr1, v1.CreateOptions{}).Return(&cr1, nil),
  1069  
  1070  		// ensuring cr2.
  1071  		s.mockDynamicClient.EXPECT().Resource(
  1072  			schema.GroupVersionResource{
  1073  				Group:    crd.Spec.Group,
  1074  				Version:  "v1beta2",
  1075  				Resource: crd.Spec.Names.Plural,
  1076  			},
  1077  		).Return(s.mockNamespaceableResourceClient),
  1078  		s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr2, v1.CreateOptions{}).Return(&cr2, nil),
  1079  	)
  1080  }
  1081  
  1082  func (s *K8sBrokerSuite) TestEnsureServiceCustomResourcesUpdate(c *gc.C) {
  1083  	ctrl := s.setupController(c)
  1084  	defer ctrl.Finish()
  1085  
  1086  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
  1087  		Major: "1", Minor: "22",
  1088  	}, nil)
  1089  
  1090  	crRaw1 := getCR1()
  1091  	crRaw2 := getCR2()
  1092  
  1093  	cr1 := getCR1()
  1094  	cr1.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"})
  1095  	cr1.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()})
  1096  	cr2 := getCR2()
  1097  	cr2.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"})
  1098  	cr2.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()})
  1099  
  1100  	crUpdatedResourceVersion1 := getCR1()
  1101  	crUpdatedResourceVersion1.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"})
  1102  	crUpdatedResourceVersion1.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()})
  1103  	crUpdatedResourceVersion1.SetResourceVersion("11111")
  1104  
  1105  	crUpdatedResourceVersion2 := getCR2()
  1106  	crUpdatedResourceVersion2.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"})
  1107  	crUpdatedResourceVersion2.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()})
  1108  	crUpdatedResourceVersion2.SetResourceVersion("11111")
  1109  
  1110  	crs := map[string][]unstructured.Unstructured{
  1111  		"tfjobs.kubeflow.org": {
  1112  			crRaw1, crRaw2,
  1113  		},
  1114  	}
  1115  
  1116  	crd := &apiextensionsv1.CustomResourceDefinition{
  1117  		ObjectMeta: v1.ObjectMeta{
  1118  			Name:        "tfjobs.kubeflow.org",
  1119  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"},
  1120  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
  1121  		},
  1122  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1123  			Group: "kubeflow.org",
  1124  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  1125  				{
  1126  					Name:    "v1",
  1127  					Served:  true,
  1128  					Storage: true,
  1129  					Schema: &apiextensionsv1.CustomResourceValidation{
  1130  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  1131  							Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1132  								"spec": {
  1133  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1134  										"tfReplicaSpecs": {
  1135  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1136  												"PS": {
  1137  													Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1138  														"replicas": {
  1139  															Type: "integer", Minimum: pointer.Float64Ptr(1),
  1140  														},
  1141  													},
  1142  												},
  1143  												"Chief": {
  1144  													Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1145  														"replicas": {
  1146  															Type:    "integer",
  1147  															Minimum: pointer.Float64Ptr(1),
  1148  															Maximum: pointer.Float64Ptr(1),
  1149  														},
  1150  													},
  1151  												},
  1152  												"Worker": {
  1153  													Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1154  														"replicas": {
  1155  															Type:    "integer",
  1156  															Minimum: pointer.Float64Ptr(1),
  1157  														},
  1158  													},
  1159  												},
  1160  											},
  1161  										},
  1162  									},
  1163  								},
  1164  							},
  1165  						},
  1166  					},
  1167  				},
  1168  				{Name: "v1beta2", Served: true, Storage: false},
  1169  			},
  1170  			Scope: "Namespaced",
  1171  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1172  				Kind:     "TFJob",
  1173  				Plural:   "tfjobs",
  1174  				Singular: "tfjob",
  1175  			},
  1176  		},
  1177  	}
  1178  
  1179  	s.assertCustomerResources(
  1180  		c, crs,
  1181  		func() {
  1182  			err := s.clock.WaitAdvance(time.Second, testing.LongWait, 1)
  1183  			c.Assert(err, jc.ErrorIsNil)
  1184  
  1185  			err = s.clock.WaitAdvance(time.Second, testing.LongWait, 1)
  1186  			c.Assert(err, jc.ErrorIsNil)
  1187  		},
  1188  		// waits CRD stabilised.
  1189  		// 1. CRD not found.
  1190  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(nil, s.k8sNotFoundError()),
  1191  		// 2. CRD resource type not ready yet.
  1192  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil),
  1193  		s.mockDynamicClient.EXPECT().Resource(
  1194  			schema.GroupVersionResource{
  1195  				Group:    crd.Spec.Group,
  1196  				Version:  "v1",
  1197  				Resource: crd.Spec.Names.Plural,
  1198  			},
  1199  		).Times(1).Return(s.mockNamespaceableResourceClient),
  1200  		s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(nil, s.k8sNotFoundError()),
  1201  		// 3. CRD is ready.
  1202  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil),
  1203  		s.mockDynamicClient.EXPECT().Resource(
  1204  			schema.GroupVersionResource{
  1205  				Group:    crd.Spec.Group,
  1206  				Version:  "v1",
  1207  				Resource: crd.Spec.Names.Plural,
  1208  			},
  1209  		).Times(1).Return(s.mockNamespaceableResourceClient),
  1210  		s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(
  1211  			&unstructured.UnstructuredList{Items: []unstructured.Unstructured{{Object: map[string]interface{}{}}}}, nil,
  1212  		),
  1213  
  1214  		// ensuring cr1.
  1215  		s.mockDynamicClient.EXPECT().Resource(
  1216  			schema.GroupVersionResource{
  1217  				Group:    crd.Spec.Group,
  1218  				Version:  "v1",
  1219  				Resource: crd.Spec.Names.Plural,
  1220  			},
  1221  		).Return(s.mockNamespaceableResourceClient),
  1222  		s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr1, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()),
  1223  		s.mockResourceClient.EXPECT().Get(gomock.Any(), "dist-mnist-for-e2e-test-1", v1.GetOptions{}).Return(&crUpdatedResourceVersion1, nil),
  1224  		s.mockResourceClient.EXPECT().Update(gomock.Any(), &crUpdatedResourceVersion1, v1.UpdateOptions{}).Return(&crUpdatedResourceVersion1, nil),
  1225  
  1226  		// ensuring cr2.
  1227  		s.mockDynamicClient.EXPECT().Resource(
  1228  			schema.GroupVersionResource{
  1229  				Group:    crd.Spec.Group,
  1230  				Version:  "v1beta2",
  1231  				Resource: crd.Spec.Names.Plural,
  1232  			},
  1233  		).Return(s.mockNamespaceableResourceClient),
  1234  		s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr2, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()),
  1235  		s.mockResourceClient.EXPECT().Get(gomock.Any(), "dist-mnist-for-e2e-test-2", v1.GetOptions{}).Return(&crUpdatedResourceVersion2, nil),
  1236  		s.mockResourceClient.EXPECT().Update(gomock.Any(), &crUpdatedResourceVersion2, v1.UpdateOptions{}).Return(&crUpdatedResourceVersion2, nil),
  1237  	)
  1238  }
  1239  
  1240  func (s *K8sBrokerSuite) TestCRDGetter(c *gc.C) {
  1241  	ctrl := s.setupController(c)
  1242  	defer ctrl.Finish()
  1243  
  1244  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
  1245  		Major: "1", Minor: "22",
  1246  	}, nil)
  1247  
  1248  	crdGetter := provider.CRDGetter{s.broker}
  1249  
  1250  	badCRDNoVersion := &apiextensionsv1.CustomResourceDefinition{
  1251  		ObjectMeta: v1.ObjectMeta{
  1252  			Name:        "tfjobs.kubeflow.org",
  1253  			Labels:      map[string]string{"juju-app": "app-name", "juju-model": "test"},
  1254  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
  1255  		},
  1256  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1257  			Group: "kubeflow.org",
  1258  			Scope: "Namespaced",
  1259  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1260  				Plural:   "tfjobs",
  1261  				Kind:     "TFJob",
  1262  				Singular: "tfjob",
  1263  			},
  1264  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  1265  				{
  1266  					Name:    "v1",
  1267  					Storage: true,
  1268  					Schema: &apiextensionsv1.CustomResourceValidation{
  1269  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  1270  							Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1271  								"tfReplicaSpecs": {
  1272  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1273  										"Worker": {
  1274  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1275  												"replicas": {
  1276  													Type:    "integer",
  1277  													Minimum: pointer.Float64Ptr(1),
  1278  												},
  1279  											},
  1280  										},
  1281  										"PS": {
  1282  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1283  												"replicas": {
  1284  													Type: "integer", Minimum: pointer.Float64Ptr(1),
  1285  												},
  1286  											},
  1287  										},
  1288  										"Chief": {
  1289  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1290  												"replicas": {
  1291  													Type:    "integer",
  1292  													Minimum: pointer.Float64Ptr(1),
  1293  													Maximum: pointer.Float64Ptr(1),
  1294  												},
  1295  											},
  1296  										},
  1297  									},
  1298  								},
  1299  							},
  1300  						},
  1301  					},
  1302  				},
  1303  			},
  1304  		},
  1305  	}
  1306  
  1307  	// Test 1: Invalid CRD found - no version.
  1308  	gomock.InOrder(
  1309  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(badCRDNoVersion, nil),
  1310  	)
  1311  	result, err := crdGetter.Get("tfjobs.kubeflow.org")
  1312  	c.Assert(err, jc.Satisfies, errors.IsNotValid)
  1313  	c.Assert(result, gc.IsNil)
  1314  
  1315  	crd := &apiextensionsv1.CustomResourceDefinition{
  1316  		ObjectMeta: v1.ObjectMeta{
  1317  			Name:        "tfjobs.kubeflow.org",
  1318  			Labels:      map[string]string{"juju-app": "app-name", "juju-model": "test"},
  1319  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
  1320  		},
  1321  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1322  			Group: "kubeflow.org",
  1323  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  1324  				{
  1325  					Name:    "v1",
  1326  					Served:  true,
  1327  					Storage: true,
  1328  					Schema: &apiextensionsv1.CustomResourceValidation{
  1329  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  1330  							Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1331  								"tfReplicaSpecs": {
  1332  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1333  										"Worker": {
  1334  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1335  												"replicas": {
  1336  													Type:    "integer",
  1337  													Minimum: pointer.Float64Ptr(1),
  1338  												},
  1339  											},
  1340  										},
  1341  										"PS": {
  1342  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1343  												"replicas": {
  1344  													Type: "integer", Minimum: pointer.Float64Ptr(1),
  1345  												},
  1346  											},
  1347  										},
  1348  										"Chief": {
  1349  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1350  												"replicas": {
  1351  													Type:    "integer",
  1352  													Minimum: pointer.Float64Ptr(1),
  1353  													Maximum: pointer.Float64Ptr(1),
  1354  												},
  1355  											},
  1356  										},
  1357  									},
  1358  								},
  1359  							},
  1360  						},
  1361  					},
  1362  				},
  1363  			},
  1364  			Scope: "Namespaced",
  1365  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1366  				Plural:   "tfjobs",
  1367  				Kind:     "TFJob",
  1368  				Singular: "tfjob",
  1369  			},
  1370  		},
  1371  	}
  1372  
  1373  	// Test 2: not found CRD.
  1374  	gomock.InOrder(
  1375  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(nil, s.k8sNotFoundError()),
  1376  	)
  1377  	result, err = crdGetter.Get("tfjobs.kubeflow.org")
  1378  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
  1379  	c.Assert(result, gc.IsNil)
  1380  
  1381  	// Test 3: found CRD but CRD is not stablised yet.
  1382  	gomock.InOrder(
  1383  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil),
  1384  		s.mockDynamicClient.EXPECT().Resource(
  1385  			schema.GroupVersionResource{
  1386  				Group:    crd.Spec.Group,
  1387  				Version:  "v1",
  1388  				Resource: crd.Spec.Names.Plural,
  1389  			},
  1390  		).Times(1).Return(s.mockNamespaceableResourceClient),
  1391  		s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(nil, s.k8sNotFoundError()),
  1392  	)
  1393  	result, err = crdGetter.Get("tfjobs.kubeflow.org")
  1394  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
  1395  	c.Assert(result, gc.IsNil)
  1396  
  1397  	// Test 4: all good.
  1398  	gomock.InOrder(
  1399  		s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil),
  1400  		s.mockDynamicClient.EXPECT().Resource(
  1401  			schema.GroupVersionResource{
  1402  				Group:    crd.Spec.Group,
  1403  				Version:  "v1",
  1404  				Resource: crd.Spec.Names.Plural,
  1405  			},
  1406  		).Times(1).Return(s.mockNamespaceableResourceClient),
  1407  		s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(&unstructured.UnstructuredList{}, nil),
  1408  	)
  1409  	result, err = crdGetter.Get("tfjobs.kubeflow.org")
  1410  	c.Assert(err, jc.ErrorIsNil)
  1411  	c.Assert(result, jc.DeepEquals, crd)
  1412  }
  1413  
  1414  func (s *K8sBrokerSuite) TestGetCRDsForCRsAllGood(c *gc.C) {
  1415  	ctrl := s.setupController(c)
  1416  	defer ctrl.Finish()
  1417  
  1418  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
  1419  		Major: "1", Minor: "22",
  1420  	}, nil)
  1421  
  1422  	crd1 := &apiextensionsv1.CustomResourceDefinition{
  1423  		ObjectMeta: v1.ObjectMeta{
  1424  			Name:        "tfjobs.kubeflow.org",
  1425  			Labels:      map[string]string{"juju-app": "app-name", "juju-model": "test"},
  1426  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
  1427  		},
  1428  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1429  			Group: "kubeflow.org",
  1430  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  1431  				{
  1432  					Name:   "v1",
  1433  					Served: true,
  1434  					Schema: &apiextensionsv1.CustomResourceValidation{
  1435  						OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  1436  							Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1437  								"tfReplicaSpecs": {
  1438  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1439  										"Worker": {
  1440  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1441  												"replicas": {
  1442  													Type:    "integer",
  1443  													Minimum: pointer.Float64Ptr(1),
  1444  												},
  1445  											},
  1446  										},
  1447  										"PS": {
  1448  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1449  												"replicas": {
  1450  													Type: "integer", Minimum: pointer.Float64Ptr(1),
  1451  												},
  1452  											},
  1453  										},
  1454  										"Chief": {
  1455  											Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1456  												"replicas": {
  1457  													Type:    "integer",
  1458  													Minimum: pointer.Float64Ptr(1),
  1459  													Maximum: pointer.Float64Ptr(1),
  1460  												},
  1461  											},
  1462  										},
  1463  									},
  1464  								},
  1465  							},
  1466  						},
  1467  					},
  1468  				},
  1469  			},
  1470  			Scope: "Namespaced",
  1471  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1472  				Plural:   "tfjobs",
  1473  				Kind:     "TFJob",
  1474  				Singular: "tfjob",
  1475  			},
  1476  		},
  1477  	}
  1478  	crd2 := &apiextensionsv1.CustomResourceDefinition{
  1479  		ObjectMeta: v1.ObjectMeta{
  1480  			Name:        "scheduledworkflows.kubeflow.org",
  1481  			Labels:      map[string]string{"juju-app": "app-name", "juju-model": "test"},
  1482  			Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()},
  1483  		},
  1484  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1485  			Group: "kubeflow.org",
  1486  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  1487  				{
  1488  					Name: "v1beta1", Served: true,
  1489  				},
  1490  			},
  1491  			Scope: "Namespaced",
  1492  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1493  				Plural:   "scheduledworkflows",
  1494  				Kind:     "ScheduledWorkflow",
  1495  				Singular: "scheduledworkflow",
  1496  				ListKind: "ScheduledWorkflowList",
  1497  				ShortNames: []string{
  1498  					"swf",
  1499  				},
  1500  			},
  1501  		},
  1502  	}
  1503  
  1504  	expectedResult := map[string]*apiextensionsv1.CustomResourceDefinition{
  1505  		crd1.GetName(): crd1,
  1506  		crd2.GetName(): crd2,
  1507  	}
  1508  
  1509  	mockCRDGetter := mocks.NewMockCRDGetterInterface(ctrl)
  1510  
  1511  	// round 1. crd1 not found.
  1512  	mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").Times(1).Return(nil, errors.NotFoundf(""))
  1513  	// round 1. crd2 not found.
  1514  	mockCRDGetter.EXPECT().Get("scheduledworkflows.kubeflow.org").Times(1).Return(nil, errors.NotFoundf(""))
  1515  
  1516  	// round 2. crd1 not found.
  1517  	mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").Times(1).Return(nil, errors.NotFoundf(""))
  1518  	// round 2. crd2 found.
  1519  	mockCRDGetter.EXPECT().Get("scheduledworkflows.kubeflow.org").Times(1).Return(crd2, nil)
  1520  
  1521  	// round 3. crd1 found.
  1522  	mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").Times(1).Return(crd1, nil)
  1523  
  1524  	resultChan := make(chan map[string]*apiextensionsv1.CustomResourceDefinition)
  1525  	errChan := make(chan error)
  1526  
  1527  	go func(broker *provider.KubernetesClient) {
  1528  		crs := map[string][]unstructured.Unstructured{
  1529  			"tfjobs.kubeflow.org":             {},
  1530  			"scheduledworkflows.kubeflow.org": {},
  1531  		}
  1532  		result, err := broker.GetCRDsForCRs(crs, mockCRDGetter)
  1533  		errChan <- err
  1534  		resultChan <- result
  1535  	}(s.broker)
  1536  
  1537  	err := s.clock.WaitAdvance(time.Second, testing.ShortWait, 2)
  1538  	c.Assert(err, jc.ErrorIsNil)
  1539  
  1540  	err = s.clock.WaitAdvance(time.Second, testing.ShortWait, 1)
  1541  	c.Assert(err, jc.ErrorIsNil)
  1542  
  1543  	select {
  1544  	case err := <-errChan:
  1545  		c.Assert(err, jc.ErrorIsNil)
  1546  		result := <-resultChan
  1547  		c.Assert(result, gc.DeepEquals, expectedResult)
  1548  	case <-time.After(testing.LongWait):
  1549  		c.Fatalf("timed out waiting for GetCRDsForCRs return")
  1550  	}
  1551  }
  1552  
  1553  func (s *K8sBrokerSuite) TestGetCRDsForCRsFailEarly(c *gc.C) {
  1554  	ctrl := s.setupController(c)
  1555  	defer ctrl.Finish()
  1556  
  1557  	s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{
  1558  		Major: "1", Minor: "22",
  1559  	}, nil)
  1560  
  1561  	mockCRDGetter := mocks.NewMockCRDGetterInterface(ctrl)
  1562  	unExpectedErr := errors.New("a non not found error")
  1563  
  1564  	// round 1. crd1 not found.
  1565  	mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").AnyTimes().Return(nil, errors.NotFoundf(""))
  1566  	// round 1. crd2 un expected error - will not retry but abort the whole wg.
  1567  	mockCRDGetter.EXPECT().Get("scheduledworkflows.kubeflow.org").Times(1).Return(nil, unExpectedErr)
  1568  
  1569  	resultChan := make(chan map[string]*apiextensionsv1.CustomResourceDefinition)
  1570  	errChan := make(chan error)
  1571  
  1572  	go func(broker *provider.KubernetesClient) {
  1573  		crs := map[string][]unstructured.Unstructured{
  1574  			"tfjobs.kubeflow.org":             {},
  1575  			"scheduledworkflows.kubeflow.org": {},
  1576  		}
  1577  		result, err := broker.GetCRDsForCRs(crs, mockCRDGetter)
  1578  		errChan <- err
  1579  		resultChan <- result
  1580  	}(s.broker)
  1581  
  1582  	err := s.clock.WaitAdvance(time.Second, testing.ShortWait, 1)
  1583  	c.Assert(err, jc.ErrorIsNil)
  1584  
  1585  	select {
  1586  	case err := <-errChan:
  1587  		c.Assert(err, gc.ErrorMatches, `getting custom resources: a non not found error`)
  1588  		result := <-resultChan
  1589  		c.Assert(result, gc.IsNil)
  1590  	case <-time.After(testing.LongWait):
  1591  		c.Fatalf("timed out waiting for GetCRDsForCRs return")
  1592  	}
  1593  }