diff --git a/apis/synapse/v1alpha1/synapse_types.go b/apis/synapse/v1alpha1/synapse_types.go index 14432c3..5973671 100644 --- a/apis/synapse/v1alpha1/synapse_types.go +++ b/apis/synapse/v1alpha1/synapse_types.go @@ -89,6 +89,15 @@ type SynapseBridges struct { // * enable the bridge and specify an existing ConfigMap by its Name and // Namespace containing a heisenbridge.yaml. Heisenbridge SynapseHeisenbridge `json:"heisenbridge,omitempty"` + + // Configuration options for the mautrix-signal bridge. The user can + // either: + // * disable the deployment of the bridge. + // * enable the bridge, without specifying additional configuration + // options. The bridge will be deployed with a default configuration. + // * enable the bridge and specify an existing ConfigMap by its Name and + // Namespace containing a config.yaml file. + MautrixSignal SynapseMautrixSignal `json:"mautrixSignal,omitempty"` } type SynapseHeisenbridge struct { @@ -123,6 +132,29 @@ type SynapseHeisenbridgeConfigMap struct { Namespace string `json:"namespace,omitempty"` } +type SynapseMautrixSignal struct { + // +kubebuilder:default:=false + + // Whether to deploy mautrix-signal or not + Enabled bool `json:"enabled,omitempty"` + + // Holds information about the ConfigMap containing the config.yaml + // configuration file to be used as input for the configuration of the + // mautrix-signal Bridge. + ConfigMap SynapseMautrixSignalConfigMap `json:"configMap,omitempty"` +} + +type SynapseMautrixSignalConfigMap struct { + // +kubebuilder:validation:Required + + // Name of the ConfigMap in the given Namespace. + Name string `json:"name"` + + // Namespace in which the ConfigMap is living. If left empty, the Synapse + // namespace is used. + Namespace string `json:"namespace,omitempty"` +} + // SynapseStatus defines the observed state of Synapse type SynapseStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster @@ -150,6 +182,9 @@ type SynapseStatus struct { type SynapseStatusBridgesConfiguration struct { // Status of the Heisenbridge Heisenbridge SynapseStatusHeisenbridge `json:"heisenbridge,omitempty"` + + // Status of the mautrix-signal bridge + MautrixSignal SynapseStatusMautrixSignal `json:"mautrixSignal,omitempty"` } type SynapseStatusHeisenbridge struct { @@ -157,6 +192,11 @@ type SynapseStatusHeisenbridge struct { IP string `json:"ip,omitempty"` } +type SynapseStatusMautrixSignal struct { + // IP at which the mautrix-signal bridge is available + IP string `json:"ip,omitempty"` +} + type SynapseStatusDatabaseConnectionInfo struct { // Endpoint to connect to the PostgreSQL database ConnectionURL string `json:"connectionURL,omitempty"` diff --git a/apis/synapse/v1alpha1/zz_generated.deepcopy.go b/apis/synapse/v1alpha1/zz_generated.deepcopy.go index 2d78191..4aabf88 100644 --- a/apis/synapse/v1alpha1/zz_generated.deepcopy.go +++ b/apis/synapse/v1alpha1/zz_generated.deepcopy.go @@ -56,6 +56,7 @@ func (in *Synapse) DeepCopyObject() runtime.Object { func (in *SynapseBridges) DeepCopyInto(out *SynapseBridges) { *out = *in out.Heisenbridge = in.Heisenbridge + out.MautrixSignal = in.MautrixSignal } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SynapseBridges. @@ -186,6 +187,37 @@ func (in *SynapseList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SynapseMautrixSignal) DeepCopyInto(out *SynapseMautrixSignal) { + *out = *in + out.ConfigMap = in.ConfigMap +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SynapseMautrixSignal. +func (in *SynapseMautrixSignal) DeepCopy() *SynapseMautrixSignal { + if in == nil { + return nil + } + out := new(SynapseMautrixSignal) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SynapseMautrixSignalConfigMap) DeepCopyInto(out *SynapseMautrixSignalConfigMap) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SynapseMautrixSignalConfigMap. +func (in *SynapseMautrixSignalConfigMap) DeepCopy() *SynapseMautrixSignalConfigMap { + if in == nil { + return nil + } + out := new(SynapseMautrixSignalConfigMap) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SynapseSpec) DeepCopyInto(out *SynapseSpec) { *out = *in @@ -225,6 +257,7 @@ func (in *SynapseStatus) DeepCopy() *SynapseStatus { func (in *SynapseStatusBridgesConfiguration) DeepCopyInto(out *SynapseStatusBridgesConfiguration) { *out = *in out.Heisenbridge = in.Heisenbridge + out.MautrixSignal = in.MautrixSignal } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SynapseStatusBridgesConfiguration. @@ -281,3 +314,18 @@ func (in *SynapseStatusHomeserverConfiguration) DeepCopy() *SynapseStatusHomeser in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SynapseStatusMautrixSignal) DeepCopyInto(out *SynapseStatusMautrixSignal) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SynapseStatusMautrixSignal. +func (in *SynapseStatusMautrixSignal) DeepCopy() *SynapseStatusMautrixSignal { + if in == nil { + return nil + } + out := new(SynapseStatusMautrixSignal) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/synapse.opdev.io_synapses.yaml b/bundle/manifests/synapse.opdev.io_synapses.yaml index e032900..88312d5 100644 --- a/bundle/manifests/synapse.opdev.io_synapses.yaml +++ b/bundle/manifests/synapse.opdev.io_synapses.yaml @@ -71,6 +71,34 @@ spec: "-v" * 2 corresponds to "-vv" * 3 corresponds to "-vvv"' type: integer type: object + mautrixSignal: + description: 'Configuration options for the mautrix-signal bridge. + The user can either: * disable the deployment of the bridge. + * enable the bridge, without specifying additional configuration options. + The bridge will be deployed with a default configuration. * + enable the bridge and specify an existing ConfigMap by its Name + and Namespace containing a config.yaml file.' + properties: + configMap: + description: Holds information about the ConfigMap containing + the config.yaml configuration file to be used as input for + the configuration of the mautrix-signal Bridge. + properties: + name: + description: Name of the ConfigMap in the given Namespace. + type: string + namespace: + description: Namespace in which the ConfigMap is living. + If left empty, the Synapse namespace is used. + type: string + required: + - name + type: object + enabled: + default: false + description: Whether to deploy mautrix-signal or not + type: boolean + type: object type: object createNewPostgreSQL: default: false @@ -135,6 +163,13 @@ spec: description: IP at which the Heisenbridge is available type: string type: object + mautrixSignal: + description: Status of the mautrix-signal bridge + properties: + ip: + description: IP at which the mautrix-signal bridge is available + type: string + type: object type: object databaseConnectionInfo: description: Connection information to the external PostgreSQL Database diff --git a/config/crd/bases/synapse.opdev.io_synapses.yaml b/config/crd/bases/synapse.opdev.io_synapses.yaml index e41f51d..d626c61 100644 --- a/config/crd/bases/synapse.opdev.io_synapses.yaml +++ b/config/crd/bases/synapse.opdev.io_synapses.yaml @@ -73,6 +73,34 @@ spec: "-v" * 2 corresponds to "-vv" * 3 corresponds to "-vvv"' type: integer type: object + mautrixSignal: + description: 'Configuration options for the mautrix-signal bridge. + The user can either: * disable the deployment of the bridge. + * enable the bridge, without specifying additional configuration options. + The bridge will be deployed with a default configuration. * + enable the bridge and specify an existing ConfigMap by its Name + and Namespace containing a config.yaml file.' + properties: + configMap: + description: Holds information about the ConfigMap containing + the config.yaml configuration file to be used as input for + the configuration of the mautrix-signal Bridge. + properties: + name: + description: Name of the ConfigMap in the given Namespace. + type: string + namespace: + description: Namespace in which the ConfigMap is living. + If left empty, the Synapse namespace is used. + type: string + required: + - name + type: object + enabled: + default: false + description: Whether to deploy mautrix-signal or not + type: boolean + type: object type: object createNewPostgreSQL: default: false @@ -132,6 +160,13 @@ spec: description: IP at which the Heisenbridge is available type: string type: object + mautrixSignal: + description: Status of the mautrix-signal bridge + properties: + ip: + description: IP at which the mautrix-signal bridge is available + type: string + type: object type: object databaseConnectionInfo: description: Connection information to the external PostgreSQL Database diff --git a/controllers/synapse/reconcile.go b/controllers/synapse/reconcile.go index 34e6db7..deb0c65 100644 --- a/controllers/synapse/reconcile.go +++ b/controllers/synapse/reconcile.go @@ -34,7 +34,7 @@ func (r *SynapseReconciler) reconcileResource( log := ctrllog.FromContext(ctx) log.Info( "Reconciling resource", - "Kind", resource.GetObjectKind(), + "Kind", resource.GetObjectKind().GroupVersionKind().Kind, "Name", objectMeta.Name, "Namespace", objectMeta.Namespace, ) @@ -43,7 +43,7 @@ func (r *SynapseReconciler) reconcileResource( if k8serrors.IsNotFound(err) { log.Info( "Creating a new resource for Synapse", - "Kind", resource.GetObjectKind(), + "Kind", resource.GetObjectKind().GroupVersionKind().Kind, "Name", objectMeta.Name, "Namespace", objectMeta.Namespace, ) @@ -53,7 +53,7 @@ func (r *SynapseReconciler) reconcileResource( log.Error( err, "Failed to generate a new resource for Synapse", - "Kind", resource.GetObjectKind(), + "Kind", resource.GetObjectKind().GroupVersionKind().Kind, "Name", objectMeta.Name, "Namespace", objectMeta.Namespace, ) @@ -65,7 +65,7 @@ func (r *SynapseReconciler) reconcileResource( log.Error( err, "Failed to create a new resource for Synapse", - "Kind", resource.GetObjectKind(), + "Kind", resource.GetObjectKind().GroupVersionKind().Kind, "Name", objectMeta.Name, "Namespace", objectMeta.Namespace, ) @@ -78,7 +78,7 @@ func (r *SynapseReconciler) reconcileResource( log.Error( err, "Error reading resource", - "Kind", resource.GetObjectKind(), + "Kind", resource.GetObjectKind().GroupVersionKind().Kind, "Name", objectMeta.Name, "Namespace", objectMeta.Namespace, ) @@ -86,8 +86,8 @@ func (r *SynapseReconciler) reconcileResource( } log.Info( - "Reconciling resource", - "Kind", resource.GetObjectKind(), + "Finished reconciling resource", + "Kind", resource.GetObjectKind().GroupVersionKind().Kind, "Name", objectMeta.Name, "Namespace", objectMeta.Namespace, ) diff --git a/controllers/synapse/synapse_configmap.go b/controllers/synapse/synapse_configmap.go index 1f7c623..741b171 100644 --- a/controllers/synapse/synapse_configmap.go +++ b/controllers/synapse/synapse_configmap.go @@ -799,7 +799,7 @@ database: # A yaml python logging config file as described by # https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema # -log_config: "/data/example.com.log.config" +log_config: "/data/` + s.Spec.Homeserver.Values.ServerName + `.log.config" ## Ratelimiting ## @@ -1461,7 +1461,7 @@ form_secret: "uD#~UE2pAzLUQIvj8x1;0iCzNL-UcUs1._WtUGXHRp@1Ogmyg4" # Path to the signing key to sign messages with # -signing_key_path: "data/example.com.signing.key" +signing_key_path: "data/` + s.Spec.Homeserver.Values.ServerName + `.signing.key" # The keys that the server used to sign messages with but won't use # to sign new messages. @@ -2849,3 +2849,16 @@ func (r *SynapseReconciler) updateHomeserverWithHeisenbridgeInfos( homeserver["app_service_config_files"] = []string{"/data-heisenbridge/heisenbridge.yaml"} return nil } + +// updateHomeserverWithMautrixSignalInfos is a function of type updateDataFunc +// function to be passed as an argument in a call to updateConfigMap. +// +// It enables the mautrix-signal bridge as an AppService in Synapse. +func (r *SynapseReconciler) updateHomeserverWithMautrixSignalInfos( + s synapsev1alpha1.Synapse, + homeserver map[string]interface{}, +) error { + // Add mautrix-signal configuration file to the list of application services + homeserver["app_service_config_files"] = []string{"/data-mautrixsignal/registration.yaml"} + return nil +} diff --git a/controllers/synapse/synapse_controller.go b/controllers/synapse/synapse_controller.go index d9afc0f..3d042e9 100644 --- a/controllers/synapse/synapse_controller.go +++ b/controllers/synapse/synapse_controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" "reflect" + "strings" "time" appsv1 "k8s.io/api/apps/v1" @@ -62,6 +63,18 @@ type HomeserverPgsqlDatabase struct { //+kubebuilder:rbac:groups=synapse.opdev.io,resources=synapses/status,verbs=get;update;patch //+kubebuilder:rbac:groups=synapse.opdev.io,resources=synapses/finalizers,verbs=update +func (r *SynapseReconciler) GetHeisenbridgeResourceName(synapse synapsev1alpha1.Synapse) string { + return strings.Join([]string{synapse.Name, "heisenbridge"}, "-") +} + +func (r *SynapseReconciler) GetSignaldResourceName(synapse synapsev1alpha1.Synapse) string { + return strings.Join([]string{synapse.Name, "signald"}, "-") +} + +func (r *SynapseReconciler) GetMautrixSignalResourceName(synapse synapsev1alpha1.Synapse) string { + return strings.Join([]string{synapse.Name, "mautrixsignal"}, "-") +} + // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // @@ -221,8 +234,8 @@ func (r *SynapseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // Heisenbridge is composed of a ConfigMap, a Service and a Deployment. // Resources associated to the Heisenbridge are append with "-heisenbridge" createdHeisenbridgeService := &corev1.Service{} - objectMetaHeisenbridge := setObjectMeta(synapse.Name+"-heisenbridge", synapse.Namespace, map[string]string{}) - heisenbergKey := types.NamespacedName{Name: synapse.Name + "-heisenbridge", Namespace: synapse.Namespace} + objectMetaHeisenbridge := setObjectMeta(r.GetHeisenbridgeResourceName(synapse), synapse.Namespace, map[string]string{}) + heisenbridgeKey := types.NamespacedName{Name: r.GetHeisenbridgeResourceName(synapse), Namespace: synapse.Namespace} // First create the service as we need its IP address for the // heisenbridge.yaml configuration file @@ -237,7 +250,7 @@ func (r *SynapseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } // Get Service IP and update the Synapse status - heisenbridgeIP, err := r.getServiceIP(ctx, heisenbergKey, createdHeisenbridgeService) + heisenbridgeIP, err := r.getServiceIP(ctx, heisenbridgeKey, createdHeisenbridgeService) if err != nil { return ctrl.Result{}, err } @@ -348,6 +361,206 @@ func (r *SynapseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } } + if synapse.Spec.Bridges.MautrixSignal.Enabled { + log.Info("mautrix-signal is enabled - deploying mautrix-signal") + // mautrix-signal is composed of a ConfigMap, a Service and 1 Deployment. + // Resources associated to the mautrix-signal are append with "-mautrixsignal" + // In addition, a second deployment is needed to run signald. This is append with "-signald" + createdMautrixSignalService := &corev1.Service{} + objectMetaMautrixSignal := setObjectMeta(r.GetMautrixSignalResourceName(synapse), synapse.Namespace, map[string]string{}) + mautrixSignalKey := types.NamespacedName{Name: r.GetMautrixSignalResourceName(synapse), Namespace: synapse.Namespace} + objectMetaSignald := setObjectMeta(r.GetSignaldResourceName(synapse), synapse.Namespace, map[string]string{}) + + // First create the service as we need its IP address for the + // config.yaml configuration file + if err := r.reconcileResource( + ctx, + r.serviceForMautrixSignal, + &synapse, + createdMautrixSignalService, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + + // Get Service IP and update the Synapse status + mautrixSignalIP, err := r.getServiceIP(ctx, mautrixSignalKey, createdMautrixSignalService) + if err != nil { + return ctrl.Result{}, err + } + synapse.Status.BridgesConfiguration.MautrixSignal.IP = mautrixSignalIP + if err := r.updateSynapseStatus(ctx, &synapse); err != nil { + return ctrl.Result{}, err + } + + // The ConfigMap for mautrix-signal, containing the config.yaml config + // file. It's either a copy of a user-provided ConfigMap, if defined in + // Spec.Bridges.MautrixSignal.ConfigMap, or a new ConfigMap containing + // a default config.yaml. + createdMautrixSignalConfigMap := &corev1.ConfigMap{} + + // The user may specify a ConfigMap, containing the config.yaml config + // file, under Spec.Bridges.MautrixSignal.ConfigMap + inputConfigMapName := synapse.Spec.Bridges.MautrixSignal.ConfigMap.Name + setConfigMapNamespace := synapse.Spec.Bridges.MautrixSignal.ConfigMap.Namespace + inputConfigMapNamespace := r.getConfigMapNamespace(synapse, setConfigMapNamespace) + if inputConfigMapName != "" { + // If the user provided a custom mautrix-signal configuration via a + // ConfigMap, we need to validate that the ConfigMap exists, and + // create a copy in createdMautrixSignalConfigMap + + // Get and check the input ConfigMap for mautrix-signal + var inputMautrixSignalConfigMap = &corev1.ConfigMap{} + keyForConfigMap := types.NamespacedName{ + Name: inputConfigMapName, + Namespace: inputConfigMapNamespace, + } + + if err := r.Get(ctx, keyForConfigMap, inputMautrixSignalConfigMap); err != nil { + reason := "ConfigMap " + inputConfigMapName + " does not exist in namespace " + inputConfigMapNamespace + if err := r.setFailedState(ctx, &synapse, reason); err != nil { + log.Error(err, "Error updating Synapse State") + } + + log.Error( + err, + "Failed to get ConfigMap", + "ConfigMap.Namespace", + inputConfigMapNamespace, + "ConfigMap.Name", + inputConfigMapName, + ) + return ctrl.Result{RequeueAfter: time.Duration(30)}, err + } + + // Create a copy of the inputMautrixSignalConfigMap defined in Spec.Bridges.MautrixSignal.ConfigMap + // Here we use the createdMautrixSignalConfigMap function as createResourceFunc + if err := r.reconcileResource( + ctx, + r.configMapForMautrixSignalCopy, + &synapse, + createdMautrixSignalConfigMap, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + + // Correct data in mautrix-signal ConfigMap + if err := r.updateConfigMap( + ctx, + createdMautrixSignalConfigMap, + synapse, + r.updateMautrixSignalData, + "config.yaml", + ); err != nil { + return ctrl.Result{}, err + } + } else { + // If the user hasn't provided a ConfigMap with a custom + // config.yaml, we create a new ConfigMap with a default + // config.yaml. + + // Here we use configMapForMautrixSignal as createResourceFunc + if err := r.reconcileResource( + ctx, + r.configMapForMautrixSignal, + &synapse, + &corev1.ConfigMap{}, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + } + + // Create a PVC for signald + if err := r.reconcileResource( + ctx, + r.persistentVolumeClaimForSignald, + &synapse, + &corev1.PersistentVolumeClaim{}, + objectMetaSignald, + ); err != nil { + return ctrl.Result{}, err + } + + // Create Deployment for signald + if err := r.reconcileResource( + ctx, + r.deploymentForSignald, + &synapse, + &appsv1.Deployment{}, + objectMetaSignald, + ); err != nil { + return ctrl.Result{}, err + } + + // Create the SA for mautrix-signal + if err := r.reconcileResource( + ctx, + r.serviceAccountForMautrixSignal, + &synapse, + &corev1.ServiceAccount{}, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + + // Create the RoleBinding for mautrix-signal + if err := r.reconcileResource( + ctx, + r.roleBindingForMautrixSignal, + &synapse, + &rbacv1.RoleBinding{}, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + + // Create a PVC for mautrix-signal + if err := r.reconcileResource( + ctx, + r.persistentVolumeClaimForMautrixSignal, + &synapse, + &corev1.PersistentVolumeClaim{}, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + + // Create Deployment for mautrix-signal + if err := r.reconcileResource( + ctx, + r.deploymentForMautrixSignal, + &synapse, + &appsv1.Deployment{}, + objectMetaMautrixSignal, + ); err != nil { + return ctrl.Result{}, err + } + + // Create Deployment for mautrix-signal + if err := r.reconcileResource( + ctx, + r.deploymentForSignald, + &synapse, + &appsv1.Deployment{}, + objectMetaSignald, + ); err != nil { + return ctrl.Result{}, err + } + + // Update the Synapse ConfigMap to enable mautrix-signal + if err := r.updateConfigMap( + ctx, + &createdConfigMap, + synapse, + r.updateHomeserverWithMautrixSignalInfos, + "homeserver.yaml", + ); err != nil { + return ctrl.Result{}, err + } + } + // Reconcile Synapse resources: PVC, Deployment and Service if err := r.reconcileResource( ctx, diff --git a/controllers/synapse/synapse_controller_test.go b/controllers/synapse/synapse_controller_test.go index 57e0eae..6f8c311 100644 --- a/controllers/synapse/synapse_controller_test.go +++ b/controllers/synapse/synapse_controller_test.go @@ -30,28 +30,6 @@ import ( synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" ) -// Helper function for struct construction requiring a boolean pointer -func BoolAddr(b bool) *bool { - boolVar := b - return &boolVar -} - -func convert(i interface{}) interface{} { - switch x := i.(type) { - case map[interface{}]interface{}: - m2 := map[string]interface{}{} - for k, v := range x { - m2[k.(string)] = convert(v) - } - return m2 - case []interface{}: - for i, v := range x { - x[i] = convert(v) - } - } - return i -} - var _ = Describe("Integration tests for the Synapse controller", Ordered, Label("integration"), func() { // Define utility constants for object names and testing timeouts/durations and intervals. const ( @@ -71,6 +49,10 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( var ctx context.Context var cancel context.CancelFunc + var deleteResource func(client.Object, types.NamespacedName, bool) + var checkSubresourceAbsence func(string) + var checkResourcePresence func(client.Object, types.NamespacedName, metav1.OwnerReference) + // Common function to start envTest var startenvTest = func() { cfg, err := testEnv.Start() @@ -97,79 +79,18 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) + deleteResource = deleteResourceFunc(k8sClient, ctx, timeout, interval) + checkSubresourceAbsence = checkSubresourceAbsenceFunc(SynapseName, SynapseNamespace, k8sClient, ctx, timeout, interval) + checkResourcePresence = checkResourcePresenceFunc(k8sClient, ctx, timeout, interval) + go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).ToNot(HaveOccurred(), "failed to run manager") }() } - // Verify the absence of Synapse sub-resources - // This function common to multiple tests - var checkSubresourceAbsence = func(expectedReason string) { - s := &synapsev1alpha1.Synapse{} - synapseLookupKey := types.NamespacedName{Name: SynapseName, Namespace: SynapseNamespace} - expectedState := "FAILED" - - By("Verifying that the Synapse object was created") - Eventually(func() bool { - err := k8sClient.Get(ctx, synapseLookupKey, &synapsev1alpha1.Synapse{}) - return err == nil - }, timeout, interval).Should(BeTrue()) - - By("Checking the Synapse status") - // Status may need some time to be updated - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, synapseLookupKey, s)).Should(Succeed()) - g.Expect(s.Status.State).To(Equal(expectedState)) - g.Expect(s.Status.Reason).To(Equal(expectedReason)) - }, timeout, interval).Should(Succeed()) - - By("Checking that synapse sub-resources have not been created") - Consistently(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, synapseLookupKey, &corev1.ServiceAccount{})).ShouldNot(Succeed()) - g.Expect(k8sClient.Get(ctx, synapseLookupKey, &rbacv1.RoleBinding{})).ShouldNot(Succeed()) - g.Expect(k8sClient.Get(ctx, synapseLookupKey, &corev1.PersistentVolumeClaim{})).ShouldNot(Succeed()) - g.Expect(k8sClient.Get(ctx, synapseLookupKey, &appsv1.Deployment{})).ShouldNot(Succeed()) - g.Expect(k8sClient.Get(ctx, synapseLookupKey, &corev1.Service{})).ShouldNot(Succeed()) - }, timeout, interval).Should(Succeed()) - } - - var checkResourcePresence = func(resource client.Object, lookupKey types.NamespacedName, expectedOwnerReference metav1.OwnerReference) { - Eventually(func() bool { - err := k8sClient.Get(ctx, lookupKey, resource) - return err == nil - }, timeout, interval).Should(BeTrue()) - - Expect(resource.GetOwnerReferences()).To(ContainElement(expectedOwnerReference)) - } - - var deleteResource = func(resource client.Object, lookupKey types.NamespacedName, removeFinalizers bool) { - // Using 'Eventually' to eliminate race conditions where the Synapse - // Operator didn't have time to create a sub resource. - Eventually(func() bool { - err := k8sClient.Get(ctx, lookupKey, resource) - return err == nil - }, timeout, interval).Should(BeTrue()) - - if removeFinalizers { - // Manually remove the finalizers - resource.SetFinalizers([]string{}) - Expect(k8sClient.Update(ctx, resource)).Should(Succeed()) - } - - // Deleting - Expect(k8sClient.Delete(ctx, resource)).Should(Succeed()) - - // Check that the resource was successfully removed - Eventually(func() bool { - err := k8sClient.Get(ctx, lookupKey, resource) - return err == nil - }, timeout, interval).Should(BeFalse()) - } - Context("When a corectly configured Kubernetes cluster is present", func() { var _ = BeforeAll(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) @@ -316,6 +237,24 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( }, }, }), + Entry("when mautrix-signal ConfigMap doesn't possess a name", map[string]interface{}{ + "spec": map[string]interface{}{ + "homeserver": map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": InputConfigMapName, + "namespace": SynapseNamespace, + }, + }, + "bridges": map[string]interface{}{ + "mautrixSignal": map[string]interface{}{ + "enabled": false, + "configMap": map[string]interface{}{ + "namespace": "random-namespace", + }, + }, + }, + }, + }), // This should not work but passes PEntry("when Synapse spec possesses an invalid field", map[string]interface{}{ "spec": map[string]interface{}{ @@ -419,6 +358,43 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( }, }, ), + Entry( + "when optional mautrix-signal bridge is enabled", + map[string]interface{}{ + "spec": map[string]interface{}{ + "homeserver": map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": InputConfigMapName, + }, + }, + "bridges": map[string]interface{}{ + "mautrixSignal": map[string]interface{}{ + "enabled": true, + }, + }, + }, + }, + ), + Entry( + "when optional mautrix-signal bridge is enabled and an input ConfigMap name is given", + map[string]interface{}{ + "spec": map[string]interface{}{ + "homeserver": map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": InputConfigMapName, + }, + }, + "bridges": map[string]interface{}{ + "mautrixSignal": map[string]interface{}{ + "enabled": true, + "configMap": map[string]interface{}{ + "name": "random-name", + }, + }, + }, + }, + }, + ), ) }) @@ -839,6 +815,10 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( }) When("Enabling the Heisenbridge", func() { + const ( + heisenbridgePort = 9898 + ) + var createdHeisenbridgeDeployment *appsv1.Deployment var createdHeisenbridgeService *corev1.Service var createdHeisenbridgeConfigMap *corev1.ConfigMap @@ -903,7 +883,6 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( }) It("Should create a Deployment for Heisenbridge", func() { - By("Checking that a Synapse Deployment exists and is correctly configured") checkResourcePresence(createdHeisenbridgeDeployment, heisenbridgeLookupKey, expectedOwnerReference) }) @@ -987,7 +966,7 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( // required data for our tests. In particular, we // will test if the URL has been correctly updated inputHeisenbridgeConfigMapData = map[string]string{ - "heisenbridge.yaml": "url: http://10.217.5.134:9898", + "heisenbridge.yaml": "url: http://10.217.5.134:" + strconv.Itoa(heisenbridgePort), } inputHeisenbridgeConfigMap = &corev1.ConfigMap{ @@ -1023,7 +1002,6 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( }) It("Should create a Deployment for Heisenbridge", func() { - By("Checking that a Synapse Deployment exists and is correctly configured") checkResourcePresence(createdHeisenbridgeDeployment, heisenbridgeLookupKey, expectedOwnerReference) }) @@ -1059,11 +1037,359 @@ var _ = Describe("Integration tests for the Synapse controller", Ordered, Label( _, ok = heisenbridge["url"] g.Expect(ok).Should(BeTrue()) - g.Expect(heisenbridge["url"]).To(Equal("http://" + heisenbridgeIP + ":9898")) + g.Expect(heisenbridge["url"]).To(Equal("http://" + heisenbridgeIP + ":" + strconv.Itoa(heisenbridgePort))) }, timeout, interval).Should(Succeed()) }) }) }) + + When("Enabling the mautrix-signal", func() { + const ( + mautrixSignalPort = 29328 + ) + + var createdSignaldDeployment *appsv1.Deployment + var createdSignaldPVC *corev1.PersistentVolumeClaim + var createdMautrixSignalServiceAccount *corev1.ServiceAccount + var createdMautrixSignalRoleBinding *rbacv1.RoleBinding + var createdMautrixSignalDeployment *appsv1.Deployment + var createdMautrixSignalPVC *corev1.PersistentVolumeClaim + var createdMautrixSignalService *corev1.Service + var createdMautrixSignalConfigMap *corev1.ConfigMap + var mautrixSignalLookupKey types.NamespacedName + var signaldLookupKey types.NamespacedName + + var initMautrixSignalVariables = func() { + // Init vars + createdSignaldDeployment = &appsv1.Deployment{} + createdSignaldPVC = &corev1.PersistentVolumeClaim{} + createdMautrixSignalServiceAccount = &corev1.ServiceAccount{} + createdMautrixSignalRoleBinding = &rbacv1.RoleBinding{} + createdMautrixSignalDeployment = &appsv1.Deployment{} + createdMautrixSignalPVC = &corev1.PersistentVolumeClaim{} + createdMautrixSignalService = &corev1.Service{} + createdMautrixSignalConfigMap = &corev1.ConfigMap{} + + signaldLookupKey = types.NamespacedName{Name: SynapseName + "-signald", Namespace: SynapseNamespace} + mautrixSignalLookupKey = types.NamespacedName{Name: SynapseName + "-mautrixsignal", Namespace: SynapseNamespace} + } + + var cleanupMautrixSignalResources = func() { + By("Cleaning up the signald Deployment") + deleteResource(createdSignaldDeployment, signaldLookupKey, false) + + By("Cleaning up the mautrix-signal Deployment") + deleteResource(createdMautrixSignalDeployment, mautrixSignalLookupKey, false) + + By("Cleaning up the signald PVC") + deleteResource(createdSignaldPVC, signaldLookupKey, true) + + By("Cleaning up the mautrix-signal PVC") + deleteResource(createdMautrixSignalPVC, mautrixSignalLookupKey, true) + + By("Cleaning up the mautrix-signal Service") + deleteResource(createdMautrixSignalService, mautrixSignalLookupKey, false) + + By("Cleaning up the mautrix-signal ConfigMap") + deleteResource(createdMautrixSignalConfigMap, mautrixSignalLookupKey, false) + + By("Cleaning up mautrix-signal RoleBinding") + deleteResource(createdMautrixSignalRoleBinding, mautrixSignalLookupKey, false) + + By("Cleaning up mautrix-signal ServiceAccount") + deleteResource(createdMautrixSignalServiceAccount, mautrixSignalLookupKey, false) + } + + When("Using the default configuration", func() { + BeforeAll(func() { + initSynapseVariables() + initMautrixSignalVariables() + + inputConfigmapData = map[string]string{ + "homeserver.yaml": "server_name: " + ServerName + "\n" + + "report_stats: " + strconv.FormatBool(ReportStats), + } + + synapseSpec = synapsev1alpha1.SynapseSpec{ + Homeserver: synapsev1alpha1.SynapseHomeserver{ + ConfigMap: &synapsev1alpha1.SynapseHomeserverConfigMap{ + Name: InputConfigMapName, + }, + }, + Bridges: synapsev1alpha1.SynapseBridges{ + MautrixSignal: synapsev1alpha1.SynapseMautrixSignal{ + Enabled: true, + }, + }, + } + + createSynapseConfigMap() + createSynapseInstance() + }) + + AfterAll(func() { + // Cleanup mautrix-signal resources + cleanupSynapseResources() + cleanupSynapseConfigMap() + cleanupMautrixSignalResources() + }) + + It("Should create a Deployment for signald", func() { + checkResourcePresence(createdSignaldDeployment, signaldLookupKey, expectedOwnerReference) + }) + + It("Should create a PVC for signald", func() { + checkResourcePresence(createdSignaldPVC, signaldLookupKey, expectedOwnerReference) + }) + + It("Should create a ConfigMap for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalConfigMap, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Role Binding for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalRoleBinding, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Service Account for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalServiceAccount, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Deployment for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalDeployment, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a PVC for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalPVC, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Service for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalService, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should add the mautrix-signal IP to the Synapse Status", func() { + // Get mautrix-signal IP + var mautrixSignalIP string + Eventually(func() bool { + err := k8sClient.Get(ctx, mautrixSignalLookupKey, createdMautrixSignalService) + if err != nil { + return false + } + mautrixSignalIP = createdMautrixSignalService.Spec.ClusterIP + return mautrixSignalIP != "" + }, timeout, interval).Should(BeTrue()) + + Expect(k8sClient.Get(ctx, synapseLookupKey, synapse)).To(Succeed()) + Expect(synapse.Status.BridgesConfiguration.MautrixSignal.IP).To(Equal(mautrixSignalIP)) + }) + + It("Should update the Synapse homeserver.yaml", func() { + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, + types.NamespacedName{Name: SynapseName, Namespace: SynapseNamespace}, + createdConfigMap, + )).Should(Succeed()) + + cm_data, ok := createdConfigMap.Data["homeserver.yaml"] + g.Expect(ok).Should(BeTrue()) + + homeserver := make(map[string]interface{}) + g.Expect(yaml.Unmarshal([]byte(cm_data), homeserver)).Should(Succeed()) + + _, ok = homeserver["app_service_config_files"] + g.Expect(ok).Should(BeTrue()) + + g.Expect(homeserver["app_service_config_files"]).Should(ContainElement("/data-mautrixsignal/registration.yaml")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("The user provides an input ConfigMap", func() { + var inputMautrixSignalConfigMap *corev1.ConfigMap + var inputMautrixSignalConfigMapData map[string]string + var mautrixSignalIP string + + const InputMautrixSignalConfigMapName = "mautrix-signal-input" + + BeforeAll(func() { + initSynapseVariables() + initMautrixSignalVariables() + + mautrixSignalIP = "" + + inputConfigmapData = map[string]string{ + "homeserver.yaml": "server_name: " + ServerName + "\n" + + "report_stats: " + strconv.FormatBool(ReportStats), + } + + synapseSpec = synapsev1alpha1.SynapseSpec{ + Homeserver: synapsev1alpha1.SynapseHomeserver{ + ConfigMap: &synapsev1alpha1.SynapseHomeserverConfigMap{ + Name: InputConfigMapName, + }, + }, + Bridges: synapsev1alpha1.SynapseBridges{ + MautrixSignal: synapsev1alpha1.SynapseMautrixSignal{ + Enabled: true, + ConfigMap: synapsev1alpha1.SynapseMautrixSignalConfigMap{ + Name: InputMautrixSignalConfigMapName, + }, + }, + }, + } + + By("Creating a ConfigMap containing a basic config.yaml") + // Incomplete config.yaml, containing only the + // required data for our tests. We test that those + // values are correctly updated + configYaml := ` + homeserver: + address: http://localhost:8008 + domain: mydomain.com + appservice: + address: http://localhost:29328 + signal: + socket_path: /var/run/signald/signald.sock + bridge: + permissions: + "*": "relay" + "mydomain.com": "user" + "@admin:mydomain.com": "admin" + logging: + handlers: + file: + filename: ./mautrix-signal.log` + + inputMautrixSignalConfigMapData = map[string]string{ + "config.yaml": configYaml, + } + + inputMautrixSignalConfigMap = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: InputMautrixSignalConfigMapName, + Namespace: SynapseNamespace, + }, + Data: inputMautrixSignalConfigMapData, + } + Expect(k8sClient.Create(ctx, inputMautrixSignalConfigMap)).Should(Succeed()) + + createSynapseConfigMap() + createSynapseInstance() + }) + + AfterAll(func() { + // Cleanup mautrix-signal resources + By("Cleaning up the mautrix-signal ConfigMap") + mautrixSignalConfigMapLookupKey := types.NamespacedName{ + Name: InputMautrixSignalConfigMapName, + Namespace: SynapseNamespace, + } + + deleteResource(inputMautrixSignalConfigMap, mautrixSignalConfigMapLookupKey, false) + + cleanupSynapseResources() + cleanupSynapseConfigMap() + cleanupMautrixSignalResources() + }) + + It("Should create a Deployment for signald", func() { + checkResourcePresence(createdMautrixSignalDeployment, signaldLookupKey, expectedOwnerReference) + }) + + It("Should create a PVC for signald", func() { + checkResourcePresence(createdSignaldPVC, signaldLookupKey, expectedOwnerReference) + }) + + It("Should create a ConfigMap for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalConfigMap, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Role Binding for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalRoleBinding, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Service Account for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalServiceAccount, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Deployment for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalDeployment, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a PVC for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalPVC, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should create a Service for mautrix-signal", func() { + checkResourcePresence(createdMautrixSignalService, mautrixSignalLookupKey, expectedOwnerReference) + }) + + It("Should update the Synapse Status with mautrix-signal configuration information", func() { + // Get mautrix-signal IP + Eventually(func() bool { + err := k8sClient.Get(ctx, mautrixSignalLookupKey, createdMautrixSignalService) + if err != nil { + return false + } + mautrixSignalIP = createdMautrixSignalService.Spec.ClusterIP + return mautrixSignalIP != "" + }, timeout, interval).Should(BeTrue()) + + Expect(k8sClient.Get(ctx, synapseLookupKey, synapse)).To(Succeed()) + Expect(synapse.Status.BridgesConfiguration.MautrixSignal.IP).To(Equal(mautrixSignalIP)) + }) + + It("Should overwrite necessary values in the created mautrix-signal ConfigMap", func() { + Eventually(func(g Gomega) { + synapseIP := synapse.Status.IP + synapseServerName := synapse.Status.HomeserverConfiguration.ServerName + + By("Verifying that the mautrixsignal ConfigMap exists") + g.Expect(k8sClient.Get(ctx, mautrixSignalLookupKey, inputMautrixSignalConfigMap)).Should(Succeed()) + + cm_data, ok := inputMautrixSignalConfigMap.Data["config.yaml"] + g.Expect(ok).Should(BeTrue()) + + config := make(map[string]interface{}) + g.Expect(yaml.Unmarshal([]byte(cm_data), config)).Should(Succeed()) + + By("Verifying that the homeserver configuration has been updated") + configHomeserver, ok := config["homeserver"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + g.Expect(configHomeserver["address"]).To(Equal("http://" + synapseIP + ":8008")) + g.Expect(configHomeserver["domain"]).To(Equal(synapseServerName)) + + By("Verifying that the appservice configuration has been updated") + configAppservice, ok := config["appservice"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + g.Expect(configAppservice["address"]).To(Equal("http://" + mautrixSignalIP + ":" + strconv.Itoa(mautrixSignalPort))) + + By("Verifying that the signal configuration has been updated") + configSignal, ok := config["signal"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + g.Expect(configSignal["socket_path"]).To(Equal("/signald/signald.sock")) + + By("Verifying that the permissions have been updated") + configBridge, ok := config["bridge"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + configBridgePermissions, ok := configBridge["permissions"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + g.Expect(configBridgePermissions).Should(HaveKeyWithValue("*", "relay")) + g.Expect(configBridgePermissions).Should(HaveKeyWithValue(synapseServerName, "user")) + g.Expect(configBridgePermissions).Should(HaveKeyWithValue("@admin:"+synapseServerName, "admin")) + + By("Verifying that the log configuration file path have been updated") + configLogging, ok := config["logging"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + configLoggingHandlers, ok := configLogging["handlers"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + configLoggingHandlersFile, ok := configLoggingHandlers["file"].(map[interface{}]interface{}) + g.Expect(ok).Should(BeTrue()) + g.Expect(configLoggingHandlersFile["filename"]).To(Equal("/data/mautrix-signal.log")) + }, timeout, interval).Should(Succeed()) + }) + }) + }) + }) }) diff --git a/controllers/synapse/synapse_deployment.go b/controllers/synapse/synapse_deployment.go index cc47d09..4fcce22 100644 --- a/controllers/synapse/synapse_deployment.go +++ b/controllers/synapse/synapse_deployment.go @@ -88,6 +88,9 @@ func (r *SynapseReconciler) deploymentForSynapse(s *synapsev1alpha1.Synapse, obj ContainerPort: 8008, }}, }}, + // Synapse must run with user 991. + // We must run the workload with a Service Account + // associated to the 'anyuid' SCC. ServiceAccountName: s.Name, Volumes: []corev1.Volume{{ Name: "homeserver", @@ -112,7 +115,7 @@ func (r *SynapseReconciler) deploymentForSynapse(s *synapsev1alpha1.Synapse, obj } if s.Spec.Bridges.Heisenbridge.Enabled { - heisenbridgeConfigMapName := objectMeta.Name + "-heisenbridge" + heisenbridgeConfigMapName := r.GetHeisenbridgeResourceName(*s) dep.Spec.Template.Spec.Containers[0].VolumeMounts = append( dep.Spec.Template.Spec.Containers[0].VolumeMounts, @@ -137,6 +140,36 @@ func (r *SynapseReconciler) deploymentForSynapse(s *synapsev1alpha1.Synapse, obj ) } + if s.Spec.Bridges.MautrixSignal.Enabled { + // If the mautrix-signal bridge is enabled, then Synapse needs access + // to the registration.yaml file, containing all information to + // register the mautrix-signal bridge as an application service in + // homeserver.yaml. This registration file is generated by the bridge + // the first time it runs and located alongside the config.yaml (config + // file for mautrix-signal), that is it's in the mautrix-signal PV + mautrixSignalPVCName := r.GetMautrixSignalResourceName(*s) + + dep.Spec.Template.Spec.Containers[0].VolumeMounts = append( + dep.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: "data-mautrixsignal", + MountPath: "/data-mautrixsignal", + }, + ) + + dep.Spec.Template.Spec.Volumes = append( + dep.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: "data-mautrixsignal", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: mautrixSignalPVCName, + }, + }, + }, + ) + } + // Set Synapse instance as the owner and controller if err := ctrl.SetControllerReference(s, dep, r.Scheme); err != nil { return &appsv1.Deployment{}, err diff --git a/controllers/synapse/synapse_heisenbridge_service.go b/controllers/synapse/synapse_heisenbridge_service.go index 6502984..6e016b5 100644 --- a/controllers/synapse/synapse_heisenbridge_service.go +++ b/controllers/synapse/synapse_heisenbridge_service.go @@ -32,7 +32,7 @@ func (r *SynapseReconciler) serviceForHeisenbridge(s *synapsev1alpha1.Synapse, o ObjectMeta: objectMeta, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{{ - Name: "http", + Name: "heisenbridge", Protocol: corev1.ProtocolTCP, Port: 9898, TargetPort: intstr.FromInt(9898), diff --git a/controllers/synapse/synapse_mautrixsignal_configmap.go b/controllers/synapse/synapse_mautrixsignal_configmap.go new file mode 100644 index 0000000..0bb1dfb --- /dev/null +++ b/controllers/synapse/synapse_mautrixsignal_configmap.go @@ -0,0 +1,457 @@ +/* +Copyright 2021. + +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 synapse + +import ( + "errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// configMapForSynapse returns a synapse ConfigMap object +func (r *SynapseReconciler) configMapForMautrixSignal(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + synapseServerName := s.Status.HomeserverConfiguration.ServerName + synapseIP := s.Status.IP + + configYaml := ` +# Homeserver details +homeserver: + # The address that this appservice can use to connect to the homeserver. + address: http://` + synapseIP + `:8008 + # The domain of the homeserver (for MXIDs, etc). + domain: ` + synapseServerName + ` + # Whether or not to verify the SSL certificate of the homeserver. + # Only applies if address starts with https:// + verify_ssl: true + asmux: false + # Number of retries for all HTTP requests if the homeserver isn't reachable. + http_retry_count: 4 + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's Signal connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: null + # Endpoint for reporting per-message status. + message_send_checkpoint_endpoint: null + # Maximum number of simultaneous HTTP connections to the homeserver. + connection_limit: 100 + # Whether asynchronous uploads via MSC2246 should be enabled for media. + # Requires a media repo that supports MSC2246. + async_media: false + +# Application service host/registration related details +# Changing these values requires regeneration of the registration. +appservice: + # The address that the homeserver can use to connect to this appservice. + address: http://` + s.Status.BridgesConfiguration.MautrixSignal.IP + `:29328 + # When using https:// the TLS certificate and key files for the address. + tls_cert: false + tls_key: false + + # The hostname and port where this appservice should listen. + hostname: 0.0.0.0 + port: 29328 + # The maximum body size of appservice API requests (from the homeserver) in mebibytes + # Usually 1 is enough, but on high-traffic bridges you might need to increase this to avoid 413s + max_body_size: 1 + + # The full URI to the database. SQLite and Postgres are supported. + # However, SQLite support is extremely experimental and should not be used. + # Format examples: + # SQLite: sqlite:///filename.db + # Postgres: postgres://username:password@hostname/dbname + #database: postgres://username:password@hostname/db + database: sqlite:////data/sqlite.db + + # Additional arguments for asyncpg.create_pool() or sqlite3.connect() + # https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool + # https://docs.python.org/3/library/sqlite3.html#sqlite3.connect + # For sqlite, min_size is used as the connection thread pool size and max_size is ignored. + database_opts: + min_size: 5 + max_size: 10 + + # The unique ID of this appservice. + id: signal + # Username of the appservice bot. + bot_username: signalbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + bot_displayname: Signal bridge bot + bot_avatar: mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp + + # Whether or not to receive ephemeral events via appservice transactions. + # Requires MSC2409 support (i.e. Synapse 1.22+). + # You should disable bridge -> sync_with_custom_puppets when this is enabled. + ephemeral_events: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + +# Prometheus telemetry config. Requires prometheus-client to be installed. +metrics: + enabled: false + listen_port: 8000 + +# Manhole config. +manhole: + # Whether or not opening the manhole is allowed. + enabled: false + # The path for the unix socket. + path: /var/tmp/mautrix-signal.manhole + # The list of UIDs who can be added to the whitelist. + # If empty, any UIDs can be specified in the open-manhole command. + whitelist: + - 0 + +signal: + # Path to signald unix socket + socket_path: /signald/signald.sock + # Directory for temp files when sending files to Signal. This should be an + # absolute path that signald can read. For attachments in the other direction, + # make sure signald is configured to use an absolute path as the data directory. + outgoing_attachment_dir: /tmp + # Directory where signald stores avatars for groups. + avatar_dir: ~/.config/signald/avatars + # Directory where signald stores auth data. Used to delete data when logging out. + data_dir: ~/.config/signald/data + # Whether or not unknown signald accounts should be deleted when the bridge is started. + # When this is enabled, any UserInUse errors should be resolved by restarting the bridge. + delete_unknown_accounts_on_start: false + # Whether or not message attachments should be removed from disk after they're bridged. + remove_file_after_handling: true + # Whether or not users can register a primary device + registration_enabled: true + # Whether or not to enable disappearing messages in groups. If enabled, then the expiration + # time of the messages will be determined by the first users to read the message, rather + # than individually. If the bridge has a single user, this can be turned on safely. + enable_disappearing_messages_in_groups: false + +# Bridge config +bridge: + # Localpart template of MXIDs for Signal users. + # {userid} is replaced with an identifier for the Signal user. + username_template: "signal_{userid}" + # Displayname template for Signal users. + # {displayname} is replaced with the displayname of the Signal user, which is the first + # available variable in displayname_preference. The variables in displayname_preference + # can also be used here directly. + displayname_template: "{displayname} (Signal)" + # Whether or not contact list displaynames should be used. + # Possible values: disallow, allow, prefer + # + # Multi-user instances are recommended to disallow contact list names, as otherwise there can + # be conflicts between names from different users' contact lists. + contact_list_names: disallow + # Available variables: full_name, first_name, last_name, phone, uuid + displayname_preference: + - full_name + - phone + + # Whether or not to create portals for all groups on login/connect. + autocreate_group_portal: true + # Whether or not to create portals for all contacts on login/connect. + autocreate_contact_portal: false + # Whether or not to use /sync to get read receipts and typing notifications + # when double puppeting is enabled + sync_with_custom_puppets: true + # Whether or not to update the m.direct account data event when double puppeting is enabled. + # Note that updating the m.direct event is not atomic (except with mautrix-asmux) + # and is therefore prone to race conditions. + sync_direct_chat_list: false + # Allow using double puppeting from any server with a valid client .well-known file. + double_puppet_allow_discovery: false + # Servers to allow double puppeting from, even if double_puppet_allow_discovery is false. + double_puppet_server_map: + example.com: https://example.com + # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth + # + # If set, custom puppets will be enabled automatically for local users + # instead of users having to find an access token and run 'login-matrix' + # manually. + # If using this for other servers than the bridge's server, + # you must also set the URL in the double_puppet_server_map. + login_shared_secret_map: + example.com: foo + # Whether or not created rooms should have federation enabled. + # If false, created portal rooms will never be federated. + federate_rooms: true + # End-to-bridge encryption support options. + # + # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + default: false + # Options for automatic key sharing. + key_sharing: + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow: false + # Require the requesting device to have a valid cross-signing signature? + # This doesn't require that the bridge has verified the device, only that the user has verified it. + # Not yet implemented. + require_cross_signing: false + # Require devices to be verified by the bridge? + # Verification by the bridge is not yet implemented. + require_verification: true + # Whether or not to explicitly set the avatar and room name for private + # chat portal rooms. This will be implicitly enabled if encryption.default is true. + private_chat_portal_meta: false + # Whether or not the bridge should send a read receipt from the bridge bot when a message has + # been sent to Signal. This let's you check manually whether the bridge is receiving your + # messages. + # Note that this is not related to Signal delivery receipts. + delivery_receipts: false + # Whether or not delivery errors should be reported as messages in the Matrix room. (not yet implemented) + delivery_error_reports: false + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. + # This field will automatically be changed back to false after it, + # except if the config file is not writable. + resend_bridge_info: false + # Interval at which to resync contacts (in seconds). + periodic_sync: 0 + # Should leaving the room on Matrix make the user leave on Signal? + bridge_matrix_leave: true + + # Provisioning API part of the web server for automated portal creation and fetching information. + # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager). + provisioning: + # Whether or not the provisioning API should be enabled. + enabled: true + # The prefix to use in the provisioning API endpoints. + prefix: /_matrix/provision + # The shared secret to authorize users of the API. + # Set to "generate" to generate and save a new token. + shared_secret: generate + # Segment API key to enable analytics tracking for web server + # endpoints. Set to null to disable. + # Currently the only events are login start, QR code scan, and login + # success/failure. + segment_key: null + + # The prefix for commands. Only required in non-management rooms. + command_prefix: "!signal" + + # Messages sent upon joining a management room. + # Markdown is supported. The defaults are listed below. + management_room_text: + # Sent when joining a room. + welcome: "Hello, I'm a Signal bridge bot." + # Sent when joining a management room and the user is already logged in. + welcome_connected: "Use 'help' for help." + # Sent when joining a management room and the user is not logged in. + welcome_unconnected: "Use 'help' for help or 'link' to log in." + # Optional extra text sent when joining a management room. + additional_help: "" + + # Send each message separately (for readability in some clients) + management_room_multiple_messages: false + + # Permissions for using the bridge. + # Permitted values: + # relay - Allowed to be relayed through the bridge, no access to commands. + # user - Use the bridge with puppeting. + # admin - Use and administrate the bridge. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": "relay" + "` + synapseServerName + `": "user" + "@admin:` + synapseServerName + `": "admin" + + relay: + # Whether relay mode should be allowed. If allowed, '!signal set-relay' can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # The formats to use when sending messages to Signal via a relay user. + # + # Available variables: + # $sender_displayname - The display name of the sender (e.g. Example User) + # $sender_username - The username (Matrix ID localpart) of the sender (e.g. exampleuser) + # $sender_mxid - The Matrix ID of the sender (e.g. @exampleuser:example.com) + # $message - The message content + message_formats: + m.text: '$sender_displayname: $message' + m.notice: '$sender_displayname: $message' + m.emote: '* $sender_displayname $message' + m.file: '$sender_displayname sent a file' + m.image: '$sender_displayname sent an image' + m.audio: '$sender_displayname sent an audio file' + m.video: '$sender_displayname sent a video' + m.location: '$sender_displayname sent a location' + +# Python logging configuration. +# +# See section 16.7.2 of the Python documentation for more info: +# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema +logging: + version: 1 + formatters: + colored: + (): mautrix_signal.util.ColorFormatter + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + normal: + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: normal + filename: /data/mautrix-signal.log + maxBytes: 10485760 + backupCount: 10 + console: + class: logging.StreamHandler + formatter: colored + loggers: + mau: + level: DEBUG + aiohttp: + level: INFO + root: + level: DEBUG + handlers: [file, console] +` + + cm := &corev1.ConfigMap{ + ObjectMeta: objectMeta, + Data: map[string]string{"config.yaml": configYaml}, + } + + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, cm, r.Scheme); err != nil { + return &corev1.ConfigMap{}, err + } + + return cm, nil +} + +// configMapForMautrixSignalCopy is a function of type createResourceFunc, to be +// passed as an argument in a call to reconcileResouce. +// +// The ConfigMap returned by configMapForMautrixSignalCopy is a copy of the ConfigMap +// defined in Spec.Bridges.MautrixSignal.ConfigMap. +func (r *SynapseReconciler) configMapForMautrixSignalCopy( + s *synapsev1alpha1.Synapse, + objectMeta metav1.ObjectMeta, +) (client.Object, error) { + var copyConfigMap *corev1.ConfigMap + + sourceConfigMapName := s.Spec.Bridges.MautrixSignal.ConfigMap.Name + sourceConfigMapNamespace := r.getConfigMapNamespace(*s, s.Spec.Bridges.MautrixSignal.ConfigMap.Namespace) + + copyConfigMap, err := r.getConfigMapCopy(sourceConfigMapName, sourceConfigMapNamespace, objectMeta) + if err != nil { + return &corev1.ConfigMap{}, err + } + + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, copyConfigMap, r.Scheme); err != nil { + return &corev1.ConfigMap{}, err + } + + return copyConfigMap, nil +} + +// updateMautrixSignalData is a function of type updateDataFunc function to +// be passed as an argument in a call to updateConfigMap. +// +// It configures the user-provided config.yaml with the correct values. Among +// other things, it ensures that the bridge can reach the Synapse homeserver +// and knows the correct path to the signald socket. +func (r *SynapseReconciler) updateMautrixSignalData( + s synapsev1alpha1.Synapse, + config map[string]interface{}, +) error { + synapseServerName := s.Status.HomeserverConfiguration.ServerName + synapseIP := s.Status.IP + + // Update the homeserver section so that the bridge can reach Synapse + configHomeserver, ok := config["homeserver"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'homeserver' section") + return err + } + configHomeserver["address"] = "http://" + synapseIP + ":8008" + configHomeserver["domain"] = synapseServerName + config["homeserver"] = configHomeserver + + // Update the appservice section so that Synapse can reach the bridge + configAppservice, ok := config["appservice"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'appservice' section") + return err + } + configAppservice["address"] = "http://" + s.Status.BridgesConfiguration.MautrixSignal.IP + ":29328" + config["appservice"] = configAppservice + + // Update the path to the signal socket path + configSignal, ok := config["signal"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'signal' section") + return err + } + configSignal["socket_path"] = "/signald/signald.sock" + config["signal"] = configSignal + + // Update persmissions to use the correct domain name + configBridge, ok := config["bridge"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'bridge' section") + return err + } + configBridge["permissions"] = map[string]string{ + "*": "relay", + synapseServerName: "user", + "@admin:" + synapseServerName: "admin", + } + config["bridge"] = configBridge + + // Update the path to the log file + configLogging, ok := config["logging"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'logging' section") + return err + } + configLoggingHandlers, ok := configLogging["handlers"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'logging/handlers' section") + return err + } + configLoggingHandlersFile, ok := configLoggingHandlers["file"].(map[interface{}]interface{}) + if !ok { + err := errors.New("cannot parse mautrix-signal config.yaml: error parsing 'logging/handlers/file' section") + return err + } + configLoggingHandlersFile["filename"] = "/data/mautrix-signal.log" + configLoggingHandlers["file"] = configLoggingHandlersFile + configLogging["handlers"] = configLoggingHandlers + config["logging"] = configLogging + + return nil +} diff --git a/controllers/synapse/synapse_mautrixsignal_deployment.go b/controllers/synapse/synapse_mautrixsignal_deployment.go new file mode 100644 index 0000000..31a31f7 --- /dev/null +++ b/controllers/synapse/synapse_mautrixsignal_deployment.go @@ -0,0 +1,126 @@ +/* +Copyright 2021. + +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 synapse + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// labelsForMautrixSignal returns the labels for selecting the resources +// belonging to the given synapse CR name. +func labelsForMautrixSignal(name string) map[string]string { + return map[string]string{"app": "mautrix-signal", "synapse_cr": name} +} + +// deploymentForMautrixSignal returns a Deployment object for the mautrix-signal bridge +func (r *SynapseReconciler) deploymentForMautrixSignal(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + ls := labelsForMautrixSignal(s.Name) + replicas := int32(1) + + // The associated mautrix-signal objects (ConfigMap, PVC, SA) share the + // same name as the mautrix-signal Deployment + mautrixSignalConfigMapName := objectMeta.Name + mautrixSignalPVCName := objectMeta.Name + mautrixSignalServiceAccountName := objectMeta.Name + + // The Signald PVC name is the Synapse object name with "-signald" appended + SignaldPVCName := r.GetSignaldResourceName(*s) + + dep := &appsv1.Deployment{ + ObjectMeta: objectMeta, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + // mautrix-signal must run with user 1337. + // We must run the workload with a Service Account + // associated to the 'anyuid' SCC. + ServiceAccountName: mautrixSignalServiceAccountName, + // The init container is responsible of copying the + // config.yaml from the read-only ConfigMap to the + // mautrixsignal-data volume. The mautrixsignal process + // needs read & write access to the config.yaml file. + InitContainers: []corev1.Container{{ + Image: "ubi8/ubi-minimal:8.6", + Name: "initconfig", + VolumeMounts: []corev1.VolumeMount{{ + Name: "config", + MountPath: "/input", + }, { + Name: "mautrixsignal-data", + MountPath: "/data", + }}, + Command: []string{"bin/sh", "-c"}, + Args: []string{"if [ ! -f /data/config.yaml ]; then cp /input/config.yaml /data/config.yaml; fi"}, + }}, + Containers: []corev1.Container{{ + Image: "dock.mau.dev/mautrix/signal:v0.3.0", + Name: "mautrix-signal", + VolumeMounts: []corev1.VolumeMount{{ + Name: "signald", + MountPath: "/signald", + }, { + Name: "mautrixsignal-data", + MountPath: "/data", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: mautrixSignalConfigMapName, + }, + }, + }, + }, { + Name: "signald", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: SignaldPVCName, + }, + }, + }, { + Name: "mautrixsignal-data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: mautrixSignalPVCName, + }, + }, + }}, + }, + }, + }, + } + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, dep, r.Scheme); err != nil { + return &appsv1.Deployment{}, err + } + return dep, nil +} diff --git a/controllers/synapse/synapse_mautrixsignal_pvc.go b/controllers/synapse/synapse_mautrixsignal_pvc.go new file mode 100644 index 0000000..cdafb8c --- /dev/null +++ b/controllers/synapse/synapse_mautrixsignal_pvc.go @@ -0,0 +1,51 @@ +/* +Copyright 2021. + +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 synapse + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// persistentVolumeClaimForSynapse returns a synapse PVC object +func (r *SynapseReconciler) persistentVolumeClaimForMautrixSignal(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + pvcmode := corev1.PersistentVolumeFilesystem + + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: objectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{"ReadWriteOnce"}, + VolumeMode: &pvcmode, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "storage": *resource.NewQuantity(5*1024*1024*1024, resource.BinarySI), + }, + }, + }, + } + + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, pvc, r.Scheme); err != nil { + return &corev1.PersistentVolumeClaim{}, err + } + return pvc, nil +} diff --git a/controllers/synapse/synapse_mautrixsignal_service.go b/controllers/synapse/synapse_mautrixsignal_service.go new file mode 100644 index 0000000..ec5d687 --- /dev/null +++ b/controllers/synapse/synapse_mautrixsignal_service.go @@ -0,0 +1,49 @@ +/* +Copyright 2021. + +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 synapse + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// serviceForSynapse returns a synapse Service object +func (r *SynapseReconciler) serviceForMautrixSignal(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + service := &corev1.Service{ + ObjectMeta: objectMeta, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: "mautrix-signal", + Protocol: corev1.ProtocolTCP, + Port: 29328, + TargetPort: intstr.FromInt(29328), + }}, + Selector: labelsForMautrixSignal(s.Name), + Type: corev1.ServiceTypeClusterIP, + }, + } + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, service, r.Scheme); err != nil { + return &corev1.Service{}, err + } + return service, nil +} diff --git a/controllers/synapse/synapse_mautrixsignal_serviceaccount.go b/controllers/synapse/synapse_mautrixsignal_serviceaccount.go new file mode 100644 index 0000000..ceb472d --- /dev/null +++ b/controllers/synapse/synapse_mautrixsignal_serviceaccount.go @@ -0,0 +1,65 @@ +/* +Copyright 2021. + +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 synapse + +import ( + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// serviceAccountForMautrixSignal returns a ServiceAccount object for running the mautrix-signal bridge +func (r *SynapseReconciler) serviceAccountForMautrixSignal(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + // TODO: https://github.com/opdev/synapse-operator/issues/19 + sa := &corev1.ServiceAccount{ + ObjectMeta: objectMeta, + } + + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, sa, r.Scheme); err != nil { + return &corev1.ServiceAccount{}, err + } + return sa, nil +} + +// roleBindingForMautrixSignal returns a RoleBinding object for the mautrix-signal bridge +func (r *SynapseReconciler) roleBindingForMautrixSignal(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + // TODO: https://github.com/opdev/synapse-operator/issues/19 + rb := &rbacv1.RoleBinding{ + ObjectMeta: objectMeta, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "system:openshift:scc:anyuid", + }, + Subjects: []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: objectMeta.Name, + Namespace: objectMeta.Namespace, + }}, + } + + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, rb, r.Scheme); err != nil { + return &rbacv1.RoleBinding{}, err + } + return rb, nil +} diff --git a/controllers/synapse/synapse_service.go b/controllers/synapse/synapse_service.go index 6e89306..87d52e6 100644 --- a/controllers/synapse/synapse_service.go +++ b/controllers/synapse/synapse_service.go @@ -32,7 +32,7 @@ func (r *SynapseReconciler) serviceForSynapse(s *synapsev1alpha1.Synapse, object ObjectMeta: objectMeta, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{{ - Name: "http", + Name: "synapse_unsecure", Protocol: corev1.ProtocolTCP, Port: 8008, TargetPort: intstr.FromInt(8008), diff --git a/controllers/synapse/synapse_serviceaccount.go b/controllers/synapse/synapse_serviceaccount.go index f72fbfb..601fb4a 100644 --- a/controllers/synapse/synapse_serviceaccount.go +++ b/controllers/synapse/synapse_serviceaccount.go @@ -28,6 +28,7 @@ import ( // serviceAccountForSynapse returns a synapse ServiceAccount object func (r *SynapseReconciler) serviceAccountForSynapse(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + // TODO: https://github.com/opdev/synapse-operator/issues/19 sa := &corev1.ServiceAccount{ ObjectMeta: objectMeta, } @@ -41,6 +42,7 @@ func (r *SynapseReconciler) serviceAccountForSynapse(s *synapsev1alpha1.Synapse, // roleBindingForSynapse returns a synapse RoleBinding object func (r *SynapseReconciler) roleBindingForSynapse(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + // TODO: https://github.com/opdev/synapse-operator/issues/19 rb := &rbacv1.RoleBinding{ ObjectMeta: objectMeta, RoleRef: rbacv1.RoleRef{ diff --git a/controllers/synapse/synapse_signald_deployment.go b/controllers/synapse/synapse_signald_deployment.go new file mode 100644 index 0000000..0d80f0b --- /dev/null +++ b/controllers/synapse/synapse_signald_deployment.go @@ -0,0 +1,78 @@ +/* +Copyright 2021. + +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 synapse + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// labelsForSynapse returns the labels for selecting the resources +// belonging to the given synapse CR name. +func labelsForSignald(name string) map[string]string { + return map[string]string{"app": "heisenbridge", "synapse_cr": name} +} + +// deploymentForSynapse returns a synapse Deployment object +func (r *SynapseReconciler) deploymentForSignald(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + ls := labelsForSignald(s.Name) + replicas := int32(1) + signaldPVCName := objectMeta.Name + + dep := &appsv1.Deployment{ + ObjectMeta: objectMeta, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: "docker.io/signald/signald:0.19.1", + Name: "signald", + VolumeMounts: []corev1.VolumeMount{{ + Name: "signald", + MountPath: "/signald", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "signald", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: signaldPVCName, + }, + }, + }}, + }, + }, + }, + } + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, dep, r.Scheme); err != nil { + return &appsv1.Deployment{}, err + } + return dep, nil +} diff --git a/controllers/synapse/synapse_signald_pvc.go b/controllers/synapse/synapse_signald_pvc.go new file mode 100644 index 0000000..1df4b60 --- /dev/null +++ b/controllers/synapse/synapse_signald_pvc.go @@ -0,0 +1,51 @@ +/* +Copyright 2021. + +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 synapse + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// persistentVolumeClaimForSynapse returns a synapse PVC object +func (r *SynapseReconciler) persistentVolumeClaimForSignald(s *synapsev1alpha1.Synapse, objectMeta metav1.ObjectMeta) (client.Object, error) { + pvcmode := corev1.PersistentVolumeFilesystem + + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: objectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{"ReadWriteOnce"}, + VolumeMode: &pvcmode, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "storage": *resource.NewQuantity(5*1024*1024*1024, resource.BinarySI), + }, + }, + }, + } + + // Set Synapse instance as the owner and controller + if err := ctrl.SetControllerReference(s, pvc, r.Scheme); err != nil { + return &corev1.PersistentVolumeClaim{}, err + } + return pvc, nil +} diff --git a/controllers/synapse/tests_utils.go b/controllers/synapse/tests_utils.go new file mode 100644 index 0000000..75671ff --- /dev/null +++ b/controllers/synapse/tests_utils.go @@ -0,0 +1,127 @@ +package synapse + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 Ginkgo and gomega are usually dot-imported + . "github.com/onsi/gomega" //lint:ignore ST1001 Ginkgo and gomega are usually dot-imported + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + synapsev1alpha1 "github.com/opdev/synapse-operator/apis/synapse/v1alpha1" +) + +// Helper function for struct construction requiring a boolean pointer +func BoolAddr(b bool) *bool { + boolVar := b + return &boolVar +} + +func convert(i interface{}) interface{} { + switch x := i.(type) { + case map[interface{}]interface{}: + m2 := map[string]interface{}{} + for k, v := range x { + m2[k.(string)] = convert(v) + } + return m2 + case []interface{}: + for i, v := range x { + x[i] = convert(v) + } + } + return i +} + +func deleteResourceFunc( + k8sClient client.Client, + ctx context.Context, + timeout time.Duration, + interval time.Duration, +) func(client.Object, types.NamespacedName, bool) { + return func(resource client.Object, lookupKey types.NamespacedName, removeFinalizers bool) { + // Using 'Eventually' to eliminate race conditions where the Synapse + // Operator didn't have time to create a sub resource. + Eventually(func() bool { + err := k8sClient.Get(ctx, lookupKey, resource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + if removeFinalizers { + // Manually remove the finalizers + resource.SetFinalizers([]string{}) + Expect(k8sClient.Update(ctx, resource)).Should(Succeed()) + } + + // Deleting + Expect(k8sClient.Delete(ctx, resource)).Should(Succeed()) + + // Check that the resource was successfully removed + Eventually(func() bool { + err := k8sClient.Get(ctx, lookupKey, resource) + return err == nil + }, timeout, interval).Should(BeFalse()) + } +} + +func checkSubresourceAbsenceFunc( + SynapseName string, + SynapseNamespace string, + k8sClient client.Client, + ctx context.Context, + timeout time.Duration, + interval time.Duration, +) func(string) { + // Verify the absence of Synapse sub-resources + // This function common to multiple tests + return func(expectedReason string) { + s := &synapsev1alpha1.Synapse{} + synapseLookupKey := types.NamespacedName{Name: SynapseName, Namespace: SynapseNamespace} + expectedState := "FAILED" + + By("Verifying that the Synapse object was created") + Eventually(func() bool { + err := k8sClient.Get(ctx, synapseLookupKey, &synapsev1alpha1.Synapse{}) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By("Checking the Synapse status") + // Status may need some time to be updated + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, synapseLookupKey, s)).Should(Succeed()) + g.Expect(s.Status.State).To(Equal(expectedState)) + g.Expect(s.Status.Reason).To(Equal(expectedReason)) + }, timeout, interval).Should(Succeed()) + + By("Checking that synapse sub-resources have not been created") + Consistently(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, synapseLookupKey, &corev1.ServiceAccount{})).ShouldNot(Succeed()) + g.Expect(k8sClient.Get(ctx, synapseLookupKey, &rbacv1.RoleBinding{})).ShouldNot(Succeed()) + g.Expect(k8sClient.Get(ctx, synapseLookupKey, &corev1.PersistentVolumeClaim{})).ShouldNot(Succeed()) + g.Expect(k8sClient.Get(ctx, synapseLookupKey, &appsv1.Deployment{})).ShouldNot(Succeed()) + g.Expect(k8sClient.Get(ctx, synapseLookupKey, &corev1.Service{})).ShouldNot(Succeed()) + }, timeout, interval).Should(Succeed()) + } +} + +func checkResourcePresenceFunc( + k8sClient client.Client, + ctx context.Context, + timeout time.Duration, + interval time.Duration, +) func(client.Object, types.NamespacedName, metav1.OwnerReference) { + return func(resource client.Object, lookupKey types.NamespacedName, expectedOwnerReference metav1.OwnerReference) { + Eventually(func() bool { + err := k8sClient.Get(ctx, lookupKey, resource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(resource.GetOwnerReferences()).To(ContainElement(expectedOwnerReference)) + } + +} diff --git a/examples/05-deploying-mautrixsignal/A-default-configuration/synapse.yaml b/examples/05-deploying-mautrixsignal/A-default-configuration/synapse.yaml new file mode 100644 index 0000000..65b72f5 --- /dev/null +++ b/examples/05-deploying-mautrixsignal/A-default-configuration/synapse.yaml @@ -0,0 +1,12 @@ +apiVersion: synapse.opdev.io/v1alpha1 +kind: Synapse +metadata: + name: synapse-with-mautrixsignal +spec: + homeserver: + values: + serverName: example2.com + reportStats: true + bridges: + mautrixSignal: + enabled: true diff --git a/examples/05-deploying-mautrixsignal/B-using-existing-configmap/config.yaml b/examples/05-deploying-mautrixsignal/B-using-existing-configmap/config.yaml new file mode 100644 index 0000000..27ce71e --- /dev/null +++ b/examples/05-deploying-mautrixsignal/B-using-existing-configmap/config.yaml @@ -0,0 +1,302 @@ +# Homeserver details +homeserver: + # The address that this appservice can use to connect to the homeserver. + address: https://example.com + # The domain of the homeserver (for MXIDs, etc). + domain: example.com + # Whether or not to verify the SSL certificate of the homeserver. + # Only applies if address starts with https:// + verify_ssl: true + asmux: false + # Number of retries for all HTTP requests if the homeserver isn't reachable. + http_retry_count: 4 + # The URL to push real-time bridge status to. + # If set, the bridge will make POST requests to this URL whenever a user's Signal connection state changes. + # The bridge will use the appservice as_token to authorize requests. + status_endpoint: null + # Endpoint for reporting per-message status. + message_send_checkpoint_endpoint: null + # Maximum number of simultaneous HTTP connections to the homeserver. + connection_limit: 100 + # Whether asynchronous uploads via MSC2246 should be enabled for media. + # Requires a media repo that supports MSC2246. + async_media: false + +# Application service host/registration related details +# Changing these values requires regeneration of the registration. +appservice: + # The address that the homeserver can use to connect to this appservice. + address: http://localhost:29328 + # When using https:// the TLS certificate and key files for the address. + tls_cert: false + tls_key: false + + # The hostname and port where this appservice should listen. + hostname: 0.0.0.0 + port: 29328 + # The maximum body size of appservice API requests (from the homeserver) in mebibytes + # Usually 1 is enough, but on high-traffic bridges you might need to increase this to avoid 413s + max_body_size: 1 + + # The full URI to the database. SQLite and Postgres are supported. + # However, SQLite support is extremely experimental and should not be used. + # Format examples: + # SQLite: sqlite:///filename.db + # Postgres: postgres://username:password@hostname/dbname + database: sqlite:////data/sqlite.db + # Additional arguments for asyncpg.create_pool() or sqlite3.connect() + # https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool + # https://docs.python.org/3/library/sqlite3.html#sqlite3.connect + # For sqlite, min_size is used as the connection thread pool size and max_size is ignored. + database_opts: + min_size: 5 + max_size: 10 + + # The unique ID of this appservice. + id: signal + # Username of the appservice bot. + bot_username: signalbot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + bot_displayname: Signal bridge bot + bot_avatar: mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp + + # Whether or not to receive ephemeral events via appservice transactions. + # Requires MSC2409 support (i.e. Synapse 1.22+). + # You should disable bridge -> sync_with_custom_puppets when this is enabled. + ephemeral_events: false + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + +# Prometheus telemetry config. Requires prometheus-client to be installed. +metrics: + enabled: false + listen_port: 8000 + +# Manhole config. +manhole: + # Whether or not opening the manhole is allowed. + enabled: false + # The path for the unix socket. + path: /var/tmp/mautrix-signal.manhole + # The list of UIDs who can be added to the whitelist. + # If empty, any UIDs can be specified in the open-manhole command. + whitelist: + - 0 + +signal: + # Path to signald unix socket + socket_path: /var/run/signald/signald.sock + # Directory for temp files when sending files to Signal. This should be an + # absolute path that signald can read. For attachments in the other direction, + # make sure signald is configured to use an absolute path as the data directory. + outgoing_attachment_dir: /tmp + # Directory where signald stores avatars for groups. + avatar_dir: ~/.config/signald/avatars + # Directory where signald stores auth data. Used to delete data when logging out. + data_dir: ~/.config/signald/data + # Whether or not unknown signald accounts should be deleted when the bridge is started. + # When this is enabled, any UserInUse errors should be resolved by restarting the bridge. + delete_unknown_accounts_on_start: false + # Whether or not message attachments should be removed from disk after they're bridged. + remove_file_after_handling: true + # Whether or not users can register a primary device + registration_enabled: true + # Whether or not to enable disappearing messages in groups. If enabled, then the expiration + # time of the messages will be determined by the first users to read the message, rather + # than individually. If the bridge has a single user, this can be turned on safely. + enable_disappearing_messages_in_groups: false + +# Bridge config +bridge: + # Localpart template of MXIDs for Signal users. + # {userid} is replaced with an identifier for the Signal user. + username_template: "signal_{userid}" + # Displayname template for Signal users. + # {displayname} is replaced with the displayname of the Signal user, which is the first + # available variable in displayname_preference. The variables in displayname_preference + # can also be used here directly. + displayname_template: "{displayname} (Signal)" + # Whether or not contact list displaynames should be used. + # Possible values: disallow, allow, prefer + # + # Multi-user instances are recommended to disallow contact list names, as otherwise there can + # be conflicts between names from different users' contact lists. + contact_list_names: disallow + # Available variables: full_name, first_name, last_name, phone, uuid + displayname_preference: + - full_name + - phone + + # Whether or not to create portals for all groups on login/connect. + autocreate_group_portal: true + # Whether or not to create portals for all contacts on login/connect. + autocreate_contact_portal: false + # Whether or not to use /sync to get read receipts and typing notifications + # when double puppeting is enabled + sync_with_custom_puppets: true + # Whether or not to update the m.direct account data event when double puppeting is enabled. + # Note that updating the m.direct event is not atomic (except with mautrix-asmux) + # and is therefore prone to race conditions. + sync_direct_chat_list: false + # Allow using double puppeting from any server with a valid client .well-known file. + double_puppet_allow_discovery: false + # Servers to allow double puppeting from, even if double_puppet_allow_discovery is false. + double_puppet_server_map: + example.com: https://example.com + # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth + # + # If set, custom puppets will be enabled automatically for local users + # instead of users having to find an access token and run `login-matrix` + # manually. + # If using this for other servers than the bridge's server, + # you must also set the URL in the double_puppet_server_map. + login_shared_secret_map: + example.com: foo + # Whether or not created rooms should have federation enabled. + # If false, created portal rooms will never be federated. + federate_rooms: true + # End-to-bridge encryption support options. + # + # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + default: false + # Options for automatic key sharing. + key_sharing: + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow: false + # Require the requesting device to have a valid cross-signing signature? + # This doesn't require that the bridge has verified the device, only that the user has verified it. + # Not yet implemented. + require_cross_signing: false + # Require devices to be verified by the bridge? + # Verification by the bridge is not yet implemented. + require_verification: true + # Whether or not to explicitly set the avatar and room name for private + # chat portal rooms. This will be implicitly enabled if encryption.default is true. + private_chat_portal_meta: false + # Whether or not the bridge should send a read receipt from the bridge bot when a message has + # been sent to Signal. This let's you check manually whether the bridge is receiving your + # messages. + # Note that this is not related to Signal delivery receipts. + delivery_receipts: false + # Whether or not delivery errors should be reported as messages in the Matrix room. (not yet implemented) + delivery_error_reports: false + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. + # This field will automatically be changed back to false after it, + # except if the config file is not writable. + resend_bridge_info: false + # Interval at which to resync contacts (in seconds). + periodic_sync: 0 + # Should leaving the room on Matrix make the user leave on Signal? + bridge_matrix_leave: true + + # Provisioning API part of the web server for automated portal creation and fetching information. + # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager). + provisioning: + # Whether or not the provisioning API should be enabled. + enabled: true + # The prefix to use in the provisioning API endpoints. + prefix: /_matrix/provision + # The shared secret to authorize users of the API. + # Set to "generate" to generate and save a new token. + shared_secret: generate + # Segment API key to enable analytics tracking for web server + # endpoints. Set to null to disable. + # Currently the only events are login start, QR code scan, and login + # success/failure. + segment_key: null + + # The prefix for commands. Only required in non-management rooms. + command_prefix: "!signal" + + # Messages sent upon joining a management room. + # Markdown is supported. The defaults are listed below. + management_room_text: + # Sent when joining a room. + welcome: "Hello, I'm a Signal bridge bot." + # Sent when joining a management room and the user is already logged in. + welcome_connected: "Use `help` for help." + # Sent when joining a management room and the user is not logged in. + welcome_unconnected: "Use `help` for help or `link` to log in." + # Optional extra text sent when joining a management room. + additional_help: "" + + # Send each message separately (for readability in some clients) + management_room_multiple_messages: false + + # Permissions for using the bridge. + # Permitted values: + # relay - Allowed to be relayed through the bridge, no access to commands. + # user - Use the bridge with puppeting. + # admin - Use and administrate the bridge. + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": "relay" + "example.com": "user" + "@admin:example.com": "admin" + + relay: + # Whether relay mode should be allowed. If allowed, `!signal set-relay` can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # The formats to use when sending messages to Signal via a relay user. + # + # Available variables: + # $sender_displayname - The display name of the sender (e.g. Example User) + # $sender_username - The username (Matrix ID localpart) of the sender (e.g. exampleuser) + # $sender_mxid - The Matrix ID of the sender (e.g. @exampleuser:example.com) + # $message - The message content + message_formats: + m.text: '$sender_displayname: $message' + m.notice: '$sender_displayname: $message' + m.emote: '* $sender_displayname $message' + m.file: '$sender_displayname sent a file' + m.image: '$sender_displayname sent an image' + m.audio: '$sender_displayname sent an audio file' + m.video: '$sender_displayname sent a video' + m.location: '$sender_displayname sent a location' + +# Python logging configuration. +# +# See section 16.7.2 of the Python documentation for more info: +# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema +logging: + version: 1 + formatters: + colored: + (): mautrix_signal.util.ColorFormatter + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + normal: + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: normal + filename: ./mautrix-signal.log + maxBytes: 10485760 + backupCount: 10 + console: + class: logging.StreamHandler + formatter: colored + loggers: + mau: + level: DEBUG + aiohttp: + level: INFO + root: + level: DEBUG + handlers: [file, console] + diff --git a/examples/05-deploying-mautrixsignal/B-using-existing-configmap/synapse.yaml b/examples/05-deploying-mautrixsignal/B-using-existing-configmap/synapse.yaml new file mode 100644 index 0000000..e806b8d --- /dev/null +++ b/examples/05-deploying-mautrixsignal/B-using-existing-configmap/synapse.yaml @@ -0,0 +1,14 @@ +apiVersion: synapse.opdev.io/v1alpha1 +kind: Synapse +metadata: + name: synapse-with-mautrixsignal +spec: + homeserver: + values: + serverName: example2.com + reportStats: true + bridges: + mautrixSignal: + enabled: true + configMap: + name: my-custom-mautrixsignal-config diff --git a/examples/README.md b/examples/README.md index f9853b4..0213f72 100644 --- a/examples/README.md +++ b/examples/README.md @@ -116,7 +116,7 @@ An example is available under the `02-using-existing-configmap` directory. To ru it: ```shell -$ kubectl create configmap my-custom-homeserver --from-file=02-using-existing-configmap/homeserver.yaml +$ kubectl create configmap my-custom-homeserver --from-file=examples/02-using-existing-configmap/homeserver.yaml configmap/my-custom-homeserver created $ kubectl apply -f examples/02-using-existing-configmap/synapse.yaml synapse.synapse.opdev.io/using-existing-configmap created @@ -177,10 +177,21 @@ configmap/synapse-with-postgresql-ssh-config 2 98s ## Deploying a bridge -For now, only the deployment of the -[Heisenbridge](https://github.com/hifi/heisenbridge) is supported. +The synapse Operator supports the deployment of: +* [Heisenbridge](https://github.com/hifi/heisenbridge): a bouncer-style IRC + bridge. +* [mautrix-signal](https://github.com/mautrix/signal): a double-pupetting + bridge for Signal. -### Using the default Heisenbridge configuration +The bridges don't have any dependency to one another. You can choose to deploy +one, several, or none of them. + +For both bridges, you can choose between using the default configuration file +and providing your custom configuration file. + +### Using the default configuration file + +In this case, the Synapse operator provides default configuration values. An example of a `Synapse` resource using the default Heisenbridge configuration is available under the `04-deploying-heisenbridge/A-default-configuration` @@ -213,16 +224,22 @@ configmap/synapse-with-heisenbridge 1 22s configmap/synapse-with-heisenbridge-heisenbridge 1 22s ``` -### Using an existing `heisenbridge.yaml` configuration file +A similar example for mautrix-signal is available under the +`05-deploying-mautrixsignal/A-default-configuration` directory. + +### Providing a custom configuration file -If the default `heisenbridge.yaml` doesn't answer your needs, you can use a -custom configuration file. You first have to add your custom -`heisenbridge.yaml` to a `ConfigMap` and configure the `Heisenbridge.ConfigMap` -section of the `Synapse` resource to reference the `ConfigMap`, as illustrated -in the `04-deploying-heisenbridge/B-using-existing-configmap` directory: +If the default configuration file doesn't answer your needs, you can use a +custom configuration file. You first have to add your custom config file +(for instance `heisenbridge.yaml`) to a `ConfigMap` and configure the +corresponding section of the `Synapse` resource (for instance +`Heisenbridge.ConfigMap`) to reference the `ConfigMap`. + +For heisenbridge, this is illustrated in the +`04-deploying-heisenbridge/B-using-existing-configmap` directory: ```shell -$ kubectl create configmap my-custom-heisenbridge --from-file=04-deploying-heisenbridge/B-using-existing-configmap/heisenbridge.yaml +$ kubectl create configmap my-custom-heisenbridge --from-file=examples/04-deploying-heisenbridge/B-using-existing-configmap/heisenbridge.yaml configmap/my-custom-heisenbridge created $ kubectl apply -f examples/04-deploying-heisenbridge/B-using-existing-configmap/synapse.yaml synapse.synapse.opdev.io/synapse-with-heisenbridge created @@ -248,4 +265,7 @@ configmap/kube-root-ca.crt 1 32d configmap/my-custom-heisenbridge 1 92s configmap/openshift-service-ca.crt 1 32d configmap/synapse-with-heisenbridge 1 78s -``` \ No newline at end of file +``` + +A similar example for mautrix-signal is available under the +`05-deploying-mautrixsignal/B-using-existing-configmap` directory. \ No newline at end of file