]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw/notification: support GetTopicAttributes API 38171/head
authorYuval Lifshitz <ylifshit@redhat.com>
Wed, 18 Nov 2020 16:43:16 +0000 (18:43 +0200)
committerYuval Lifshitz <ylifshit@redhat.com>
Wed, 25 Nov 2020 09:23:52 +0000 (11:23 +0200)
fixes: https://tracker.ceph.com/issues/46296

Signed-off-by: Yuval Lifshitz <ylifshit@redhat.com>
PendingReleaseNotes
doc/radosgw/notifications.rst
doc/radosgw/pubsub-module.rst
src/rgw/rgw_pubsub.cc
src/rgw/rgw_pubsub.h
src/rgw/rgw_rest_pubsub.cc
src/rgw/rgw_rest_pubsub.h
src/test/rgw/rgw_multi/tests_ps.py
src/test/rgw/rgw_multi/zone_ps.py

index 4684d9d03d3c83be1f33e1a8f9a22e23f20cd67a..ead538cb0b1ac82cefbcd68001b2ba58600988cc 100644 (file)
@@ -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
 --------
index 7a4b9d070669c3fd5b14d4a1d0a38520395b9d4b..28c549b6923705c7b1ad5c88e17f690a5ac80a63 100644 (file)
@@ -188,10 +188,68 @@ The topic ARN in the response will have the following format:
 
    arn:aws:sns:<zone-group>:<tenant>:<topic>
 
+Get Topic Attributes
+````````````````````
+
+Returns information about a specific topic. This includes push-endpoint information, if provided.
+
+::
+
+   POST
+
+   Action=GetTopicAttributes
+   &TopicArn=<topic-arn>
+
+Response will have the following format:
+
+::
+
+    <GetTopicAttributesResponse>
+        <GetTopicAttributesRersult>
+            <Attributes>
+                <entry>
+                    <key>User</key>
+                    <value></value>
+                </entry> 
+                <entry>
+                    <key>Name</key>
+                    <value></value>
+                </entry> 
+                <entry>
+                    <key>EndPoint</key>
+                    <value></value>
+                </entry> 
+                <entry>
+                    <key>TopicArn</key>
+                    <value></value>
+                </entry> 
+                <entry>
+                    <key>OpaqueData</key>
+                    <value></value>
+                </entry> 
+            </Attributes>
+        </GetTopicAttributesResult>
+        <ResponseMetadata>
+            <RequestId></RequestId>
+        </ResponseMetadata>
+    </GetTopicAttributesResponse>
+
+- 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:
                     <EndpointAddress></EndpointAddress>
                     <EndpointArgs></EndpointArgs>
                     <EndpointTopic></EndpointTopic>
+                    <HasStoredSecret></HasStoredSecret>
+                    <Persistent></Persistent>
                 </EndPoint>
                 <TopicArn></TopicArn>
                 <OpaqueData></OpaqueData>
@@ -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
 ````````````
index d39ab3e84a849cc57bd4bac310d8206e5b41290a..c92a2b4ee5ec7523b63153c85c369cb8cd0fac9b 100644 (file)
@@ -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":""
index d3a27807c3b66c0dfb03965db2b3a6f0d5bce9ce..d1a58dd5e12803b434ad4ce6c9b8c9cb0e30ea8b 100644 (file)
@@ -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);
index 8c76a4df2010f3de3488efe8af8be425d8465395..809e59f1ea757e3c475abeb26023730fd020da14 100644 (file)
@@ -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());
index d6de68c2cf16c15d22a25e6d605c0abdac775399..a232500e6ae4a67f0757d747f0a429ca6ae7ab9e 100644 (file)
@@ -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=<topic-arn>
+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=<topic-arn>
@@ -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);
 }
 
index 7e31642b3f4fa1d6e4e2578156c2c7d662f181da..3b1a1bc9670b8e92d3bf24a5c18d28badd0dbf12 100644 (file)
@@ -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:
index 269d358f529a658511bb5a148a8d6ea676e8573f..d074644a5ea20c444312b223b230fe504e89dd9a 100644 (file)
@@ -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()
index 241bbe8a26e2faec580e80d111afd82f5ee00031..9f55626d21181f796c150fc80f9ec0715df9d3c5 100644 (file)
@@ -187,6 +187,7 @@ class PSTopicS3:
     POST ?Action=CreateTopic&Name=<topic name>[&OpaqueData=<data>[&push-endpoint=<endpoint>&[<arg1>=<value1>...]]]
     POST ?Action=ListTopics
     POST ?Action=GetTopic&TopicArn=<topic-arn>
+    POST ?Action=GetTopicAttributes&TopicArn=<topic-arn>
     POST ?Action=DeleteTopic&TopicArn=<topic-arn>
     """
     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)