You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2022/03/14 08:10:10 UTC
[zeppelin] branch master updated: [ZEPPELIN-5663] Provide REST API about notebook version control (#4300)
This is an automated email from the ASF dual-hosted git repository.
zjffdu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 60ddde6 [ZEPPELIN-5663] Provide REST API about notebook version control (#4300)
60ddde6 is described below
commit 60ddde694681f2071e60b21dfd600c4831b0aad4
Author: nihua <gu...@foxmail.com>
AuthorDate: Mon Mar 14 16:09:56 2022 +0800
[ZEPPELIN-5663] Provide REST API about notebook version control (#4300)
* [ZEPPELIN-5663] Provide REST API about notebook version control
* change RESTFUL API method checkpointNote(...) to return revisionId like that addParagraph return paragraphId
---
docs/usage/rest_api/notebook.md | 238 +++++++++++++++++++++
.../org/apache/zeppelin/rest/NotebookRestApi.java | 88 +++++++-
.../rest/message/CheckpointNoteRequest.java | 28 +++
.../apache/zeppelin/service/NotebookService.java | 10 +-
.../apache/zeppelin/rest/NotebookRestApiTest.java | 200 +++++++++++++++++
5 files changed, 559 insertions(+), 5 deletions(-)
diff --git a/docs/usage/rest_api/notebook.md b/docs/usage/rest_api/notebook.md
index b0dcd46..c49d805 100644
--- a/docs/usage/rest_api/notebook.md
+++ b/docs/usage/rest_api/notebook.md
@@ -1574,3 +1574,241 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete,
</table>
+## Version control
+
+
+
+### Get revisions of a note
+
+ <table class="table-configuration">
+ <col width="200">
+ <tr>
+ <td>Description</td>
+ <td>This ```GET``` method gets the revisions of a note.
+ </td>
+ </tr>
+ <tr>
+ <td>URL</td>
+ <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId]/revision```</td>
+ </tr>
+ <tr>
+ <td>Success code</td>
+ <td>200</td>
+ </tr>
+ <tr>
+ <td>Fail code</td>
+ <td>500</td>
+ </tr>
+ <tr>
+ <td> sample JSON response </td>
+ <td>
+
+```json
+{
+ "status": "OK",
+ "body": [
+ {
+ "id": "f97ce5c7f076783023d33623ad52ca994277e5c1",
+ "message": "first commit",
+ "time": 1645712061
+ },
+ {
+ "id": "e9b964bebdecec6a59efe085f97db4040ae333aa",
+ "message": "second commit",
+ "time": 1645693163
+ }
+ ]
+}
+```
+</td>
+ </tr>
+ </table>
+
+<br/>
+### Save a revision for a note
+ <table class="table-configuration">
+ <col width="200">
+ <tr>
+ <td>Description</td>
+ <td>This ```POST``` method saves a revision for a note.
+ </td>
+ </tr>
+ <tr>
+ <td>URL</td>
+ <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId]/revision```</td>
+ </tr>
+ <tr>
+ <td>Success code</td>
+ <td>200</td>
+ </tr>
+ <tr>
+ <td>Bad Request code</td>
+ <td>400</td>
+ </tr>
+ <tr>
+ <td>Fail code</td>
+ <td>500</td>
+ </tr>
+ <tr>
+ <td> sample JSON input </td>
+ <td>
+
+```json
+{
+ "commitMessage": "first commit"
+}
+```
+</td>
+ </tr>
+ <tr>
+ <td> sample JSON response </td>
+ <td>
+
+```json
+{
+ "status": "OK",
+ "message": "",
+ "body": "6a5879218dfb797b013bcd24a594808045e34875"
+}
+```
+</td>
+ </tr>
+ </table>
+### Get a revision of a note
+ <table class="table-configuration">
+ <col width="200">
+ <tr>
+ <td>Description</td>
+ <td>This ```GET``` method gets a revision of a note.
+ </td>
+ </tr>
+ <tr>
+ <td>URL</td>
+ <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId]/revision/{revisionId}```</td>
+ </tr>
+ <tr>
+ <td>Success code</td>
+ <td>200</td>
+ </tr>
+ <tr>
+ <td>Fail code</td>
+ <td>500</td>
+ </tr>
+ <tr>
+ <td> sample JSON response </td>
+ <td>
+
+```json
+{
+ "status": "OK",
+ "message": "",
+ "body": {
+ "paragraphs": [
+ {
+ "text": "%sql \nselect age, count(1) value\nfrom bank \nwhere age < 30 \ngroup by
+ age \norder by age",
+ "config": {
+ "colWidth": 4,
+ "graph": {
+ "mode": "multiBarChart",
+ "height": 300,
+ "optionOpen": false,
+ "keys": [
+ {
+ "name": "age",
+ "index": 0,
+ "aggr": "sum"
+ }
+ ],
+ "values": [
+ {
+ "name": "value",
+ "index": 1,
+ "aggr": "sum"
+ }
+ ],
+ "groups": [],
+ "scatter": {
+ "xAxis": {
+ "name": "age",
+ "index": 0,
+ "aggr": "sum"
+ },
+ "yAxis": {
+ "name": "value",
+ "index": 1,
+ "aggr": "sum"
+ }
+ }
+ }
+ },
+ "settings": {
+ "params": {},
+ "forms": {}
+ },
+ "jobName": "paragraph\_1423500782552\_-1439281894",
+ "id": "20150210-015302\_1492795503",
+ "results": {
+ "code": "SUCCESS",
+ "msg": [
+ {
+ "type": "TABLE",
+ "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26
+\t77\n27\t94\n28\t103\n29\t97\n"
+ }
+ ]
+ },
+ "dateCreated": "Feb 10, 2015 1:53:02 AM",
+ "dateStarted": "Jul 3, 2015 1:43:17 PM",
+ "dateFinished": "Jul 3, 2015 1:43:23 PM",
+ "status": "FINISHED",
+ "progressUpdateIntervalMs": 500
+ }
+ ],
+ "name": "Zeppelin Tutorial",
+ "id": "2A94M5J1Z",
+ "angularObjects": {},
+ "config": {
+ "looknfeel": "default"
+ },
+ "info": {}
+ }
+}
+```
+</td>
+ </tr>
+ </table>
+### Revert a note to a specified version
+ <table class="table-configuration">
+ <col width="200">
+ <tr>
+ <td>Description</td>
+ <td>This ```PUT``` method reverts a note to a specified version
+ </td>
+ </tr>
+ <tr>
+ <td>URL</td>
+ <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId]/revision/{revisionId}```</td>
+ </tr>
+ <tr>
+ <td>Success code</td>
+ <td>200</td>
+ </tr>
+ <tr>
+ <td>Fail code</td>
+ <td>500</td>
+ </tr>
+ <tr>
+ <td> sample JSON response </td>
+ <td>
+
+```json
+{
+ "status": "OK"
+}
+```
+</td>
+ </tr>
+ </table>
+
+
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
index d68a68a..40ad46a 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
@@ -48,6 +48,7 @@ import org.apache.zeppelin.notebook.NoteInfo;
import org.apache.zeppelin.notebook.Notebook;
import org.apache.zeppelin.notebook.Paragraph;
import org.apache.zeppelin.notebook.AuthorizationService;
+import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl;
import org.apache.zeppelin.notebook.scheduler.SchedulerService;
import org.apache.zeppelin.rest.exception.BadRequestException;
import org.apache.zeppelin.rest.exception.ForbiddenException;
@@ -334,9 +335,94 @@ public class NotebookRestApi extends AbstractRestApi {
/**
+ * Get revision history of a note.
+ *
+ * @param noteId
+ * @return
+ * @throws IOException
+ */
+ @GET
+ @Path("{noteId}/revision")
+ @ZeppelinApi
+ public Response getNoteRevisionHistory(@PathParam("noteId") String noteId) throws IOException {
+ LOGGER.info("Get revision history of note {}", noteId);
+ List<NotebookRepoWithVersionControl.Revision> revisions = notebookService.listRevisionHistory(noteId, getServiceContext(), new RestServiceCallback<>());
+ return new JsonResponse<>(Status.OK, revisions).build();
+ }
+
+
+ /**
+ * Save a revision for the a note
+ *
+ * @param message
+ * @param noteId
+ * @return
+ * @throws IOException
+ */
+ @POST
+ @Path("{noteId}/revision")
+ @ZeppelinApi
+ public Response checkpointNote(String message,
+ @PathParam("noteId") String noteId) throws IOException {
+ LOGGER.info("Commit note by JSON {}", message);
+ CheckpointNoteRequest request = GSON.fromJson(message, CheckpointNoteRequest.class);
+ if (request == null || StringUtils.isEmpty(request.getCommitMessage())) {
+ LOGGER.warn("Trying to commit notebook {} with empty commitMessage", noteId);
+ throw new BadRequestException("commitMessage can not be empty");
+ }
+ NotebookRepoWithVersionControl.Revision revision = notebookService.checkpointNote(noteId, request.getCommitMessage(), getServiceContext(), new RestServiceCallback<>());
+ if (revision == null || StringUtils.isEmpty(revision.id)) {
+ return new JsonResponse<>(Status.OK, "Couldn't checkpoint note revision: possibly no changes found or storage doesn't support versioning. "
+ + "Please check the logs for more details.").build();
+ }
+ return new JsonResponse<>(Status.OK, "", revision.id).build();
+ }
+
+
+ /**
+ * Get a specified revision of a note.
+ *
+ * @param noteId
+ * @param revisionId
+ * @param reload
+ * @return
+ * @throws IOException
+ */
+ @GET
+ @Path("{noteId}/revision/{revisionId}")
+ @ZeppelinApi
+ public Response getNoteByRevison(@PathParam("noteId") String noteId,
+ @PathParam("revisionId") String revisionId,
+ @QueryParam("reload") boolean reload) throws IOException {
+ LOGGER.info("Get note {} by the revision {}", noteId, revisionId);
+ Note noteRevision = notebookService.getNotebyRevision(noteId, revisionId, getServiceContext(), new RestServiceCallback<>());
+ return new JsonResponse<>(Status.OK, "", noteRevision).build();
+ }
+
+
+ /**
+ * Revert a note to the specified version
+ *
+ * @param noteId
+ * @param revisionId
+ * @return
+ * @throws IOException
+ */
+ @PUT
+ @Path("{noteId}/revision/{revisionId}")
+ @ZeppelinApi
+ public Response setNoteRevision(@PathParam("noteId") String noteId,
+ @PathParam("revisionId") String revisionId) throws IOException {
+ LOGGER.info("Revert note {} to the revision {}", noteId, revisionId);
+ notebookService.setNoteRevision(noteId, revisionId, getServiceContext(), new RestServiceCallback<>());
+ return new JsonResponse<>(Status.OK).build();
+ }
+
+
+ /**
* Get note of this specified notePath.
*
- * @param message - JSON containing notePath
+ * @param message - JSON containing notePath
* @param reload
* @return
* @throws IOException
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CheckpointNoteRequest.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CheckpointNoteRequest.java
new file mode 100644
index 0000000..e47d07a
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CheckpointNoteRequest.java
@@ -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.
+ */
+package org.apache.zeppelin.rest.message;
+
+public class CheckpointNoteRequest {
+ String commitMessage;
+
+ public CheckpointNoteRequest() {
+ }
+
+ public String getCommitMessage() {
+ return commitMessage;
+ }
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java b/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
index fff9a68..02a4bbd 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
@@ -371,7 +371,7 @@ public class NotebookService {
/**
* Executes given paragraph with passed paragraph info like noteId, paragraphId, title, text and etc.
*
- * @param noteId
+ * @param note
* @param paragraphId
* @param title
* @param text
@@ -1012,12 +1012,14 @@ public class NotebookService {
}
- public void getNotebyRevision(String noteId,
+ // notebook.getNoteByRevision(...) does not use the NoteCache,
+ // so we can return a Note object here.
+ public Note getNotebyRevision(String noteId,
String revisionId,
ServiceContext context,
ServiceCallback<Note> callback) throws IOException {
- notebook.processNote(noteId ,
+ return notebook.processNote(noteId,
note -> {
if (note == null) {
callback.onFailure(new NoteNotFoundException(noteId), context);
@@ -1031,7 +1033,7 @@ public class NotebookService {
Note revisionNote = notebook.getNoteByRevision(noteId, note.getPath(), revisionId,
context.getAutheInfo());
callback.onSuccess(revisionNote, context);
- return null;
+ return revisionNote;
});
}
diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java
index dc95df5..62398c0 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java
@@ -30,6 +30,7 @@ import com.google.gson.reflect.TypeToken;
import org.apache.zeppelin.interpreter.InterpreterSetting;
import org.apache.zeppelin.interpreter.InterpreterSettingManager;
import org.apache.zeppelin.notebook.Notebook;
+import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl;
import org.apache.zeppelin.rest.message.ParametersRequest;
import org.apache.zeppelin.socket.NotebookServer;
import org.apache.zeppelin.utils.TestUtils;
@@ -165,6 +166,112 @@ public class NotebookRestApiTest extends AbstractTestRestApi {
}
@Test
+ public void testGetNoteRevisionHistory() throws IOException {
+ LOG.info("Running testGetNoteRevisionHistory");
+ String note1Id = null;
+ Notebook notebook = TestUtils.getInstance(Notebook.class);
+ try {
+ String notePath = "note1";
+ note1Id = notebook.createNote(notePath, anonymous);
+
+ //Add a paragraph and commit
+ NotebookRepoWithVersionControl.Revision first_commit =
+ notebook.processNote(note1Id, note -> {
+ Paragraph p1 = note.addNewParagraph(anonymous);
+ p1.setText("text1");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return notebook.checkpointNote(note.getId(), note.getPath(), "first commit", anonymous);
+ });
+
+ //Add a paragraph again
+ notebook.processNote(note1Id, note -> {
+ Paragraph p2 = note.addNewParagraph(anonymous);
+ p2.setText("text2");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return null;
+ });
+
+ // Verify
+ CloseableHttpResponse get1 = httpGet("/notebook/" + note1Id + "/revision");
+
+ assertThat(get1, isAllowed());
+ Map<String, Object> resp = gson.fromJson(EntityUtils.toString(get1.getEntity(), StandardCharsets.UTF_8),
+ new TypeToken<Map<String, Object>>() {
+ }.getType());
+ List<Map<String, Object>> body = (List<Map<String, Object>>) resp.get("body");
+ assertEquals(1, body.size());
+ assertEquals(first_commit.id, body.get(0).get("id"));
+ get1.close();
+
+ // Second commit
+ NotebookRepoWithVersionControl.Revision second_commit = notebook.processNote(note1Id, note -> notebook.checkpointNote(note.getId(), note.getPath(), "Second commit", anonymous));
+
+ // Verify
+ CloseableHttpResponse get2 = httpGet("/notebook/" + note1Id + "/revision");
+
+ assertThat(get2, isAllowed());
+ resp = gson.fromJson(EntityUtils.toString(get2.getEntity(), StandardCharsets.UTF_8),
+ new TypeToken<Map<String, Object>>() {
+ }.getType());
+ body = (List<Map<String, Object>>) resp.get("body");
+ assertEquals(2, body.size());
+ assertEquals(second_commit.id, body.get(0).get("id"));
+ get2.close();
+
+ } finally {
+ // cleanup
+ if (null != note1Id) {
+ notebook.removeNote(note1Id, anonymous);
+ }
+ }
+ }
+
+ @Test
+ public void testGetNoteByRevision() throws IOException {
+ LOG.info("Running testGetNoteByRevision");
+ String note1Id = null;
+ Notebook notebook = TestUtils.getInstance(Notebook.class);
+ try {
+ String notePath = "note1";
+ note1Id = notebook.createNote(notePath, anonymous);
+
+ //Add a paragraph and commit
+ NotebookRepoWithVersionControl.Revision first_commit =
+ notebook.processNote(note1Id, note -> {
+ Paragraph p1 = note.addNewParagraph(anonymous);
+ p1.setText("text1");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return notebook.checkpointNote(note.getId(), note.getPath(), "first commit", anonymous);
+ });
+
+ //Add a paragraph again
+ notebook.processNote(note1Id, note -> {
+ Paragraph p2 = note.addNewParagraph(anonymous);
+ p2.setText("text2");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return null;
+ });
+
+ // Verify
+ CloseableHttpResponse get = httpGet("/notebook/" + note1Id + "/revision/" + first_commit.id);
+
+ assertThat(get, isAllowed());
+ Map<String, Object> resp = gson.fromJson(EntityUtils.toString(get.getEntity(), StandardCharsets.UTF_8),
+ new TypeToken<Map<String, Object>>() {
+ }.getType());
+ Map<String, Object> noteObject = (Map<String, Object>) resp.get("body");
+ assertEquals(1, ((List) noteObject.get("paragraphs")).size());
+ assertEquals("text1", ((List<Map<String, String>>) noteObject.get("paragraphs")).get(0).get("text"));
+ get.close();
+ } finally {
+ // cleanup
+ if (null != note1Id) {
+ notebook.removeNote(note1Id, anonymous);
+ }
+ }
+ }
+
+ @Test
public void testGetNoteParagraphJobStatus() throws IOException {
LOG.info("Running testGetNoteParagraphJobStatus");
String note1Id = null;
@@ -194,6 +301,99 @@ public class NotebookRestApiTest extends AbstractTestRestApi {
}
@Test
+ public void testCheckpointNote() throws IOException {
+ LOG.info("Running testCheckpointNote");
+ String note1Id = null;
+ Notebook notebook = TestUtils.getInstance(Notebook.class);
+ try {
+ String notePath = "note1";
+ note1Id = notebook.createNote(notePath, anonymous);
+
+ //Add a paragraph
+ notebook.processNote(note1Id, note -> {
+ Paragraph p1 = note.addNewParagraph(anonymous);
+ p1.setText("text1");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return null;
+ });
+
+ // Call restful api to save a revision and verify
+ String commitMessage = "first commit";
+ CloseableHttpResponse post = httpPost("/notebook/" + note1Id + "/revision", "{\"commitMessage\" : \"" + commitMessage + "\"}");
+
+ assertThat(post, isAllowed());
+ Map<String, Object> resp = gson.fromJson(EntityUtils.toString(post.getEntity(), StandardCharsets.UTF_8),
+ new TypeToken<Map<String, Object>>() {
+ }.getType());
+ assertEquals("OK", resp.get("status"));
+ String revisionId = (String) resp.get("body");
+ notebook.processNote(note1Id, note -> {
+ Note revisionOfNote = notebook.getNoteByRevision(note.getId(), note.getPath(), revisionId, anonymous);
+ assertEquals(1, notebook.listRevisionHistory(note.getId(), note.getPath(), anonymous).size());
+ assertEquals(1, revisionOfNote.getParagraphs().size());
+ assertEquals("text1", revisionOfNote.getParagraph(0).getText());
+ return null;
+ });
+ post.close();
+ } finally {
+ // cleanup
+ if (null != note1Id) {
+ notebook.removeNote(note1Id, anonymous);
+ }
+ }
+ }
+
+
+ @Test
+ public void testSetNoteRevision() throws IOException {
+ LOG.info("Running testSetNoteRevision");
+ String note1Id = null;
+ Notebook notebook = TestUtils.getInstance(Notebook.class);
+ try {
+ String notePath = "note1";
+ note1Id = notebook.createNote(notePath, anonymous);
+
+ //Add a paragraph and commit
+ NotebookRepoWithVersionControl.Revision first_commit =
+ notebook.processNote(note1Id, note -> {
+ Paragraph p1 = note.addNewParagraph(anonymous);
+ p1.setText("text1");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return notebook.checkpointNote(note.getId(), note.getPath(), "first commit", anonymous);
+ });
+
+ //Add a paragraph again
+ notebook.processNote(note1Id, note -> {
+ Paragraph p2 = note.addNewParagraph(anonymous);
+ p2.setText("text2");
+ notebook.saveNote(note, AuthenticationInfo.ANONYMOUS);
+ return null;
+ });
+
+ // Call restful api to revert note to first revision and verify
+ CloseableHttpResponse put = httpPut("/notebook/" + note1Id + "/revision/" + first_commit.id, "");
+
+ assertThat(put, isAllowed());
+ Map<String, Object> resp = gson.fromJson(EntityUtils.toString(put.getEntity(), StandardCharsets.UTF_8),
+ new TypeToken<Map<String, Object>>() {
+ }.getType());
+ assertEquals("OK", resp.get("status"));
+ notebook.processNote(note1Id, note -> {
+ assertEquals(1, note.getParagraphs().size());
+ assertEquals("text1", note.getParagraph(0).getText());
+ return null;
+ });
+ put.close();
+ } finally {
+ // cleanup
+ if (null != note1Id) {
+ notebook.removeNote(note1Id, anonymous);
+ }
+ }
+ }
+
+
+ @Test
public void testRunParagraphJob() throws Exception {
LOG.info("Running testRunParagraphJob");
String note1Id = null;