diff --git a/apis/apps/v2beta1/emqx_types.go b/apis/apps/v2beta1/emqx_types.go index 105754264..496859125 100644 --- a/apis/apps/v2beta1/emqx_types.go +++ b/apis/apps/v2beta1/emqx_types.go @@ -100,10 +100,22 @@ type EMQXSpec struct { type BootstrapAPIKey struct { // +kubebuilder:validation:Pattern:=`^[a-zA-Z\d-_]+$` - Key string `json:"key"` + Key string `json:"key,omitempty"` // +kubebuilder:validation:MinLength:=3 - // +kubebuilder:validation:MaxLength:=32 - Secret string `json:"secret"` + // +kubebuilder:validation:MaxLength:=128 + Secret string `json:"secret,omitempty"` + SecretRef *SecretRef `json:"secretRef,omitempty"` +} + +type SecretRef struct { + Key KeyRef `json:"key"` + Secret KeyRef `json:"secret"` +} + +type KeyRef struct { + SecretName string `json:"secretName"` + // +kubebuilder:validation:Pattern:=`^[a-zA-Z\d-_]+$` + SecretKey string `json:"secretKey"` } type Config struct { diff --git a/apis/apps/v2beta1/zz_generated.deepcopy.go b/apis/apps/v2beta1/zz_generated.deepcopy.go index d4b2d7491..524169422 100644 --- a/apis/apps/v2beta1/zz_generated.deepcopy.go +++ b/apis/apps/v2beta1/zz_generated.deepcopy.go @@ -30,6 +30,11 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BootstrapAPIKey) DeepCopyInto(out *BootstrapAPIKey) { *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(SecretRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapAPIKey. @@ -347,7 +352,9 @@ func (in *EMQXSpec) DeepCopyInto(out *EMQXSpec) { if in.BootstrapAPIKeys != nil { in, out := &in.BootstrapAPIKeys, &out.BootstrapAPIKeys *out = make([]BootstrapAPIKey, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.Config = in.Config if in.RevisionHistoryLimit != nil { @@ -444,6 +451,21 @@ func (in *EvacuationStrategy) DeepCopy() *EvacuationStrategy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyRef) DeepCopyInto(out *KeyRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyRef. +func (in *KeyRef) DeepCopy() *KeyRef { + if in == nil { + return nil + } + out := new(KeyRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeEvacuationStats) DeepCopyInto(out *NodeEvacuationStats) { *out = *in @@ -663,6 +685,23 @@ func (in *RebalanceStrategy) DeepCopy() *RebalanceStrategy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in + out.Key = in.Key + out.Secret = in.Secret +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTemplate) DeepCopyInto(out *ServiceTemplate) { *out = *in diff --git a/config/crd/bases/apps.emqx.io_emqxes.yaml b/config/crd/bases/apps.emqx.io_emqxes.yaml index 44b8ceee6..438e8c6ab 100644 --- a/config/crd/bases/apps.emqx.io_emqxes.yaml +++ b/config/crd/bases/apps.emqx.io_emqxes.yaml @@ -6489,12 +6489,37 @@ spec: pattern: ^[a-zA-Z\d-_]+$ type: string secret: - maxLength: 32 + maxLength: 128 minLength: 3 type: string - required: - - key - - secret + secretRef: + properties: + key: + properties: + secretKey: + pattern: ^[a-zA-Z\d-_]+$ + type: string + secretName: + type: string + required: + - secretKey + - secretName + type: object + secret: + properties: + secretKey: + pattern: ^[a-zA-Z\d-_]+$ + type: string + secretName: + type: string + required: + - secretKey + - secretName + type: object + required: + - key + - secret + type: object type: object type: array clusterDomain: diff --git a/controllers/apps/v2beta1/add_bootstrap_resource.go b/controllers/apps/v2beta1/add_bootstrap_resource.go index 1515db27a..787e0372f 100644 --- a/controllers/apps/v2beta1/add_bootstrap_resource.go +++ b/controllers/apps/v2beta1/add_bootstrap_resource.go @@ -7,8 +7,10 @@ import ( corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" appsv2beta1 "github.com/emqx/emqx-operator/apis/apps/v2beta1" innerReq "github.com/emqx/emqx-operator/internal/requester" @@ -23,7 +25,7 @@ type addBootstrap struct { func (a *addBootstrap) reconcile(ctx context.Context, instance *appsv2beta1.EMQX, _ innerReq.RequesterInterface) subResult { for _, resource := range []client.Object{ generateNodeCookieSecret(instance), - generateBootstrapAPIKeySecret(instance), + generateBootstrapAPIKeySecret(a.Client, ctx, instance), } { if err := ctrl.SetControllerReference(instance, resource, a.Scheme); err != nil { return subResult{err: emperror.Wrap(err, "failed to set controller reference")} @@ -63,12 +65,52 @@ func generateNodeCookieSecret(instance *appsv2beta1.EMQX) *corev1.Secret { } } -func generateBootstrapAPIKeySecret(instance *appsv2beta1.EMQX) *corev1.Secret { +// ReadSecret reads a secret from the Kubernetes cluster. +func ReadSecret(k8sClient client.Client, ctx context.Context, namespace string, name string, key string) (string, error) { + // Define a new Secret object + secret := &corev1.Secret{} + + // Define the Secret Name and Namespace + secretName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + // Use the client to fetch the Secret + if err := k8sClient.Get(ctx, secretName, secret); err != nil { + return "", err + } + + // secret.Data is a map[string][]byte + secretValue := string(secret.Data[key]) + + return secretValue, nil +} + +func generateBootstrapAPIKeySecret(k8sClient client.Client, ctx context.Context, instance *appsv2beta1.EMQX) *corev1.Secret { + logger := log.FromContext(ctx) bootstrapAPIKeys := "" + for _, apiKey := range instance.Spec.BootstrapAPIKeys { - bootstrapAPIKeys += apiKey.Key + ":" + apiKey.Secret + "\n" - } + if apiKey.SecretRef != nil { + logger.V(1).Info("Read SecretRef") + // Read key and secret values from the refenced secrets + keyValue, err := ReadSecret(k8sClient, ctx, instance.Namespace, apiKey.SecretRef.Key.SecretName, apiKey.SecretRef.Key.SecretKey) + if err != nil { + logger.V(1).Error(err, "read secretRef", "key") + continue + } + secretValue, err := ReadSecret(k8sClient, ctx, instance.Namespace, apiKey.SecretRef.Secret.SecretName, apiKey.SecretRef.Secret.SecretKey) + if err != nil { + logger.V(1).Error(err, "read secretRef", "secret") + continue + } + bootstrapAPIKeys += keyValue + ":" + secretValue + "\n" + } else { + bootstrapAPIKeys += apiKey.Key + ":" + apiKey.Secret + "\n" + } + } defPassword, _ := password.Generate(64, 10, 0, true, true) bootstrapAPIKeys += appsv2beta1.DefaultBootstrapAPIKey + ":" + defPassword diff --git a/controllers/apps/v2beta1/add_bootstrap_resource_suite_test.go b/controllers/apps/v2beta1/add_bootstrap_resource_suite_test.go new file mode 100644 index 000000000..f538c4bcf --- /dev/null +++ b/controllers/apps/v2beta1/add_bootstrap_resource_suite_test.go @@ -0,0 +1,169 @@ +package v2beta1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv2beta1 "github.com/emqx/emqx-operator/apis/apps/v2beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("AddBootstrap", Ordered, Label("bootstrap"), func() { + var ( + instance *appsv2beta1.EMQX + a *addBootstrap + ns *corev1.Namespace + ) + instance = new(appsv2beta1.EMQX) + ns = &corev1.Namespace{} + + BeforeEach(func() { + a = &addBootstrap{emqxReconciler} + ns = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "controller-v2beta1-add-emqx-bootstrap-test", + Labels: map[string]string{ + "test": "e2e", + }, + }, + } + instance = emqx.DeepCopy() + instance.Namespace = ns.Name + }) + + AfterEach(func() { + // Clean up bootstrap_api_key secret + bootstrapSecret := &corev1.Secret{} + err := k8sClient.Get(context.TODO(), client.ObjectKey{ + Namespace: ns.Name, + Name: instance.BootstrapAPIKeyNamespacedName().Name, + }, bootstrapSecret) + if err == nil { + // If the secret exists, delete it + Expect(k8sClient.Delete(context.TODO(), bootstrapSecret)).Should(Succeed()) + } else if !errors.IsNotFound(err) { + // If the error is not a NotFound error, fail the test + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("create namespace", func() { + Expect(k8sClient.Create(context.TODO(), ns)).Should(Succeed()) + }) + + It("should create bootstrap secrets", func() { + // Wait until the bootstrap secrets are created + // Call the reconciler. + result := a.reconcile(ctx, instance, nil) + + // Make sure there were no errors. + Expect(result.err).NotTo(HaveOccurred()) + // Check the created secrets. + cookieSecret := &corev1.Secret{} + err := k8sClient.Get(context.Background(), client.ObjectKey{ + Namespace: ns.Name, + Name: instance.NodeCookieNamespacedName().Name, + }, cookieSecret) + Expect(err).NotTo(HaveOccurred()) + Expect(cookieSecret.Data["node_cookie"]).ShouldNot(BeEmpty()) + + bootstrapSecret := &corev1.Secret{} + err = k8sClient.Get(context.Background(), client.ObjectKey{ + Namespace: ns.Name, + Name: instance.BootstrapAPIKeyNamespacedName().Name, + }, bootstrapSecret) + Expect(err).NotTo(HaveOccurred()) + Expect(bootstrapSecret.Data["bootstrap_api_key"]).ShouldNot(BeEmpty()) + }) + + It("should contain key and secret in bootstrap secret given initial values", func() { + // Given + instance.Spec.BootstrapAPIKeys = []appsv2beta1.BootstrapAPIKey{ + { + Key: "test_key", + Secret: "test_secret", + }, + } + + // Call the reconciler. + result := a.reconcile(ctx, instance, nil) + + // Make sure there were no errors. + Expect(result.err).NotTo(HaveOccurred()) + + // Check the created secrets. + bootstrapSecret := &corev1.Secret{} + err := k8sClient.Get(context.Background(), client.ObjectKey{ + Namespace: ns.Name, + Name: instance.BootstrapAPIKeyNamespacedName().Name, + }, bootstrapSecret) + Expect(err).NotTo(HaveOccurred()) + + // Verify that the bootstrap API key contains the initial key and secret. + Expect(string(bootstrapSecret.Data["bootstrap_api_key"])).Should(ContainSubstring("test_key:test_secret")) + }) + + It("should contain key and secret in bootstrap secret given SecretRef values", func() { + // Given + instance.Spec.BootstrapAPIKeys = []appsv2beta1.BootstrapAPIKey{ + { + SecretRef: &appsv2beta1.SecretRef{ + Key: appsv2beta1.KeyRef{ + // Note: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters + SecretName: "test-key-secret", + SecretKey: "key", + }, + Secret: appsv2beta1.KeyRef{ + SecretName: "test-value-secret", + SecretKey: "secret", + }, + }, + }, + } + + // Create referenced secrets + keySecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-key-secret", + }, + StringData: map[string]string{ + "key": "test_key", + }, + } + Expect(k8sClient.Create(context.TODO(), keySecret)).Should(Succeed()) + + secretSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-value-secret", + }, + StringData: map[string]string{ + "secret": "test_secret", + }, + } + Expect(k8sClient.Create(context.TODO(), secretSecret)).Should(Succeed()) + + // Call the reconciler. + result := a.reconcile(ctx, instance, nil) + + // Make sure there were no errors. + Expect(result.err).NotTo(HaveOccurred()) + + // Check the created secrets. + bootstrapSecret := &corev1.Secret{} + err := k8sClient.Get(context.Background(), client.ObjectKey{ + Namespace: ns.Name, + Name: instance.BootstrapAPIKeyNamespacedName().Name, + }, bootstrapSecret) + Expect(err).NotTo(HaveOccurred()) + + // Verify that the bootstrap API key contains the initial key and secret. + Expect(string(bootstrapSecret.Data["bootstrap_api_key"])).Should(ContainSubstring("test_key:test_secret")) + }) +}) diff --git a/controllers/apps/v2beta1/add_bootstrap_resource_test.go b/controllers/apps/v2beta1/add_bootstrap_resource_test.go index 2781bebf6..76d1dc951 100644 --- a/controllers/apps/v2beta1/add_bootstrap_resource_test.go +++ b/controllers/apps/v2beta1/add_bootstrap_resource_test.go @@ -1,12 +1,16 @@ package v2beta1 import ( + "context" "strings" "testing" appsv2beta1 "github.com/emqx/emqx-operator/apis/apps/v2beta1" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestGenerateNodeCookieSecret(t *testing.T) { @@ -35,6 +39,18 @@ func TestGenerateNodeCookieSecret(t *testing.T) { } func TestGenerateBootstrapAPIKeySecret(t *testing.T) { + // Create a fake client + scheme := runtime.NewScheme() + err := corev1.AddToScheme(scheme) + if err != nil { + t.Fatal(err) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + // Create a context + ctx := context.Background() + instance := &appsv2beta1.EMQX{ ObjectMeta: metav1.ObjectMeta{ Name: "emqx", @@ -44,21 +60,141 @@ func TestGenerateBootstrapAPIKeySecret(t *testing.T) { BootstrapAPIKeys: []appsv2beta1.BootstrapAPIKey{ { Key: "test_key", - Secret: "secret", + Secret: "test_secret", }, }, }, } - got := generateBootstrapAPIKeySecret(instance) + got := generateBootstrapAPIKeySecret(fakeClient, ctx, instance) assert.Equal(t, "emqx-bootstrap-api-key", got.Name) data, ok := got.StringData["bootstrap_api_key"] assert.True(t, ok) users := strings.Split(data, "\n") var usernames []string + var secrets []string for _, user := range users { usernames = append(usernames, user[:strings.Index(user, ":")]) + secrets = append(secrets, user[strings.Index(user, ":")+1:]) } assert.ElementsMatch(t, usernames, []string{appsv2beta1.DefaultBootstrapAPIKey, "test_key"}) + assert.Contains(t, secrets, "test_secret") +} + +func TestGenerateBootstrapAPIKeySecretWithSecretRef(t *testing.T) { + // Create a fake client + scheme := runtime.NewScheme() + err := corev1.AddToScheme(scheme) + if err != nil { + t.Fatal(err) + } + + keySecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-key-secret", + Namespace: "emqx", + }, + Data: map[string][]byte{ + "key": []byte("test_key"), + }, + } + valueSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-value-secret", + Namespace: "emqx", + }, + Data: map[string][]byte{ + "secret": []byte("test_secret"), + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + // Create a context + ctx := context.Background() + + // Add secrets to the fake client + err = fakeClient.Create(ctx, keySecret) + if err != nil { + t.Fatal(err) + } + err = fakeClient.Create(ctx, valueSecret) + if err != nil { + t.Fatal(err) + } + + instance := &appsv2beta1.EMQX{ + ObjectMeta: metav1.ObjectMeta{ + Name: "emqx", + Namespace: "emqx", + }, + Spec: appsv2beta1.EMQXSpec{ + BootstrapAPIKeys: []appsv2beta1.BootstrapAPIKey{ + { + SecretRef: &appsv2beta1.SecretRef{ + Key: appsv2beta1.KeyRef{ + SecretName: "test-key-secret", + SecretKey: "key", + }, + Secret: appsv2beta1.KeyRef{ + SecretName: "test-value-secret", + SecretKey: "secret", + }, + }, + }, + }, + }, + } + + got := generateBootstrapAPIKeySecret(fakeClient, ctx, instance) + assert.Equal(t, "emqx-bootstrap-api-key", got.Name) + data, ok := got.StringData["bootstrap_api_key"] + assert.True(t, ok) + + users := strings.Split(data, "\n") + var usernames []string + var secrets []string + for _, user := range users { + usernames = append(usernames, user[:strings.Index(user, ":")]) + secrets = append(secrets, user[strings.Index(user, ":")+1:]) + } + assert.ElementsMatch(t, usernames, []string{appsv2beta1.DefaultBootstrapAPIKey, "test_key"}) + assert.Contains(t, secrets, "test_secret") +} + +func TestReadSecret(t *testing.T) { + // Create a fake client + scheme := runtime.NewScheme() + err := corev1.AddToScheme(scheme) + if err != nil { + t.Fatal(err) + } + + // Define the secret data + secretData := map[string][]byte{ + "key": []byte("value"), + } + + // Create a secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + }, + Data: secretData, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + + // Create a context + ctx := context.Background() + + val, err := ReadSecret(fakeClient, ctx, "default", "test-secret", "key") + if err != nil { + t.Fatal(err) + } + + // Check the secret value + assert.Equal(t, "value", val) } diff --git a/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml b/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml index 2e22df9c6..a449d23f2 100644 --- a/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml +++ b/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml @@ -6501,12 +6501,37 @@ spec: pattern: ^[a-zA-Z\d-_]+$ type: string secret: - maxLength: 32 + maxLength: 128 minLength: 3 type: string - required: - - key - - secret + secretRef: + properties: + key: + properties: + secretKey: + pattern: ^[a-zA-Z\d-_]+$ + type: string + secretName: + type: string + required: + - secretKey + - secretName + type: object + secret: + properties: + secretKey: + pattern: ^[a-zA-Z\d-_]+$ + type: string + secretName: + type: string + required: + - secretKey + - secretName + type: object + required: + - key + - secret + type: object type: object type: array clusterDomain: diff --git a/docs/en_US/reference/v2alpha2-reference.md b/docs/en_US/reference/v2alpha2-reference.md new file mode 100644 index 000000000..4d7ef43a9 --- /dev/null +++ b/docs/en_US/reference/v2alpha2-reference.md @@ -0,0 +1,4 @@ +# API Reference + +## Packages + diff --git a/docs/en_US/reference/v2beta1-reference.md b/docs/en_US/reference/v2beta1-reference.md index 90a247fcc..5496d326d 100644 --- a/docs/en_US/reference/v2beta1-reference.md +++ b/docs/en_US/reference/v2beta1-reference.md @@ -29,6 +29,7 @@ _Appears in:_ | --- | --- | | `key` _string_ | | | `secret` _string_ | | +| `secretRef` _[SecretRef](#secretref)_ | | #### Config @@ -287,6 +288,17 @@ _Appears in:_ | `sessEvictRate` _integer_ | Just work in EMQX Enterprise. | +#### KeyRef + +_Underlying type:_ _[struct{SecretName string "json:\"secretName\""; SecretKey string "json:\"secretKey\""}](#struct{secretname-string-"json:\"secretname\"";-secretkey-string-"json:\"secretkey\""})_ + + + +_Appears in:_ +- [SecretRef](#secretref) + + + #### NodeEvacuationStats @@ -475,6 +487,21 @@ _Appears in:_ | `relSessThreshold` _string_ | RelSessThreshold represents the relative threshold for checking session connection balance. same to rel-sess-threshold in [EMQX Rebalancing](https://docs.emqx.com/en/enterprise/v4.4/advanced/rebalancing.html#rebalancing) the usage of float highly discouraged, as support for them varies across languages. So we define the RelSessThreshold field as string type and you not float type The value must be greater than "1.0" Defaults to "1.1". | +#### SecretRef + + + + + +_Appears in:_ +- [BootstrapAPIKey](#bootstrapapikey) + +| Field | Description | +| --- | --- | +| `key` _[KeyRef](#keyref)_ | | +| `secret` _[KeyRef](#keyref)_ | | + + #### ServiceTemplate diff --git a/docs/zh_CN/reference/v2alpha2-reference.md b/docs/zh_CN/reference/v2alpha2-reference.md new file mode 100644 index 000000000..4d7ef43a9 --- /dev/null +++ b/docs/zh_CN/reference/v2alpha2-reference.md @@ -0,0 +1,4 @@ +# API Reference + +## Packages + diff --git a/docs/zh_CN/reference/v2beta1-reference.md b/docs/zh_CN/reference/v2beta1-reference.md index 90a247fcc..5496d326d 100644 --- a/docs/zh_CN/reference/v2beta1-reference.md +++ b/docs/zh_CN/reference/v2beta1-reference.md @@ -29,6 +29,7 @@ _Appears in:_ | --- | --- | | `key` _string_ | | | `secret` _string_ | | +| `secretRef` _[SecretRef](#secretref)_ | | #### Config @@ -287,6 +288,17 @@ _Appears in:_ | `sessEvictRate` _integer_ | Just work in EMQX Enterprise. | +#### KeyRef + +_Underlying type:_ _[struct{SecretName string "json:\"secretName\""; SecretKey string "json:\"secretKey\""}](#struct{secretname-string-"json:\"secretname\"";-secretkey-string-"json:\"secretkey\""})_ + + + +_Appears in:_ +- [SecretRef](#secretref) + + + #### NodeEvacuationStats @@ -475,6 +487,21 @@ _Appears in:_ | `relSessThreshold` _string_ | RelSessThreshold represents the relative threshold for checking session connection balance. same to rel-sess-threshold in [EMQX Rebalancing](https://docs.emqx.com/en/enterprise/v4.4/advanced/rebalancing.html#rebalancing) the usage of float highly discouraged, as support for them varies across languages. So we define the RelSessThreshold field as string type and you not float type The value must be greater than "1.0" Defaults to "1.1". | +#### SecretRef + + + + + +_Appears in:_ +- [BootstrapAPIKey](#bootstrapapikey) + +| Field | Description | +| --- | --- | +| `key` _[KeyRef](#keyref)_ | | +| `secret` _[KeyRef](#keyref)_ | | + + #### ServiceTemplate