This is an automated email from the ASF dual-hosted git repository.

alexey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kudu.git


The following commit(s) were added to refs/heads/master by this push:
     new 9b92708a6 Add OpenAPI specification for REST API endpoints
9b92708a6 is described below

commit 9b92708a6544e9c4119a471b8c881e3689d3a8ec
Author: gabriellalotz <[email protected]>
AuthorDate: Thu May 8 14:22:21 2025 +0000

    Add OpenAPI specification for REST API endpoints
    
    Create a hand-written swagger/kudu-api.json specification rather than
    implementing automated generation infrastructure. This approach was
    chosen for several practical reasons:
    
    - Kudu's REST API currently has only 3 main endpoints
      (/api/v1/tables, /api/v1/tables/<table_id>, /api/v1/leader) with ~5
      total method/path combinations. The overhead of automated generation
      (C++ fluent APIs, annotation parsers, or complex macros) would
      exceed the maintenance burden of manual updates.
    
    - These endpoints are relatively stable and not changing frequently,
      reducing the risk of documentation drift.
    
    - A single JSON file is easier to review, debug, and modify than
      custom C++ infrastructure or parsing scripts.
    
    - No additional build steps, external tools, or runtime overhead. The
      specification is immediately available when the server starts.
    
    - If the API grows significantly or changes frequently, we can revisit
      automated generation.
    
    The OpenAPI 3.0 specification includes complete documentation for all
    current endpoints with request/response schemas, parameter validation,
    and error codes. It's served at /swagger/kudu-api.json and integrates
    with the existing Swagger UI at /api/docs.
    
    A Jira has been created for adding an automatic validation script
    (https://issues.apache.org/jira/browse/KUDU-3700).
    
    Change-Id: I3981665c78f478e89d0300f3a2fc5d68b73b8762
    Reviewed-on: http://gerrit.cloudera.org:8080/23224
    Reviewed-by: Alexey Serbin <[email protected]>
    Tested-by: Alexey Serbin <[email protected]>
---
 src/kudu/master/CMakeLists.txt                |   2 +-
 src/kudu/master/rest_catalog-test.cc          |  91 +++
 src/kudu/master/rest_catalog_path_handlers.cc |  55 ++
 src/kudu/master/rest_catalog_path_handlers.h  |   4 +
 www/api/docs.mustache                         |  28 +
 www/swagger/kudu-api.json                     | 962 ++++++++++++++++++++++++++
 www/swagger/kudu-custom.css                   |  21 +
 www/swagger/kudu-swagger-init.js              |  70 ++
 www/swagger/swagger-ui-bundle.js              |   2 +
 www/swagger/swagger-ui.css                    |   1 +
 10 files changed, 1235 insertions(+), 1 deletion(-)

diff --git a/src/kudu/master/CMakeLists.txt b/src/kudu/master/CMakeLists.txt
index da2553b02..8aa16eaa2 100644
--- a/src/kudu/master/CMakeLists.txt
+++ b/src/kudu/master/CMakeLists.txt
@@ -145,7 +145,7 @@ ADD_KUDU_TEST(master-test RESOURCE_LOCK "master-web-port"
                           DATA_FILES ../scripts/first_argument.sh)
 ADD_KUDU_TEST(mini_master-test RESOURCE_LOCK "master-web-port")
 ADD_KUDU_TEST(placement_policy-test)
-ADD_KUDU_TEST(rest_catalog-test)
+ADD_KUDU_TEST(rest_catalog-test DATA_FILES ../../../www)
 ADD_KUDU_TEST(spnego_rest_catalog-test)
 ADD_KUDU_TEST(sys_catalog-test RESOURCE_LOCK "master-web-port")
 ADD_KUDU_TEST(ts_descriptor-test DATA_FILES ../scripts/first_argument.sh)
diff --git a/src/kudu/master/rest_catalog-test.cc 
b/src/kudu/master/rest_catalog-test.cc
index cf29ca0ad..8b1874ec3 100644
--- a/src/kudu/master/rest_catalog-test.cc
+++ b/src/kudu/master/rest_catalog-test.cc
@@ -23,6 +23,7 @@
 
 #include <gflags/gflags_declare.h>
 #include <gtest/gtest.h>
+#include <rapidjson/document.h>
 
 #include "kudu/client/client.h"
 #include "kudu/client/schema.h"
@@ -32,8 +33,11 @@
 #include "kudu/master/rest_catalog_test_base.h"
 #include "kudu/mini-cluster/internal_mini_cluster.h"
 #include "kudu/util/curl_util.h"
+#include "kudu/util/env.h"
 #include "kudu/util/faststring.h"
+#include "kudu/util/jsonreader.h"
 #include "kudu/util/net/sockaddr.h"
+#include "kudu/util/path_util.h"
 #include "kudu/util/regex.h"
 #include "kudu/util/status.h"
 #include "kudu/util/test_macros.h"
@@ -46,6 +50,7 @@ using kudu::client::KuduTable;
 using kudu::client::sp::shared_ptr;
 using kudu::cluster::InternalMiniCluster;
 using kudu::cluster::InternalMiniClusterOptions;
+using rapidjson::Value;
 using std::set;
 using std::string;
 using std::unique_ptr;
@@ -53,6 +58,7 @@ using std::vector;
 using strings::Substitute;
 
 DECLARE_bool(enable_rest_api);
+DECLARE_string(webserver_doc_root);
 
 namespace kudu {
 namespace master {
@@ -64,6 +70,11 @@ class RestCatalogTest : public RestCatalogTestBase {
     // Set REST endpoint flag to true
     FLAGS_enable_rest_api = true;
 
+    // Set webserver doc root to enable swagger UI and static file serving
+    string bin_path;
+    ASSERT_OK(Env::Default()->GetExecutablePath(&bin_path));
+    FLAGS_webserver_doc_root = JoinPathSegments(DirName(bin_path), 
"testdata/www");
+
     // Configure the mini-cluster
     cluster_.reset(new InternalMiniCluster(env_, 
InternalMiniClusterOptions()));
 
@@ -860,5 +871,85 @@ TEST_F(MultiMasterTest, TestGetLeaderEndpoint) {
   ASSERT_STR_CONTAINS(leader_buf.ToString(), "{\"tables\":[]}");
 }
 
+TEST_F(RestCatalogTest, TestApiSpecEndpoint) {
+  EasyCurl c;
+  faststring buf;
+  ASSERT_OK(c.FetchURL(
+      Substitute("http://$0/api/v1/spec";, 
cluster_->mini_master()->bound_http_addr().ToString()),
+      &buf));
+
+  string spec_json = buf.ToString();
+  ASSERT_FALSE(spec_json.empty()) << "API spec should not be empty";
+
+  JsonReader reader(spec_json);
+  ASSERT_OK(reader.Init());
+
+  // Helper lambda to verify an object exists in parent
+  auto VerifyObjectExists = [&reader](const Value* parent, const char* field) {
+    const Value* obj;
+    Status s = reader.ExtractObject(parent, field, &obj);
+    ASSERT_TRUE(s.ok()) << "Field '" << field << "' not found: " << 
s.ToString();
+  };
+
+  string openapi_version;
+  ASSERT_OK(reader.ExtractString(reader.root(), "openapi", &openapi_version));
+  ASSERT_FALSE(openapi_version.empty());
+
+  const Value* paths;
+  ASSERT_OK(reader.ExtractObject(reader.root(), "paths", &paths));
+
+  const Value* tables_path;
+  ASSERT_OK(reader.ExtractObject(paths, "/tables", &tables_path));
+  NO_FATALS(VerifyObjectExists(tables_path, "get"));
+  NO_FATALS(VerifyObjectExists(tables_path, "post"));
+
+  const Value* table_by_id_path;
+  ASSERT_OK(reader.ExtractObject(paths, "/tables/{table_id}", 
&table_by_id_path));
+  NO_FATALS(VerifyObjectExists(table_by_id_path, "get"));
+  NO_FATALS(VerifyObjectExists(table_by_id_path, "put"));
+  NO_FATALS(VerifyObjectExists(table_by_id_path, "delete"));
+
+  const Value* leader_path;
+  ASSERT_OK(reader.ExtractObject(paths, "/leader", &leader_path));
+  NO_FATALS(VerifyObjectExists(leader_path, "get"));
+
+  const Value* components;
+  ASSERT_OK(reader.ExtractObject(reader.root(), "components", &components));
+  const Value* schemas;
+  ASSERT_OK(reader.ExtractObject(components, "schemas", &schemas));
+
+  const vector<string> expected_schemas = {"TablesResponse", "TableInfo", 
"TableResponse",
+                                           "LeaderResponse", "ErrorResponse", 
"TableSchema",
+                                           "PartitionSchema", 
"CreateTableRequest",
+                                           "AlterTableRequest"};
+  for (const auto& schema_name : expected_schemas) {
+    NO_FATALS(VerifyObjectExists(schemas, schema_name.c_str()));
+  }
+}
+
+TEST_F(RestCatalogTest, TestApiDocsEndpoint) {
+  EasyCurl c;
+  faststring buf;
+  Status s = c.FetchURL(
+      Substitute("http://$0/api/docs";, 
cluster_->mini_master()->bound_http_addr().ToString()),
+      &buf);
+
+  ASSERT_TRUE(s.ok()) << "API docs endpoint should return HTTP 200: " << 
s.ToString();
+
+  string content = buf.ToString();
+  ASSERT_FALSE(content.empty()) << "API docs should not be empty";
+
+  ASSERT_STR_CONTAINS(content, "swagger-ui.css") << "Should include Swagger UI 
CSS";
+  ASSERT_STR_CONTAINS(content, "swagger-ui-bundle.js") << "Should include 
Swagger UI JS bundle";
+  ASSERT_STR_CONTAINS(content, "kudu-swagger-init.js") << "Should include Kudu 
Swagger init script";
+
+  ASSERT_STR_CONTAINS(content, "id=\"swagger-ui\"") << "Should have Swagger UI 
container div";
+
+  // Verify it has loading or ready state text
+  bool has_swagger_content =
+      content.find("swagger-ui") != string::npos || content.find("API") != 
string::npos;
+  ASSERT_TRUE(has_swagger_content) << "Should contain Swagger UI or 
API-related content";
+}
+
 }  // namespace master
 }  // namespace kudu
diff --git a/src/kudu/master/rest_catalog_path_handlers.cc 
b/src/kudu/master/rest_catalog_path_handlers.cc
index c0c81ce2d..98a7cd4a3 100644
--- a/src/kudu/master/rest_catalog_path_handlers.cc
+++ b/src/kudu/master/rest_catalog_path_handlers.cc
@@ -19,6 +19,7 @@
 
 #include <functional>
 #include <optional>
+#include <ostream>
 #include <string>
 #include <unordered_map>
 #include <utility>
@@ -37,12 +38,16 @@
 #include "kudu/master/master.h"
 #include "kudu/master/master.pb.h"
 #include "kudu/util/cow_object.h"
+#include "kudu/util/env.h"
+#include "kudu/util/faststring.h"
 #include "kudu/util/flag_tags.h"
 #include "kudu/util/jsonwriter.h"
 #include "kudu/util/monotime.h"
 #include "kudu/util/status.h"
 #include "kudu/util/web_callback_registry.h"
 
+DECLARE_string(webserver_doc_root);
+
 // We only use macros here to maintain cohesion with the existing 
RETURN_NOT_OK-style pattern.
 // They provide a consistent way to return JSON-formatted error responses.
 #define RETURN_JSON_ERROR(jw, error_msg, status_code, error_code) \
@@ -219,6 +224,40 @@ void RestCatalogPathHandlers::HandleLeaderEndpoint(const 
Webserver::WebRequest&
   RETURN_JSON_ERROR(jw, "No leader master found", resp->status_code, 
HttpStatusCode::NotFound);
 }
 
+// Kept as instance method for consistency with other handlers in this class.
+void RestCatalogPathHandlers::HandleApiDocsEndpoint(const 
Webserver::WebRequest& req, // NOLINT
+                                                    Webserver::WebResponse* 
resp) {
+  if (req.request_method != "GET") {
+    resp->status_code = HttpStatusCode::MethodNotAllowed;
+    return;
+  }
+
+  resp->status_code = HttpStatusCode::Ok;
+}
+
+// Kept as instance method for consistency with other handlers in this class.
+void RestCatalogPathHandlers::HandleApiSpecEndpoint(const 
Webserver::WebRequest& req, // NOLINT
+                                                    
Webserver::PrerenderedWebResponse* resp) {
+  if (req.request_method != "GET") {
+    resp->status_code = HttpStatusCode::MethodNotAllowed;
+    resp->output << "Method not allowed";
+    return;
+  }
+
+  const string spec_file_path = Substitute("$0/swagger/kudu-api.json", 
FLAGS_webserver_doc_root);
+  faststring spec_content;
+  Status s = ReadFileToString(Env::Default(), spec_file_path, &spec_content);
+
+  if (!s.ok()) {
+    resp->status_code = HttpStatusCode::NotFound;
+    resp->output << Substitute("Could not read API specification: $0", 
s.ToString());
+    return;
+  }
+
+  resp->status_code = HttpStatusCode::Ok;
+  resp->output << spec_content.ToString();
+}
+
 void RestCatalogPathHandlers::HandleGetTables(std::ostringstream* output,
                                               const Webserver::WebRequest& req,
                                               HttpStatusCode* status_code) {
@@ -449,6 +488,22 @@ void RestCatalogPathHandlers::Register(Webserver* server) {
       },
       StyleMode::JSON,
       false);
+  server->RegisterPathHandler(
+      "/api/docs",
+      "REST API Docs",
+      [this](const Webserver::WebRequest& req, Webserver::WebResponse* resp) {
+        this->HandleApiDocsEndpoint(req, resp);
+      },
+      StyleMode::STYLED,
+      true);
+  server->RegisterPrerenderedPathHandler(
+      "/api/v1/spec",
+      "",
+      [this](const Webserver::WebRequest& req, 
Webserver::PrerenderedWebResponse* resp) {
+        this->HandleApiSpecEndpoint(req, resp);
+      },
+      StyleMode::JSON,
+      false);
 }
 
 }  // namespace master
