From: Yuval Lifshitz Date: Wed, 18 Nov 2020 16:43:16 +0000 (+0200) Subject: rgw/notification: support GetTopicAttributes API X-Git-Tag: v16.1.0~411^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=3906884aa66b7b6c976d6165cc3b5dfaa8f754c4;p=ceph.git rgw/notification: support GetTopicAttributes API fixes: https://tracker.ceph.com/issues/46296 Signed-off-by: Yuval Lifshitz --- diff --git a/PendingReleaseNotes b/PendingReleaseNotes index 4684d9d03d3..ead538cb0b1 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -33,6 +33,8 @@ * MGR: progress module can now be turned on/off, using the commands: ``ceph progress on`` and ``ceph progress off``. +* An AWS-compliant API: "GetTopicAttributes" was added to replace the existing "GetTopic" API. The new API + should be used to fetch information about topics used for bucket notifications. >=15.0.0 -------- diff --git a/doc/radosgw/notifications.rst b/doc/radosgw/notifications.rst index 7a4b9d07066..28c549b6923 100644 --- a/doc/radosgw/notifications.rst +++ b/doc/radosgw/notifications.rst @@ -188,10 +188,68 @@ The topic ARN in the response will have the following format: arn:aws:sns::: +Get Topic Attributes +```````````````````` + +Returns information about a specific topic. This includes push-endpoint information, if provided. + +:: + + POST + + Action=GetTopicAttributes + &TopicArn= + +Response will have the following format: + +:: + + + + + + User + + + + Name + + + + EndPoint + + + + TopicArn + + + + OpaqueData + + + + + + + + + +- User: name of the user that created the topic +- Name: name of the topic +- EndPoint: JSON formatted endpoint parameters, including: + - EndpointAddress: the push-endpoint URL + - EndpointArgs: the push-endpoint args + - EndpointTopic: the topic name that should be sent to the endpoint (may be different than the above topic name) + - HasStoredSecret: "true" if if endpoint URL contain user/password information. In this case request must be made over HTTPS. If not, topic get request will be rejected + - Persistent: "true" is topic is persistent +- TopicArn: topic ARN +- OpaqueData: the opaque data set on the topic + Get Topic Information ````````````````````` Returns information about specific topic. This includes push-endpoint information, if provided. +Note that this API is now deprecated in favor of the AWS compliant `GetTopicAttributes` API. :: @@ -213,6 +271,8 @@ Response will have the following format: + + @@ -226,10 +286,12 @@ Response will have the following format: - User: name of the user that created the topic - Name: name of the topic - EndpointAddress: the push-endpoint URL -- if endpoint URL contain user/password information, request must be made over HTTPS. If not, topic get request will be rejected. - EndpointArgs: the push-endpoint args -- EndpointTopic: the topic name that should be sent to the endpoint (mat be different than the above topic name) +- EndpointTopic: the topic name that should be sent to the endpoint (may be different than the above topic name) +- HasStoredSecret: "true" if endpoint URL contain user/password information. In this case request must be made over HTTPS. If not, topic get request will be rejected +- Persistent: "true" is topic is persistent - TopicArn: topic ARN +- OpaqueData: the opaque data set on the topic Delete Topic ```````````` diff --git a/doc/radosgw/pubsub-module.rst b/doc/radosgw/pubsub-module.rst index d39ab3e84a8..c92a2b4ee5e 100644 --- a/doc/radosgw/pubsub-module.rst +++ b/doc/radosgw/pubsub-module.rst @@ -276,7 +276,9 @@ Response will have the following format (JSON): "oid_prefix":"", "push_endpoint":"", "push_endpoint_args":"", - "push_endpoint_topic":"" + "push_endpoint_topic":"", + "stored_secret":"", + "persistent":"" }, "arn":"" "opaqueData":"" diff --git a/src/rgw/rgw_pubsub.cc b/src/rgw/rgw_pubsub.cc index d3a27807c3b..d1a58dd5e12 100644 --- a/src/rgw/rgw_pubsub.cc +++ b/src/rgw/rgw_pubsub.cc @@ -312,6 +312,26 @@ void rgw_pubsub_topic::dump_xml(Formatter *f) const encode_xml("OpaqueData", opaque_data, f); } +void encode_xml_key_value_entry(const std::string& key, const std::string& value, Formatter *f) { + f->open_object_section("entry"); + encode_xml("key", key, f); + encode_xml("value", value, f); + f->close_section(); // entry +} + +void rgw_pubsub_topic::dump_xml_as_attributes(Formatter *f) const +{ + f->open_array_section("Attributes"); + std::string str_user; + user.to_str(str_user); + encode_xml_key_value_entry("User", str_user, f); + encode_xml_key_value_entry("Name", name, f); + encode_xml_key_value_entry("EndPoint", dest.to_json_str(), f); + encode_xml_key_value_entry("TopicArn", arn, f); + encode_xml_key_value_entry("OpaqueData", opaque_data, f); + f->close_section(); // Attributes +} + void encode_json(const char *name, const rgw::notify::EventTypeList& l, Formatter *f) { f->open_array_section(name); @@ -378,6 +398,23 @@ void rgw_pubsub_sub_dest::dump_xml(Formatter *f) const encode_xml("Persistent", persistent, f); } +std::string rgw_pubsub_sub_dest::to_json_str() const +{ + // first 2 members are omitted here since they + // dont apply to AWS compliant topics + JSONFormatter f; + f.open_object_section(""); + encode_json("EndpointAddress", push_endpoint, &f); + encode_json("EndpointArgs", push_endpoint_args, &f); + encode_json("EndpointTopic", arn_topic, &f); + encode_json("HasStoredSecret", stored_secret, &f); + encode_json("Persistent", persistent, &f); + f.close_section(); + std::stringstream ss; + f.flush(ss); + return ss.str(); +} + void rgw_pubsub_sub_config::dump(Formatter *f) const { encode_json("user", user, f); diff --git a/src/rgw/rgw_pubsub.h b/src/rgw/rgw_pubsub.h index 8c76a4df201..809e59f1ea7 100644 --- a/src/rgw/rgw_pubsub.h +++ b/src/rgw/rgw_pubsub.h @@ -403,6 +403,7 @@ struct rgw_pubsub_sub_dest { void dump(Formatter *f) const; void dump_xml(Formatter *f) const; + std::string to_json_str() const; }; WRITE_CLASS_ENCODER(rgw_pubsub_sub_dest) @@ -476,6 +477,7 @@ struct rgw_pubsub_topic { void dump(Formatter *f) const; void dump_xml(Formatter *f) const; + void dump_xml_as_attributes(Formatter *f) const; bool operator<(const rgw_pubsub_topic& t) const { return to_str().compare(t.to_str()); diff --git a/src/rgw/rgw_rest_pubsub.cc b/src/rgw/rgw_rest_pubsub.cc index d6de68c2cf1..a232500e6ae 100644 --- a/src/rgw/rgw_rest_pubsub.cc +++ b/src/rgw/rgw_rest_pubsub.cc @@ -21,6 +21,7 @@ #define dout_context g_ceph_context #define dout_subsys ceph_subsys_rgw +static const char* AWS_SNS_NS("https://sns.amazonaws.com/doc/2010-03-31/"); // command (AWS compliant): // POST @@ -86,14 +87,14 @@ public: } const auto f = s->formatter; - f->open_object_section_in_ns("CreateTopicResponse", "https://sns.amazonaws.com/doc/2010-03-31/"); + f->open_object_section_in_ns("CreateTopicResponse", AWS_SNS_NS); f->open_object_section("CreateTopicResult"); encode_xml("TopicArn", topic_arn, f); - f->close_section(); + f->close_section(); // CreateTopicResult f->open_object_section("ResponseMetadata"); encode_xml("RequestId", s->req_id, f); - f->close_section(); - f->close_section(); + f->close_section(); // ResponseMetadata + f->close_section(); // CreateTopicResponse rgw_flush_formatter_and_reset(s, f); } }; @@ -115,14 +116,14 @@ public: } const auto f = s->formatter; - f->open_object_section_in_ns("ListTopicsResponse", "https://sns.amazonaws.com/doc/2010-03-31/"); + f->open_object_section_in_ns("ListTopicsResponse", AWS_SNS_NS); f->open_object_section("ListTopicsResult"); encode_xml("Topics", result, f); - f->close_section(); + f->close_section(); // ListTopicsResult f->open_object_section("ResponseMetadata"); encode_xml("RequestId", s->req_id, f); - f->close_section(); - f->close_section(); + f->close_section(); // ResponseMetadat + f->close_section(); // ListTopicsResponse rgw_flush_formatter_and_reset(s, f); } }; @@ -168,6 +169,47 @@ public: } }; +// command (AWS compliant): +// POST +// Action=GetTopicAttributes&TopicArn= +class RGWPSGetTopicAttributes_ObjStore_AWS : public RGWPSGetTopicOp { +public: + int get_params() override { + const auto topic_arn = rgw::ARN::parse((s->info.args.get("TopicArn"))); + + if (!topic_arn || topic_arn->resource.empty()) { + ldout(s->cct, 1) << "GetTopicAttribute Action 'TopicArn' argument is missing or invalid" << dendl; + return -EINVAL; + } + + topic_name = topic_arn->resource; + return 0; + } + + void send_response() override { + if (op_ret) { + set_req_state_err(s, op_ret); + } + dump_errno(s); + end_header(s, this, "application/xml"); + + if (op_ret < 0) { + return; + } + + const auto f = s->formatter; + f->open_object_section_in_ns("GetTopicAttributesResponse", AWS_SNS_NS); + f->open_object_section("GetTopicAttributesResult"); + result.topic.dump_xml_as_attributes(f); + f->close_section(); // GetTopicAttributesResult + f->open_object_section("ResponseMetadata"); + encode_xml("RequestId", s->req_id, f); + f->close_section(); // ResponseMetadata + f->close_section(); // GetTopicAttributesResponse + rgw_flush_formatter_and_reset(s, f); + } +}; + // command (AWS compliant): // POST // Action=DeleteTopic&TopicArn= @@ -210,11 +252,11 @@ public: } const auto f = s->formatter; - f->open_object_section_in_ns("DeleteTopicResponse", "https://sns.amazonaws.com/doc/2010-03-31/"); + f->open_object_section_in_ns("DeleteTopicResponse", AWS_SNS_NS); f->open_object_section("ResponseMetadata"); encode_xml("RequestId", s->req_id, f); - f->close_section(); - f->close_section(); + f->close_section(); // ResponseMetadata + f->close_section(); // DeleteTopicResponse rgw_flush_formatter_and_reset(s, f); } }; @@ -344,16 +386,14 @@ RGWOp* RGWHandler_REST_PSTopic_AWS::op_post() { return new RGWPSListTopics_ObjStore_AWS(); if (action.compare("GetTopic") == 0) return new RGWPSGetTopic_ObjStore_AWS(); + if (action.compare("GetTopicAttributes") == 0) + return new RGWPSGetTopicAttributes_ObjStore_AWS(); } return nullptr; } int RGWHandler_REST_PSTopic_AWS::authorize(const DoutPrefixProvider* dpp, optional_yield y) { - /*if (s->info.args.exists("Action") && s->info.args.get("Action").find("Topic") != std::string::npos) { - // TODO: some topic specific authorization - return 0; - }*/ return RGW_Auth_S3::authorize(dpp, store, auth_registry, s, y); } diff --git a/src/rgw/rgw_rest_pubsub.h b/src/rgw/rgw_rest_pubsub.h index 7e31642b3f4..3b1a1bc9670 100644 --- a/src/rgw/rgw_rest_pubsub.h +++ b/src/rgw/rgw_rest_pubsub.h @@ -27,7 +27,6 @@ class RGWHandler_REST_PSTopic_AWS : public RGWHandler_REST { const rgw::auth::StrategyRegistry& auth_registry; const std::string& post_body; void rgw_topic_parse_input(); - //static int init_from_header(struct req_state *s, int default_formatter, bool configurable_format); protected: RGWOp* op_post() override; public: diff --git a/src/test/rgw/rgw_multi/tests_ps.py b/src/test/rgw/rgw_multi/tests_ps.py index 269d358f529..d074644a5ea 100644 --- a/src/test/rgw/rgw_multi/tests_ps.py +++ b/src/test/rgw/rgw_multi/tests_ps.py @@ -873,6 +873,10 @@ def test_ps_s3_topic_on_master(): assert_equal(topic_arn, result['GetTopicResponse']['GetTopicResult']['Topic']['TopicArn']) assert_equal(endpoint_address, result['GetTopicResponse']['GetTopicResult']['Topic']['EndPoint']['EndpointAddress']) # Note that endpoint args may be ordered differently in the result + result = topic_conf3.get_attributes() + assert_equal(topic_arn, result['Attributes']['TopicArn']) + json_endpoint = json.loads(result['Attributes']['EndPoint']) + assert_equal(endpoint_address, json_endpoint['EndpointAddress']) # delete topic 1 result = topic_conf1.del_config() @@ -881,6 +885,12 @@ def test_ps_s3_topic_on_master(): # try to get a deleted topic _, status = topic_conf1.get_config() assert_equal(status, 404) + try: + topic_conf1.get_attributes() + except: + print('topic already deleted - this is expected') + else: + assert False, 'topic 1 should be deleted at this point' # get the remaining 2 topics result, status = topic_conf1.get_list() diff --git a/src/test/rgw/rgw_multi/zone_ps.py b/src/test/rgw/rgw_multi/zone_ps.py index 241bbe8a26e..9f55626d211 100644 --- a/src/test/rgw/rgw_multi/zone_ps.py +++ b/src/test/rgw/rgw_multi/zone_ps.py @@ -187,6 +187,7 @@ class PSTopicS3: POST ?Action=CreateTopic&Name=[&OpaqueData=[&push-endpoint=&[=...]]] POST ?Action=ListTopics POST ?Action=GetTopic&TopicArn= + POST ?Action=GetTopicAttributes&TopicArn= POST ?Action=DeleteTopic&TopicArn= """ def __init__(self, conn, topic_name, region, endpoint_args=None, opaque_data=None): @@ -239,6 +240,10 @@ class PSTopicS3: dict_response = xmltodict.parse(data) return dict_response, status + def get_attributes(self): + """get topic attributes""" + return self.client.get_topic_attributes(TopicArn=self.topic_arn) + def set_config(self): """set topic""" result = self.client.create_topic(Name=self.topic_name, Attributes=self.attributes)