0

Publish message

by
Published today

Produce one message to a Kafka topic and wait for the broker to acknowledge it. A string payload is sent as-is, null as a tombstone, anything else as JSON; non-string header values are JSON-encoded. Returns the partition and offset. SASL_GSSAPI (Kerberos) resources are not supported.

Script kafka Verified

The script

Submitted by hugo989 Python3
Verified 8 hours ago
1
# py312
2
# requirements:
3
# confluent-kafka==2.16.1
4

5
import json
6
import time
7
from typing import Optional, TypedDict
8

9
from confluent_kafka import KafkaError, KafkaException, Producer
10

11

12
class kafka(TypedDict):
13
    brokers: list
14
    security: dict
15

16

17
PROTOCOLS = {
18
    "PLAINTEXT": "plaintext",
19
    "SASL_PLAINTEXT": "sasl_plaintext",
20
    "SSL": "ssl",
21
    "SASL_SSL": "sasl_ssl",
22
    "SASL_SSL_OAUTHBEARER": "sasl_ssl",
23
}
24

25

26
def producer_config(auth: kafka) -> dict:
27
    # Same librdkafka settings as Windmill's Kafka trigger reads from this resource.
28
    security = auth.get("security") or {}
29
    label = security.get("label") or "PLAINTEXT"
30
    if "GSSAPI" in label:
31
        raise Exception(
32
            f"{label} is not supported: the Kafka client this script uses is built without Kerberos (GSSAPI)"
33
        )
34
    if label not in PROTOCOLS:
35
        raise Exception(f"Unknown security label {label!r}: expected one of {', '.join(PROTOCOLS)}")
36
    conf = {
37
        "bootstrap.servers": ",".join(auth["brokers"]),
38
        "security.protocol": PROTOCOLS[label],
39
        "message.timeout.ms": 30000,
40
    }
41
    if label in ("SASL_PLAINTEXT", "SASL_SSL"):
42
        conf["sasl.mechanism"] = security["mechanism"]
43
        conf["sasl.username"] = security["username"]
44
        conf["sasl.password"] = security["password"]
45
    if label == "SASL_SSL_OAUTHBEARER":
46
        conf["sasl.mechanism"] = "OAUTHBEARER"
47
        conf["sasl.oauthbearer.method"] = "oidc"
48
        conf["sasl.oauthbearer.client.id"] = security["client_id"]
49
        conf["sasl.oauthbearer.client.secret"] = security["client_secret"]
50
        conf["sasl.oauthbearer.token.endpoint.url"] = security["token_endpoint_url"]
51
        conf["https.ca.location"] = "probe"
52
        if security.get("scope"):
53
            conf["sasl.oauthbearer.scope"] = security["scope"]
54
        if security.get("extensions"):
55
            conf["sasl.oauthbearer.extensions"] = security["extensions"]
56
    for field, key in (
57
        ("ca", "ssl.ca.pem"),
58
        ("certificate", "ssl.certificate.pem"),
59
        ("key", "ssl.key.pem"),
60
        ("key_password", "ssl.key.password"),
61
    ):
62
        if security.get(field):
63
            conf[key] = security[field]
64
    return conf
65

66

67
def main(
68
    auth: kafka,
69
    topic: str,
70
    payload,
71
    key: Optional[str] = None,
72
    headers: Optional[dict] = None,
73
):
74
    # None is a tombstone (a null value, which deletes the key on a compacted topic).
75
    if payload is None:
76
        value = None
77
    else:
78
        value = (payload if isinstance(payload, str) else json.dumps(payload)).encode()
79
    errors = []  # client-level errors (bad credentials, TLS, unreachable brokers)
80
    producer = Producer({**producer_config(auth), "error_cb": errors.append})
81
    reports = []
82
    producer.produce(
83
        topic,
84
        value=value,
85
        key=str(key).encode() if key not in (None, "") else None,
86
        headers=[
87
            (k, (v if isinstance(v, str) else json.dumps(v)).encode())
88
            for k, v in (headers or {}).items()
89
        ]
90
        or None,
91
        on_delivery=lambda err, msg: reports.append((err, msg)),
92
    )
93
    # librdkafka retries refused connections until message.timeout.ms. Stop early only
94
    # on a fatal error, or when every broker is down and one refused the credentials:
95
    # a broker that is briefly unreachable, or one bad broker among several, may still
96
    # let the message through.
97
    deadline = time.monotonic() + 40
98
    while not reports and time.monotonic() < deadline:
99
        producer.poll(0.2)
100
        for e in errors:
101
            if e.fatal():
102
                raise KafkaException(e)
103
        auth_errors = [e for e in errors if e.code() == KafkaError._AUTHENTICATION]
104
        if auth_errors and any(e.code() == KafkaError._ALL_BROKERS_DOWN for e in errors):
105
            raise KafkaException(auth_errors[0])
106
    if not reports:
107
        raise Exception("Timed out waiting for the broker to acknowledge the message")
108
    err, msg = reports[0]
109
    if err is not None:
110
        # A timed-out delivery says nothing of why: add the last connection error.
111
        cause = [e for e in errors if e.code() != KafkaError._ALL_BROKERS_DOWN]
112
        raise Exception(err.str() + (f" (last error: {cause[-1].str()})" if cause else ""))
113
    return {"topic": msg.topic(), "partition": msg.partition(), "offset": msg.offset()}
114