Add topic metadata for mqtt input binding and support user defined topic for mqtt output binding (#1674)

* feat(bindings/mqtt): add data incoming topic to metadata

Signed-off-by: lotuc <lotu.c@outlook.com>

* feat(bindings/mqtt): support user defined topic on create action

Signed-off-by: lotuc <lotu.c@outlook.com>

* chore(bindings/mqtt): add integration test and topic response check test

Signed-off-by: lotuc <lotu.c@outlook.com>

* fix(bindings/mqtt): ignore misspell linting error for word mosquitto

Signed-off-by: lotuc <lotu.c@outlook.com>

Co-authored-by: Yaron Schneider <schneider.yaron@live.com>
This commit is contained in:
lotuc 2022-05-12 03:11:51 +08:00 committed by GitHub
parent 8b5554dc71
commit 5e4bd89afd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 197 additions and 3 deletions

View File

@ -204,8 +204,13 @@ func (m *MQTT) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindin
bo = backoff.WithContext(bo, ctx)
return nil, retry.NotifyRecover(func() error {
m.logger.Debugf("mqtt publishing topic %s with data: %v", m.metadata.topic, req.Data)
token := m.producer.Publish(m.metadata.topic, m.metadata.qos, m.metadata.retain, req.Data)
topic, ok := req.Metadata[mqttTopic]
if !ok || topic == "" {
// If user does not specify a topic, publish via the component's default topic.
topic = m.metadata.topic
}
m.logger.Debugf("mqtt publishing topic %s with data: %v", topic, req.Data)
token := m.producer.Publish(topic, m.metadata.qos, m.metadata.retain, req.Data)
if !token.WaitTimeout(defaultWait) || token.Error() != nil {
return fmt.Errorf("mqtt error from publish: %v", token.Error())
}
@ -218,7 +223,10 @@ func (m *MQTT) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindin
}
func (m *MQTT) handleMessage(handler func(context.Context, *bindings.ReadResponse) ([]byte, error), mqttMsg mqtt.Message) error {
msg := bindings.ReadResponse{Data: mqttMsg.Payload()}
msg := bindings.ReadResponse{
Data: mqttMsg.Payload(),
Metadata: map[string]string{mqttTopic: mqttMsg.Topic()},
}
// paho.mqtt.golang requires that handlers never block or it can deadlock on client.Disconnect.
// To ensure that the Dapr runtime does not hang on teardown on of the component, run the app's

View File

@ -0,0 +1,114 @@
package mqtt
import (
"context"
"os"
"testing"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/kit/logger"
)
const (
// Environment variable containing the host name for MQTT integration tests
// To run using docker:
// Create mosquitto.conf with content:
// listener 1883
// allow_anonymous true
// And run:
// nolint:misspell
// docker run -d -v mosquitto.conf:/mosquitto/config/mosquitto.conf --name test-mqtt -p 1883:1883 eclipse-mosquitto:2
// In that case the connection string will be: tcp://127.0.0.1:1883
testMQTTConnectionStringEnvKey = "DAPR_TEST_MQTT_URL"
)
func getConnectionString() string {
return os.Getenv(testMQTTConnectionStringEnvKey)
}
func TestInvokeWithTopic(t *testing.T) {
t.Parallel()
url := getConnectionString()
if url == "" {
t.Skipf("MQTT connection string configuration must be set in environment variable '%s' (example 'tcp://localhost:1883')", testMQTTConnectionStringEnvKey)
}
topicDefault := "/app/default"
const msgDefault = "hello from default"
dataDefault := []byte(msgDefault)
topicCustomized := "/app/customized"
const msgCustomized = "hello from customized"
dataCustomized := []byte(msgCustomized)
metadata := bindings.Metadata{
Name: "testQueue",
Properties: map[string]string{
"consumerID": uuid.NewString(),
"url": url,
"topic": topicDefault,
"qos": "1",
"retain": "false",
"cleanSession": "true",
"backOffMaxRetries": "0",
},
}
logger := logger.NewLogger("test")
r := NewMQTT(logger)
err := r.Init(metadata)
assert.Nil(t, err)
conn, err := r.connect(uuid.NewString())
assert.Nil(t, err)
defer conn.Disconnect(1)
msgCh := make(chan interface{})
defer close(msgCh)
token := conn.Subscribe("/app/#", 1, func(client mqtt.Client, mqttMsg mqtt.Message) {
msgCh <- mqttMsg
})
ok := token.WaitTimeout(2 * time.Second)
assert.True(t, ok, "subscribe to /app/# timeout")
err = token.Error()
assert.Nil(t, err, "error subscribe to test topic")
// Timeout in case message transfer error.
go func() {
time.Sleep(5 * time.Second)
msgCh <- "timeout"
}()
// Test invoke with default topic configured for component.
_, err = r.Invoke(context.TODO(), &bindings.InvokeRequest{Data: dataDefault})
assert.Nil(t, err)
m := <-msgCh
mqttMessage, ok := m.(mqtt.Message)
assert.True(t, ok)
assert.Equal(t, dataDefault, mqttMessage.Payload())
assert.Equal(t, topicDefault, mqttMessage.Topic())
// Test invoke with customized topic.
_, err = r.Invoke(context.TODO(), &bindings.InvokeRequest{
Data: dataCustomized,
Metadata: map[string]string{
mqttTopic: topicCustomized,
},
})
assert.Nil(t, err)
m = <-msgCh
mqttMessage, ok = m.(mqtt.Message)
assert.True(t, ok)
assert.Equal(t, dataCustomized, mqttMessage.Payload())
assert.Equal(t, topicCustomized, mqttMessage.Topic())
}

View File

@ -14,6 +14,7 @@ limitations under the License.
package mqtt
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
@ -22,6 +23,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/kit/logger"
)
func getFakeProperties() map[string]string {
@ -187,4 +189,74 @@ func TestParseMetadata(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, m.tlsCfg.clientKey, "failed to parse valid client certificate key")
})
t.Run("Response returns the topic that the subscribed data is from.", func(t *testing.T) {
const msg = "hello world"
payload := []byte(msg)
topic := "/topic/where/the/data/is/from"
logger := logger.NewLogger("test")
m := NewMQTT(logger)
m.ctx, m.cancel = context.WithCancel(context.Background())
m.handleMessage(func(ctx context.Context, r *bindings.ReadResponse) ([]byte, error) {
assert.Equal(t, payload, r.Data)
metadata := r.Metadata
responseTopic, ok := metadata[mqttTopic]
assert.True(t, ok)
assert.Equal(t, topic, responseTopic)
return r.Data, nil
}, &mqttMockMessage{
topic: topic,
payload: payload,
})
})
}
type mqttMockMessage struct {
duplicate bool
qos byte
retained bool
topic string
mqttMockMessageID uint16
payload []byte
ackCalled bool
noautoack bool
}
func (m *mqttMockMessage) Duplicate() bool {
return m.duplicate
}
func (m *mqttMockMessage) Qos() byte {
return m.qos
}
func (m *mqttMockMessage) Retained() bool {
return m.retained
}
func (m *mqttMockMessage) Topic() string {
return m.topic
}
func (m *mqttMockMessage) MessageID() uint16 {
return m.mqttMockMessageID
}
func (m *mqttMockMessage) Payload() []byte {
return m.payload
}
func (m *mqttMockMessage) Ack() {
m.ackCalled = true
}
func (m *mqttMockMessage) NoAutoAck() bool {
return m.noautoack
}
func (m *mqttMockMessage) AutoAckOff() {
m.noautoack = true
}