diff --git a/src/kudu/master/rest_catalog_path_handlers.h 
b/src/kudu/master/rest_catalog_path_handlers.h
index 89c51065a..982f8297f 100644
--- a/src/kudu/master/rest_catalog_path_handlers.h
+++ b/src/kudu/master/rest_catalog_path_handlers.h
@@ -47,6 +47,10 @@ class RestCatalogPathHandlers final {
                                Webserver::PrerenderedWebResponse* resp);
   void HandleLeaderEndpoint(const Webserver::WebRequest& req,
                             Webserver::PrerenderedWebResponse* resp);
+  void HandleApiDocsEndpoint(const Webserver::WebRequest& req,
+                             Webserver::WebResponse* resp);
+  void HandleApiSpecEndpoint(const Webserver::WebRequest& req,
+                             Webserver::PrerenderedWebResponse* resp);
 
   // Handles REST API endpoints based on the request method and path.
   void HandleGetTables(std::ostringstream* output,
diff --git a/www/api/docs.mustache b/www/api/docs.mustache
new file mode 100644
index 000000000..381ff12e3
--- /dev/null
+++ b/www/api/docs.mustache
@@ -0,0 +1,28 @@
+{{!
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+}}
+
+<link rel="stylesheet" type="text/css" 
href="{{base_url}}/swagger/swagger-ui.css" />
+<link rel="stylesheet" type="text/css" 
href="{{base_url}}/swagger/kudu-custom.css" />
+
+<div id="swagger-ui" data-base-url="{{base_url}}">
+  <p>Loading Swagger UI...</p>
+</div>
+
+<script src="{{base_url}}/swagger/swagger-ui-bundle.js" 
charset="UTF-8"></script>
+<script src="{{base_url}}/swagger/kudu-swagger-init.js" 
charset="UTF-8"></script>
diff --git a/www/swagger/kudu-api.json b/www/swagger/kudu-api.json
new file mode 100644
index 000000000..4a6910571
--- /dev/null
+++ b/www/swagger/kudu-api.json
@@ -0,0 +1,962 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "Kudu Master REST API",
+    "version": "1.0.0"
+  },
+  "servers": [
+    {
+      "url": "/api/v1",
+      "description": "Kudu Master REST API v1"
+    }
+  ],
+  "paths": {
+    "/tables": {
+      "get": {
+        "summary": "List all tables",
+        "description": "Retrieve a list of all tables in the Kudu cluster",
+        "operationId": "listTables",
+        "responses": {
+          "200": {
+            "description": "Successfully retrieved list of tables",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TablesResponse"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication required - SPNEGO authentication 
failed or missing (secured clusters only)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "503": {
+            "description": "Service unavailable - master not ready or not 
leader",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "summary": "Create a new table",
+        "description": "Create a new table in the Kudu cluster",
+        "operationId": "createTable",
+        "requestBody": {
+          "description": "Table creation request",
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/CreateTableRequest"
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "Table created successfully",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TableResponse"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Bad request - invalid table specification",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                },
+                "examples": {
+                  "invalid_json": {
+                    "summary": "Invalid JSON format",
+                    "value": {
+                      "error": "JSON table object is not correct: 
{\"name\":\"test_table\"}"
+                    }
+                  },
+                  "missing_schema": {
+                    "summary": "Missing required schema",
+                    "value": {
+                      "error": "JSON table object is not correct: 
{\"name\":\"test_table\",\"partition_schema\":{}}"
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication required - SPNEGO authentication 
failed or missing (secured clusters only)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "503": {
+            "description": "Service unavailable - master not ready or not 
leader",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/tables/{table_id}": {
+      "get": {
+        "summary": "Get table details",
+        "description": "Retrieve detailed information about a specific table",
+        "operationId": "getTable",
+        "parameters": [
+          {
+            "name": "table_id",
+            "in": "path",
+            "required": true,
+            "description": "The unique identifier of the table (32 
characters)",
+            "schema": {
+              "type": "string",
+              "pattern": "^[a-f0-9]{32}$",
+              "minLength": 32,
+              "maxLength": 32
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Successfully retrieved table details",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TableResponse"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Bad request - invalid table ID format",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication required - SPNEGO authentication 
failed or missing (secured clusters only)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Table not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "503": {
+            "description": "Service unavailable - master not ready or not 
leader",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "put": {
+        "summary": "Update table",
+        "description": "Modify an existing table (alter table operation)",
+        "operationId": "updateTable",
+        "parameters": [
+          {
+            "name": "table_id",
+            "in": "path",
+            "required": true,
+            "description": "The unique identifier of the table (32 
characters)",
+            "schema": {
+              "type": "string",
+              "pattern": "^[a-f0-9]{32}$",
+              "minLength": 32,
+              "maxLength": 32
+            }
+          }
+        ],
+        "requestBody": {
+          "description": "Table alteration request",
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/AlterTableRequest"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Table updated successfully",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TableResponse"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Bad request - invalid table ID or alteration 
request",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication required - SPNEGO authentication 
failed or missing (secured clusters only)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Table not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "503": {
+            "description": "Service unavailable - master not ready or not 
leader",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "delete": {
+        "summary": "Delete table",
+        "description": "Remove a table from the Kudu cluster",
+        "operationId": "deleteTable",
+        "parameters": [
+          {
+            "name": "table_id",
+            "in": "path",
+            "required": true,
+            "description": "The unique identifier of the table (32 
characters)",
+            "schema": {
+              "type": "string",
+              "pattern": "^[a-f0-9]{32}$",
+              "minLength": 32,
+              "maxLength": 32
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Table deleted successfully"
+          },
+          "400": {
+            "description": "Bad request - invalid table ID format",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication required - SPNEGO authentication 
failed or missing (secured clusters only)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Table not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "503": {
+            "description": "Service unavailable - master not ready or not 
leader",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/leader": {
+      "get": {
+        "summary": "Get leader master information",
+        "description": "Retrieve information about the current leader master",
+        "operationId": "getLeaderMaster",
+        "responses": {
+          "200": {
+            "description": "Successfully retrieved leader master information",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/LeaderResponse"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication required - SPNEGO authentication 
failed or missing (secured clusters only)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "No leader master found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "TablesResponse": {
+        "type": "object",
+        "properties": {
+          "tables": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/TableInfo"
+            }
+          }
+        },
+        "required": ["tables"]
+      },
+      "TableInfo": {
+        "type": "object",
+        "properties": {
+          "table_id": {
+            "type": "string",
+            "description": "Unique identifier of the table",
+            "pattern": "^[a-f0-9]{32}$"
+          },
+          "table_name": {
+            "type": "string",
+            "description": "Name of the table"
+          }
+        },
+        "required": ["table_id", "table_name"]
+      },
+      "TableResponse": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string",
+            "description": "Unique identifier of the table",
+            "pattern": "^[a-f0-9]{32}$"
+          },
+          "name": {
+            "type": "string",
+            "description": "Name of the table"
+          },
+          "schema": {
+            "$ref": "#/components/schemas/TableSchema"
+          },
+          "partition_schema": {
+            "$ref": "#/components/schemas/PartitionSchema"
+          },
+          "owner": {
+            "type": "string",
+            "description": "Owner of the table"
+          },
+          "comment": {
+            "type": "string",
+            "description": "Comment associated with the table"
+          },
+          "extra_config": {
+            "type": "object",
+            "description": "Additional configuration for the table"
+          }
+        },
+        "required": ["id", "name", "schema", "partition_schema", "owner"]
+      },
+      "TableSchema": {
+        "type": "object",
+        "properties": {
+          "columns": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/ColumnSchema"
+            }
+          }
+        },
+        "required": ["columns"]
+      },
+      "ColumnSchema": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "Column name",
+            "default": "new_column_name"
+          },
+          "type": {
+            "type": "string",
+            "description": "Column data type",
+            "enum": [
+              "INT8", "INT16", "INT32", "INT64", "UNIXTIME_MICROS",
+              "FLOAT", "DOUBLE", "DECIMAL", "STRING", "BOOL", "BINARY",
+              "VARCHAR", "DATE", "NESTED"
+            ]
+          },
+          "is_key": {
+            "type": "boolean",
+            "description": "Whether this column is part of the primary key"
+          },
+          "is_nullable": {
+            "type": "boolean",
+            "description": "Whether this column can contain NULL values",
+            "default": false
+          },
+          "is_auto_incrementing": {
+            "type": "boolean",
+            "description": "Whether this column is auto-incrementing",
+            "default": false
+          },
+          "nested_type": {
+            "$ref": "#/components/schemas/NestedDataType",
+            "description": "Descriptor of the nested data type. Required when 
type is NESTED."
+          }
+        },
+        "required": ["name", "type", "is_key", "is_nullable"]
+      },
+      "NestedDataType": {
+        "type": "object",
+        "properties": {
+          "array": {
+            "$ref": "#/components/schemas/ArrayTypeDescriptor"
+          }
+        },
+        "description": "Nested data type descriptor. Currently only arrays of 
scalar types are supported."
+      },
+      "ArrayTypeDescriptor": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "description": "Element type of the array",
+            "enum": [
+              "INT8", "INT16", "INT32", "INT64", "UNIXTIME_MICROS",
+              "FLOAT", "DOUBLE", "DECIMAL", "STRING", "BOOL", "BINARY",
+              "VARCHAR", "DATE"
+            ]
+          }
+        },
+        "required": ["type"]
+      },
+      "PartitionSchema": {
+        "type": "object",
+        "properties": {
+          "hash_schema": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/HashBucketSchema"
+            },
+            "description": "Table-wide hash schema. Hash schema for a 
particular range may be overridden by corresponding element in 
custom_hash_schema_ranges."
+          },
+          "range_schema": {
+            "$ref": "#/components/schemas/RangeSchema"
+          },
+          "custom_hash_schema_ranges": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/RangeWithHashSchema"
+            },
+            "description": "Ranges with custom hash schemas that override the 
table-wide hash schema for specific ranges"
+          }
+        }
+      },
+      "RangeWithHashSchema": {
+        "type": "object",
+        "properties": {
+          "range_bounds": {
+            "$ref": "#/components/schemas/RowOperations",
+            "description": "Range bounds (RANGE_LOWER_BOUND and 
RANGE_UPPER_BOUND operations)"
+          },
+          "hash_schema": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/HashBucketSchema"
+            },
+            "description": "Hash schema for this specific range"
+          }
+        }
+      },
+      "HashBucketSchema": {
+        "type": "object",
+        "properties": {
+          "columns": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/ColumnIdentifier"
+            },
+            "description": "Column identifiers of columns included in the 
hash. Every column must be a component of the primary key."
+          },
+          "num_buckets": {
+            "type": "integer",
+            "minimum": 2,
+            "description": "Number of buckets into which columns will be 
hashed. Must be at least 2."
+          },
+          "seed": {
+            "type": "integer",
+            "format": "int32",
+            "description": "Seed value for hash calculation. Optional."
+          }
+        },
+        "required": ["columns", "num_buckets"]
+      },
+      "RangeSchema": {
+        "type": "object",
+        "properties": {
+          "columns": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/ColumnIdentifier"
+            },
+            "description": "Column identifiers of columns included in the 
range. All columns must be a component of the primary key."
+          }
+        },
+        "required": ["columns"]
+      },
+      "ColumnIdentifier": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "Column name"
+          }
+        },
+        "description": "Column identifier by name. Used in partition schemas 
to specify which columns participate in partitioning."
+      },
+      "CreateTableRequest": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "Name of the table to create"
+          },
+          "schema": {
+            "$ref": "#/components/schemas/TableSchema"
+          },
+          "partition_schema": {
+            "$ref": "#/components/schemas/PartitionSchema"
+          },
+          "num_replicas": {
+            "type": "integer",
+            "enum": [1, 3, 5, 7],
+            "default": 3,
+            "description": "Number of replicas for the table. Must be an odd 
number. Typical values are 1, 3, 5, or 7, though actual limits may vary based 
on cluster configuration."
+          },
+          "owner": {
+            "type": "string",
+            "description": "Owner of the table"
+          },
+          "comment": {
+            "type": "string",
+            "description": "Comment for the table"
+          }
+        },
+        "required": ["name", "schema", "partition_schema"]
+      },
+      "AlterTableRequest": {
+        "type": "object",
+        "properties": {
+          "table": {
+            "$ref": "#/components/schemas/TableIdentifier"
+          },
+          "alter_schema_steps": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/AlterTableStep"
+            }
+          },
+          "new_table_name": {
+            "type": "string",
+            "description": "New name for the table",
+            "default": "new_table_name"
+          },
+          "new_table_owner": {
+            "type": "string",
+            "description": "New owner for the table",
+            "default": "new_table_owner"
+          },
+          "new_table_comment": {
+            "type": "string",
+            "description": "New comment for the table",
+            "default": "new_table_comment"
+          },
+          "num_replicas": {
+            "type": "integer",
+            "enum": [1, 3, 5, 7],
+            "default": 3,
+            "description": "New replication factor for the table. Must be an 
odd number. Typical values are 1, 3, 5, or 7, though actual limits may vary 
based on cluster configuration."
+          },
+          "schema": {
+            "$ref": "#/components/schemas/TableSchema",
+            "description": "Current table schema (required when 
adding/dropping range partitions)"
+          },
+          "new_extra_configs": {
+            "type": "object",
+            "additionalProperties": {
+              "type": "string"
+            },
+            "description": "New extra configuration properties for the table"
+          }
+        }
+      },
+      "TableIdentifier": {
+        "type": "object",
+        "properties": {
+          "table_name": {
+            "type": "string",
+            "description": "Name of the table"
+          }
+        },
+        "description": "Table identifier - specify either table_id OR 
table_name"
+      },
+      "AlterTableStep": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "enum": [
+              "ADD_COLUMN",
+              "DROP_COLUMN",
+              "RENAME_COLUMN",
+              "ALTER_COLUMN",
+              "ADD_RANGE_PARTITION",
+              "DROP_RANGE_PARTITION"
+            ]
+          },
+          "add_column": {
+            "$ref": "#/components/schemas/AddColumnSpec"
+          },
+          "drop_column": {
+            "$ref": "#/components/schemas/DropColumnSpec"
+          },
+          "rename_column": {
+            "$ref": "#/components/schemas/RenameColumnSpec"
+          },
+          "alter_column": {
+            "$ref": "#/components/schemas/AlterColumnSpec"
+          },
+          "add_range_partition": {
+            "$ref": "#/components/schemas/AddRangePartitionSpec"
+          },
+          "drop_range_partition": {
+            "$ref": "#/components/schemas/DropRangePartitionSpec"
+          }
+        },
+        "required": ["type"],
+        "description": "Alter table step - exactly one operation field must be 
set based on type"
+      },
+      "AddColumnSpec": {
+        "type": "object",
+        "properties": {
+          "schema": {
+            "$ref": "#/components/schemas/ColumnSchema"
+          }
+        },
+        "required": ["schema"]
+      },
+      "DropColumnSpec": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "Name of the column to drop"
+          }
+        },
+        "required": ["name"]
+      },
+      "RenameColumnSpec": {
+        "type": "object",
+        "properties": {
+          "old_name": {
+            "type": "string",
+            "description": "Current name of the column"
+          },
+          "new_name": {
+            "type": "string",
+            "description": "New name for the column"
+          }
+        },
+        "required": ["old_name", "new_name"]
+      },
+      "AlterColumnSpec": {
+        "type": "object",
+        "properties": {
+          "delta": {
+            "$ref": "#/components/schemas/ColumnSchemaDelta"
+          }
+        },
+        "required": ["delta"]
+      },
+      "ColumnSchemaDelta": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "Column name to alter"
+          },
+          "new_name": {
+            "type": "string",
+            "description": "New column name"
+          },
+          "default_value": {
+            "type": "string",
+            "format": "byte",
+            "description": "New default value (encoded)"
+          },
+          "remove_default": {
+            "type": "boolean",
+            "description": "Whether to remove the default value"
+          },
+          "new_comment": {
+            "type": "string",
+            "description": "New comment for the column"
+          }
+        }
+      },
+      "AddRangePartitionSpec": {
+        "type": "object",
+        "properties": {
+          "range_bounds": {
+            "$ref": "#/components/schemas/RowOperations",
+            "description": "Range bounds for the partition (RANGE_LOWER_BOUND 
and RANGE_UPPER_BOUND operations)"
+          },
+          "dimension_label": {
+            "type": "string",
+            "description": "Dimension label for tablet placement"
+          },
+          "custom_hash_schema": {
+            "$ref": "#/components/schemas/CustomHashSchema"
+          }
+        }
+      },
+      "DropRangePartitionSpec": {
+        "type": "object",
+        "properties": {
+          "range_bounds": {
+            "$ref": "#/components/schemas/RowOperations",
+            "description": "Range bounds for the partition to drop"
+          }
+        }
+      },
+      "CustomHashSchema": {
+        "type": "object",
+        "properties": {
+          "hash_schema": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/HashBucketSchema"
+            }
+          }
+        }
+      },
+      "RowOperations": {
+        "type": "object",
+        "description": "Row operations for range bounds - complex format, see 
protobuf documentation"
+      },
+      "LeaderResponse": {
+        "type": "object",
+        "properties": {
+          "leader": {
+            "type": "string",
+            "description": "URL of the leader master",
+            "format": "uri"
+          }
+        },
+        "required": ["leader"]
+      },
+      "ErrorResponse": {
+        "type": "object",
+        "properties": {
+          "error": {
+            "type": "string",
+            "description": "Error message describing what went wrong"
+          }
+        },
+        "required": ["error"],
+        "examples": {
+          "table_not_found": {
+            "summary": "Table not found",
+            "value": {
+              "error": "Table not found"
+            }
+          },
+          "invalid_table_id": {
+            "summary": "Invalid table ID",
+            "value": {
+              "error": "Invalid table ID: must be exactly 32 characters long."
+            }
+          },
+          "invalid_json": {
+            "summary": "Invalid JSON format",
+            "value": {
+              "error": "JSON table object is not correct: 
{\"name\":\"test_table\"}"
+            }
+          },
+          "method_not_allowed": {
+            "summary": "HTTP method not allowed",
+            "value": {
+              "error": "Method not allowed"
+            }
+          },
+          "master_not_ready": {
+            "summary": "Master not ready",
+            "value": {
+              "error": "Master is not ready: CatalogManager is not running"
+            }
+          },
+          "no_leader": {
+            "summary": "No leader master found",
+            "value": {
+              "error": "No leader master found"
+            }
+          }
+        }
+      }
+    },
+    "securitySchemes": {
+      "spnego": {
+        "type": "http",
+        "scheme": "bearer",
+        "description": "**SPNEGO/Kerberos Authentication (Not interactive):** 
When accessing a secured Kudu cluster, SPNEGO authentication using Kerberos is 
required. Clients must obtain a valid Kerberos ticket and present it via HTTP 
Negotiate (RFC 4559) authentication. This authentication scheme cannot be 
configured through Swagger UI - clients must handle SPNEGO authentication in 
their HTTP client (e.g., curl with --negotiate flag)."
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/www/swagger/kudu-custom.css b/www/swagger/kudu-custom.css
new file mode 100644
index 000000000..88f7b8311
--- /dev/null
+++ b/www/swagger/kudu-custom.css
@@ -0,0 +1,21 @@
+/* Hide any navbar/header elements */
+.swagger-ui .topbar,
+.swagger-ui .swagger-ui-wrap > .topbar,
+.swagger-ui .header,
+.swagger-ui .info .title,
+.swagger-ui .info hgroup.main {
+  display: none !important;
+}
+
+.swagger-ui {
+  max-width: none !important;
+}
+
+.swagger-ui .wrapper {
+  padding-top: 0 !important;
+}
+
+.swagger-ui .scheme-container {
+  margin-top: 0 !important;
+  padding-top: 0 !important;
+}
diff --git a/www/swagger/kudu-swagger-init.js b/www/swagger/kudu-swagger-init.js
new file mode 100644
index 000000000..3d1689618
--- /dev/null
+++ b/www/swagger/kudu-swagger-init.js
@@ -0,0 +1,70 @@
+function initializeSwaggerUI() {
+  console.log('Initializing Swagger UI...');
+  console.log('SwaggerUIBundle available:', typeof SwaggerUIBundle !== 
'undefined');
+
+  if (typeof SwaggerUIBundle === 'undefined') {
+    document.getElementById('swagger-ui').innerHTML = '<div style="color: red; 
padding: 20px; border: 1px solid red;">ERROR: SwaggerUIBundle is not loaded! 
Check browser console for details.</div>';
+    return;
+  }
+
+  try {
+    const swaggerContainer = document.getElementById('swagger-ui');
+    const baseUrl = swaggerContainer ? 
swaggerContainer.getAttribute('data-base-url') || '' : '';
+    console.log('Base URL:', baseUrl);
+
+      let specUrl;
+
+      if (baseUrl && baseUrl.includes('?')) {
+        const [basePath, queryParams] = baseUrl.split('?', 2);
+        // Remove trailing slashes to prevent double slashes
+        const cleanBasePath = basePath.replace(/\/+$/, '');
+        specUrl = `${cleanBasePath}/api/v1/spec?${queryParams}`;
+      }
+      else {
+        const cleanBasePath = baseUrl.replace(/\/+$/, '');
+        specUrl = `${cleanBasePath}/api/v1/spec`;
+      }
+      console.log('API spec URL:', specUrl);
+
+      const ui = SwaggerUIBundle({
+        url: specUrl,
+      dom_id: '#swagger-ui',
+      deepLinking: true,
+      presets: [
+        SwaggerUIBundle.presets.apis
+      ],
+      plugins: [
+        SwaggerUIBundle.plugins.DownloadUrl
+      ],
+      layout: "BaseLayout",
+        onComplete: function() {
+          const hideAuthElements = function() {
+            const modalAuthBtn = document.querySelector('.auth-btn-wrapper 
.btn.authorize');
+            if (modalAuthBtn) {
+              modalAuthBtn.style.display = 'none';
+            }
+
+            const authInputWrapper = document.querySelector('.auth-container 
input');
+            if (authInputWrapper) {
+              const wrapper = authInputWrapper.closest('.wrapper');
+              if (wrapper) {
+                wrapper.style.display = 'none';
+              }
+            }
+          };
+ 
+          hideAuthElements();
+          const observer = new MutationObserver(hideAuthElements);
+          observer.observe(document.body, { childList: true, subtree: true });
+        }
+    });
+
+    console.log('Swagger UI initialized successfully');
+  } catch (error) {
+    console.error('Error initializing Swagger UI:', error);
+    document.getElementById('swagger-ui').innerHTML = '<div style="color: red; 
padding: 20px; border: 1px solid red;">ERROR: ' + error.message + '</div>';
+  }
+}
+
+// Initialize when DOM is ready
+window.addEventListener('load', initializeSwaggerUI);
\ No newline at end of file
diff --git a/www/swagger/swagger-ui-bundle.js b/www/swagger/swagger-ui-bundle.js
new file mode 100644
index 000000000..419ffe6fa
--- /dev/null
+++ b/www/swagger/swagger-ui-bundle.js
@@ -0,0 +1,2 @@
+/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
+!function webpackUniversalModuleDefinition(s,o){"object"==typeof 
exports&&"object"==typeof module?module.exports=o():"function"==typeof 
define&&define.amd?define([],o):"object"==typeof 
exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var 
s={251:(s,o)=>{o.read=function(s,o,i,a,u){var 
_,w,x=8*u-a-1,C=(1<<x)-1,j=C>>1,L=-7,B=i?u-1:0,$=i?-1:1,V=s[o+B];for(B+=$,_=V&(1<<-L)-1,V>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=
 [...]
":95===s?" ":76===s?"\u2028":80===s?"\u2029":""}function 
charFromCodepoint(s){return 
s<=65535?String.fromCharCode(s):String.fromCharCode(55296+(s-65536>>10),56320+(s-65536&1023))}for(var
 $r=new Array(256),Vr=new 
Array(256),Ur=0;Ur<256;Ur++)$r[Ur]=simpleEscapeSequence(Ur)?1:0,Vr[Ur]=simpleEscapeSequence(Ur);function
 
State$1(s,o){this.input=s,this.filename=o.filename||null,this.schema=o.schema||Mr,this.onWarning=o.onWarning||null,this.legacy=o.legacy||!1,this.json=o.json||!1,this.listener=o
 [...]
\ No newline at end of file
diff --git a/www/swagger/swagger-ui.css b/www/swagger/swagger-ui.css
new file mode 100644
index 000000000..384ce28ae
--- /dev/null
+++ b/www/swagger/swagger-ui.css
@@ -0,0 +1 @@
+.swagger-ui{color:#3b4151;font-family:sans-serif}.swagger-ui 
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.swagger-ui
 body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui 
footer,.swagger-ui header,.swagger-ui nav,.swagger-ui 
section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui 
figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui 
figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;ov [...]

Reply via email to