diff --git a/go.mod b/go.mod index de855c4f..99db8533 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/onsi/gomega v1.30.0 github.com/openstack-k8s-operators/infra-operator/apis v0.1.1-0.20230920125017-2c76cd203b44 github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231128145648-956f4d361a63 + github.com/openstack-k8s-operators/lib-common/modules/test v0.3.0 github.com/rabbitmq/cluster-operator v1.14.0 go.uber.org/zap v1.26.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e @@ -55,6 +56,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.10.0 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index 24251cf7..25308ec0 100644 --- a/go.sum +++ b/go.sum @@ -234,6 +234,8 @@ github.com/openshift/api v0.0.0-20230414143018-3367bc7e6ac7 h1:rncLxJBpFGqBztyxC github.com/openshift/api v0.0.0-20230414143018-3367bc7e6ac7/go.mod h1:ctXNyWanKEjGj8sss1KjjHQ3ENKFm33FFnS5BKaIPh4= github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231128145648-956f4d361a63 h1:iA/8vt+o2bMxYvvenNB7VArBvM8UyDLw3G7S/teMLc0= github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231128145648-956f4d361a63/go.mod h1:OYad2L+OD4j5CR49di7gu3Q1UkLBmpYwvtdoGlnasL4= +github.com/openstack-k8s-operators/lib-common/modules/test v0.3.0 h1:w2YR0OEXxmE1kQWhyyGPmQUY6s46xxnF3AnvpMpYjRU= +github.com/openstack-k8s-operators/lib-common/modules/test v0.3.0/go.mod h1:RfLOPJbmPzPZ4XHwwDc2tFbbw5zxZL15JFGwb5c6VaU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -356,6 +358,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index 1537b15f..86efb25c 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -25,13 +25,18 @@ import ( "github.com/google/uuid" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + rabbitmqclusterv1 "github.com/rabbitmq/cluster-operator/api/v1beta1" ) const ( @@ -136,6 +141,195 @@ func GetDefaultDNSDataSpec() map[string]interface{} { return spec } +func CreateTransportURL(name types.NamespacedName, spec map[string]interface{}) client.Object { + raw := map[string]interface{}{ + "apiVersion": "rabbitmq.openstack.org/v1beta1", + "kind": "TransportURL", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } + + return th.CreateUnstructured(raw) +} + +func CreateRabbitMQCluster(name types.NamespacedName, spec map[string]interface{}) client.Object { + raw := map[string]interface{}{ + "apiVersion": "rabbitmq.com/v1beta1", + "kind": "RabbitmqCluster", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } + + return th.CreateUnstructured(raw) +} + +func UpdateRabbitMQClusterToTLS(name types.NamespacedName) { + Eventually(func(g Gomega) { + mq := GetRabbitMQCluster(name) + g.Expect(mq).ToNot(BeNil()) + + _, err := controllerutil.CreateOrPatch( + th.Ctx, th.K8sClient, mq, func() error { + mq.Spec.TLS = rabbitmqclusterv1.TLSSpec{ + CaSecretName: "rootca-internal", + DisableNonTLSListeners: true, + SecretName: "cert-rabbitmq-svc", + } + return nil + }) + g.Expect(err).ShouldNot(HaveOccurred()) + }, th.Timeout, th.Interval).Should(Succeed()) +} + +func GetDefaultRabbitMQClusterSpec(tlsEnabled bool) map[string]interface{} { + spec := make(map[string]interface{}) + spec["delayStartSeconds"] = "30" + spec["image"] = "quay.io/podified-antelope-centos9/openstack-rabbitmq:current-podified" + if tlsEnabled { + spec["tls"] = map[string]interface{}{ + "caSecretName": "rootca-internal", + "disableNonTLSListeners": true, + "secretName": "cert-rabbitmq-svc", + } + } + + return spec +} + +// DeleteRabbitMQCluster deletes a RabbitMQCluster instance from the Kubernetes cluster. +func DeleteRabbitMQCluster(name types.NamespacedName) { + Eventually(func(g Gomega) { + mq := &rabbitmqclusterv1.RabbitmqCluster{} + err := th.K8sClient.Get(th.Ctx, name, mq) + // if it is already gone that is OK + if k8s_errors.IsNotFound(err) { + return + } + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(th.K8sClient.Delete(th.Ctx, mq)).Should(Succeed()) + + err = th.K8sClient.Get(th.Ctx, name, mq) + g.Expect(k8s_errors.IsNotFound(err)).To(BeTrue()) + }, th.Timeout, th.Interval).Should(Succeed()) +} + +func CreateOrUpdateRabbitMQClusterSecret(name types.NamespacedName, mq *rabbitmqclusterv1.RabbitmqCluster) { + Eventually(func(g Gomega) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + }, + } + + // create rabbitmq-secret secret + secretData := map[string][]byte{ + "host": []byte(fmt.Sprintf("host.%s.svc", namespace)), + "password": []byte("12345678"), + "username": []byte("user"), + "port": []byte("5672"), + } + + // if tls is enabled for rabbitmq cluster port will be 5671 + if mq.Spec.TLS.SecretName != "" { + secretData["port"] = []byte("5671") + } + + _, err := controllerutil.CreateOrPatch( + th.Ctx, th.K8sClient, secret, func() error { + secret.Data = secretData + return nil + }) + g.Expect(err).ShouldNot(HaveOccurred()) + }, th.Timeout, th.Interval).Should(Succeed()) +} + +// SimulateRabbitMQClusterReady function updates the RabbitMQCluster object +// status to have AllReplicasReady condition, statusDefaultUser reference +// and creates the secret referenced there containing host, password and user. +// +// Example usage: +// +// SimulateRabbitMQClusterReady(types.NamespacedName{Name: "test-mq", Namespace: "test-namespace"}) +func SimulateRabbitMQClusterReady(name types.NamespacedName) { + Eventually(func(g Gomega) { + secretName := types.NamespacedName{Name: name.Name + "-default-user", Namespace: namespace} + + mq := GetRabbitMQCluster(name) + g.Expect(mq).ToNot(BeNil()) + + // create/update rabbitmq secret + CreateOrUpdateRabbitMQClusterSecret(secretName, mq) + + raw := map[string]interface{}{ + "apiVersion": "rabbitmq.com/v1beta1", + "kind": "RabbitmqCluster", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + } + + status := make(map[string]interface{}) + + // add AllReplicasReady condition + statusCondition := []map[string]interface{}{ + { + "reason": "AllPodsAreReady", + "status": "True", + "type": "AllReplicasReady", + }, + } + + // add status.defaultUser which is used to get the + // secret holding username/password/host + statusDefaultUser := map[string]interface{}{ + "secretReference": map[string]interface{}{ + "keys": map[string]interface{}{ + "password": "password", + "username": "username", + }, + "name": secretName.Name, + "namespace": name.Namespace, + }, + "serviceReference": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + } + + status["conditions"] = statusCondition + status["defaultUser"] = statusDefaultUser + raw["status"] = status + + un := &unstructured.Unstructured{Object: raw} + deploymentRes := schema.GroupVersionResource{ + Group: "rabbitmq.com", + Version: "v1beta1", + Resource: "rabbitmqclusters", + } + + // Patch status + result, err := dynClient.Resource(deploymentRes).Namespace(namespace).ApplyStatus( + th.Ctx, name.Name, un, metav1.ApplyOptions{FieldManager: "application/apply-patch"}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).ToNot(BeNil()) + + mq = GetRabbitMQCluster(name) + g.Expect(mq.Status.Conditions).ToNot(BeNil()) + g.Expect(mq.Status.DefaultUser).ToNot(BeNil()) + + }, th.Timeout, th.Interval).Should(Succeed()) + th.Logger.Info("Simulated RabbitMQCluster ready", "on", name) +} + func GetDNSMasq(name types.NamespacedName) *networkv1.DNSMasq { instance := &networkv1.DNSMasq{} Eventually(func(g Gomega) { @@ -176,6 +370,14 @@ func GetReservation(name types.NamespacedName) *networkv1.Reservation { return instance } +func GetRabbitMQCluster(name types.NamespacedName) *rabbitmqclusterv1.RabbitmqCluster { + mq := &rabbitmqclusterv1.RabbitmqCluster{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, mq)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return mq +} + func DNSMasqConditionGetter(name types.NamespacedName) condition.Conditions { instance := GetDNSMasq(name) return instance.Status.Conditions @@ -191,6 +393,11 @@ func IPSetConditionGetter(name types.NamespacedName) condition.Conditions { return instance.Status.Conditions } +func TransportURLConditionGetter(name types.NamespacedName) condition.Conditions { + instance := infra.GetTransportURL(name) + return instance.Status.Conditions +} + func CreateLoadBalancerService(name types.NamespacedName, addDnsAnno bool) *corev1.Service { svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ diff --git a/tests/functional/suite_test.go b/tests/functional/suite_test.go index 13d39d8f..aba28545 100644 --- a/tests/functional/suite_test.go +++ b/tests/functional/suite_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "go.uber.org/zap/zapcore" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -36,8 +37,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" + rabbitmqclusterv1 "github.com/rabbitmq/cluster-operator/api/v1beta1" network_ctrl "github.com/openstack-k8s-operators/infra-operator/controllers/network" + rabbitmq_ctrl "github.com/openstack-k8s-operators/infra-operator/controllers/rabbitmq" + + infra_test "github.com/openstack-k8s-operators/infra-operator/apis/test/helpers" + test "github.com/openstack-k8s-operators/lib-common/modules/test" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" //+kubebuilder:scaffold:imports @@ -49,12 +56,14 @@ import ( var ( cfg *rest.Config k8sClient client.Client // You'll be using this client in your tests. + dynClient *dynamic.DynamicClient testEnv *envtest.Environment ctx context.Context cancel context.CancelFunc logger logr.Logger th *TestHelper namespace string + infra *infra_test.TestHelper ) func TestAPIs(t *testing.T) { @@ -70,10 +79,15 @@ var _ = BeforeSuite(func() { ctx, cancel = context.WithCancel(context.TODO()) + rabbitmqv2CRDs, err := test.GetCRDDirFromModule( + "github.com/rabbitmq/cluster-operator", "../../go.mod", "config/crd/bases") + Expect(err).ShouldNot(HaveOccurred()) + By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ filepath.Join("..", "..", "config", "crd", "bases"), + rabbitmqv2CRDs, }, ErrorIfCRDPathMissing: true, WebhookInstallOptions: envtest.WebhookInstallOptions{ @@ -86,7 +100,6 @@ var _ = BeforeSuite(func() { } // cfg is defined in this file globally. - var err error cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) @@ -97,6 +110,10 @@ var _ = BeforeSuite(func() { // in the test env. err = networkv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = rabbitmqv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = rabbitmqclusterv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme logger = ctrl.Log.WithName("---Test---") @@ -106,6 +123,8 @@ var _ = BeforeSuite(func() { Expect(k8sClient).NotTo(BeNil()) th = NewTestHelper(ctx, k8sClient, timeout, interval, logger) Expect(th).NotTo(BeNil()) + infra = infra_test.NewTestHelper(ctx, k8sClient, timeout, interval, logger) + Expect(infra).NotTo(BeNil()) // Start the controller-manager if goroutine webhookInstallOptions := &testEnv.WebhookInstallOptions @@ -125,6 +144,10 @@ var _ = BeforeSuite(func() { kclient, err := kubernetes.NewForConfig(cfg) Expect(err).ToNot(HaveOccurred(), "failed to create kclient") + dynClient, err = dynamic.NewForConfig(cfg) + Expect(err).NotTo(HaveOccurred()) + Expect(dynClient).NotTo(BeNil()) + err = (&networkv1.NetConfig{}).SetupWebhookWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) err = (&networkv1.IPSet{}).SetupWebhookWithManager(k8sManager) @@ -162,6 +185,13 @@ var _ = BeforeSuite(func() { }).SetupWithManager(context.Background(), k8sManager) Expect(err).ToNot(HaveOccurred()) + err = (&rabbitmq_ctrl.TransportURLReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) diff --git a/tests/functional/transporturl_controller_test.go b/tests/functional/transporturl_controller_test.go new file mode 100644 index 00000000..b263f750 --- /dev/null +++ b/tests/functional/transporturl_controller_test.go @@ -0,0 +1,182 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package functional_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" + corev1 "k8s.io/api/core/v1" + + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" + + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("TransportURL controller", func() { + var transportURLName types.NamespacedName + var rabbitmqClusterName types.NamespacedName + var transportURLSecretName types.NamespacedName + + BeforeEach(func() { + transportURLName = types.NamespacedName{ + Name: "foo", + Namespace: namespace, + } + rabbitmqClusterName = types.NamespacedName{ + Name: "rabbitmq", + Namespace: namespace, + } + transportURLSecretName = types.NamespacedName{ + Name: "rabbitmq-transport-url-" + transportURLName.Name, + Namespace: namespace, + } + }) + + When("a non TLS TransportURL gets created", func() { + BeforeEach(func() { + CreateRabbitMQCluster(rabbitmqClusterName, GetDefaultRabbitMQClusterSpec(false)) + DeferCleanup(DeleteRabbitMQCluster, rabbitmqClusterName) + + spec := map[string]interface{}{ + "rabbitmqClusterName": rabbitmqClusterName.Name, + } + DeferCleanup(th.DeleteInstance, CreateTransportURL(transportURLName, spec)) + }) + + It("should have the Spec fields set", func() { + tr := infra.GetTransportURL(transportURLName) + Expect(tr.Spec.RabbitmqClusterName).Should(Equal("rabbitmq")) + }) + + It("should have not ready Conditions initialized", func() { + th.ExpectCondition( + transportURLName, + ConditionGetterFunc(TransportURLConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + th.ExpectCondition( + transportURLName, + ConditionGetterFunc(TransportURLConditionGetter), + rabbitmqv1.TransportURLReadyCondition, + corev1.ConditionFalse, + ) + }) + + It("should create a secret holding the _NON_ TLS transport url when rabbitmq cluster is ready", func() { + SimulateRabbitMQClusterReady(rabbitmqClusterName) + + Eventually(func(g Gomega) { + s := th.GetSecret(transportURLSecretName) + g.Expect(s.Data).To(HaveLen(1)) + g.Expect(s.Data).To(HaveKeyWithValue("transport_url", []byte(fmt.Sprintf("rabbit://user:12345678@host.%s.svc:5672/?ssl=0", namespace)))) + + }, timeout, interval).Should(Succeed()) + + th.ExpectCondition( + transportURLName, + ConditionGetterFunc(TransportURLConditionGetter), + rabbitmqv1.TransportURLReadyCondition, + corev1.ConditionTrue, + ) + + tr := infra.GetTransportURL(transportURLName) + Expect(tr).ToNot(BeNil()) + Expect(tr.Status.SecretName).To(Equal(transportURLSecretName.Name)) + }) + }) + + When("a TLS TransportURL gets created", func() { + BeforeEach(func() { + CreateRabbitMQCluster(rabbitmqClusterName, GetDefaultRabbitMQClusterSpec(true)) + DeferCleanup(DeleteRabbitMQCluster, rabbitmqClusterName) + + spec := map[string]interface{}{ + "rabbitmqClusterName": rabbitmqClusterName.Name, + } + DeferCleanup(th.DeleteInstance, CreateTransportURL(transportURLName, spec)) + }) + + It("should create a secret holding the TLS transport url when rabbitmq cluster is ready", func() { + SimulateRabbitMQClusterReady(rabbitmqClusterName) + + Eventually(func(g Gomega) { + s := th.GetSecret(transportURLSecretName) + g.Expect(s.Data).To(HaveLen(1)) + g.Expect(s.Data).To(HaveKeyWithValue("transport_url", []byte(fmt.Sprintf("rabbit://user:12345678@host.%s.svc:5671/?ssl=1", namespace)))) + + }, timeout, interval).Should(Succeed()) + + th.ExpectCondition( + transportURLName, + ConditionGetterFunc(TransportURLConditionGetter), + rabbitmqv1.TransportURLReadyCondition, + corev1.ConditionTrue, + ) + + tr := infra.GetTransportURL(transportURLName) + Expect(tr).ToNot(BeNil()) + Expect(tr.Status.SecretName).To(Equal(transportURLSecretName.Name)) + }) + }) + + When("TLS gets enable for RabbitMQ, the TransportURL gets updated", func() { + BeforeEach(func() { + CreateRabbitMQCluster(rabbitmqClusterName, GetDefaultRabbitMQClusterSpec(false)) + DeferCleanup(DeleteRabbitMQCluster, rabbitmqClusterName) + + spec := map[string]interface{}{ + "rabbitmqClusterName": rabbitmqClusterName.Name, + } + DeferCleanup(th.DeleteInstance, CreateTransportURL(transportURLName, spec)) + }) + + It("should update the secret holding the TLS transport url", func() { + SimulateRabbitMQClusterReady(rabbitmqClusterName) + + // validate non tls transport_url + Eventually(func(g Gomega) { + s := th.GetSecret(transportURLSecretName) + g.Expect(s.Data).To(HaveLen(1)) + g.Expect(s.Data).To(HaveKeyWithValue("transport_url", []byte(fmt.Sprintf("rabbit://user:12345678@host.%s.svc:5672/?ssl=0", namespace)))) + + }, timeout, interval).Should(Succeed()) + + // update rabbitmq to be tls + UpdateRabbitMQClusterToTLS(rabbitmqClusterName) + SimulateRabbitMQClusterReady(rabbitmqClusterName) + + Eventually(func(g Gomega) { + s := th.GetSecret(transportURLSecretName) + g.Expect(s.Data).To(HaveLen(1)) + g.Expect(s.Data).To(HaveKeyWithValue("transport_url", []byte(fmt.Sprintf("rabbit://user:12345678@host.%s.svc:5671/?ssl=1", namespace)))) + + }, timeout, interval).Should(Succeed()) + + th.ExpectCondition( + transportURLName, + ConditionGetterFunc(TransportURLConditionGetter), + rabbitmqv1.TransportURLReadyCondition, + corev1.ConditionTrue, + ) + }) + }) +})