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

ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 1b6052a  CASSANDRASC-95 Add sidecar client changes for restore from S3
1b6052a is described below

commit 1b6052a8c3b04fca48f82e887c12bcfb2bae0bdb
Author: Yifan Cai <[email protected]>
AuthorDate: Fri Jan 19 10:52:50 2024 -0800

    CASSANDRASC-95 Add sidecar client changes for restore from S3
    
    Patch by Saranya Krishnakumar, Yifan Cai; Reviewed by Francisco Guerrero 
for CASSANDRASC-95
    
    Co-authored-by: Yifan Cai <[email protected]>
    Co-authored-by: Saranya Krishnakumar <[email protected]>
---
 CHANGES.txt                                        |   1 +
 client/build.gradle                                |   1 +
 .../cassandra/sidecar/client/SidecarClient.java    |  84 +++++++++-
 .../client/SidecarClientBlobRestoreExtension.java  |  96 +++++++++++
 .../client/request/AbortRestoreJobRequest.java     |  56 +++++++
 .../client/request/CreateRestoreJobRequest.java    |  65 ++++++++
 .../request/CreateRestoreJobSliceRequest.java      |  77 +++++++++
 .../sidecar/client/request/JsonPayloadRequest.java |  30 ++++
 .../client/request/RestoreJobSummaryRequest.java   |  57 +++++++
 .../client/request/UpdateRestoreJobRequest.java    |  68 ++++++++
 .../client/retry/CreateRestoreJobRetryPolicy.java  |  62 +++++++
 .../sidecar/client/SidecarClientTest.java          |  53 ++++++
 common/build.gradle                                |   3 +-
 .../cassandra/sidecar/common/ApiEndpointsV1.java   |   2 +
 .../data/CreateRestoreJobRequestPayloadTest.java   | 182 +++++++++++++++++++++
 .../common/data/CreateSliceRequestPayloadTest.java | 109 ++++++++++++
 .../data/UpdateRestoreJobRequestPayloadTest.java   |  76 +++++++++
 17 files changed, 1020 insertions(+), 2 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 2cca9d6..6b5a9b9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 1.0.0
 -----
+ * Add sidecar client changes for restore from S3 (CASSANDRASC-95)
  * Add restore SSTables from S3 into Cassandra feature to Cassandra Sidecar 
(CASSANDRASC-92)
  * Define routing order for http routes (CASSANDRASC-93)
  * AbstractHandler is handling the request even when it fails to extract 
params (CASSANDRASC-91)
diff --git a/client/build.gradle b/client/build.gradle
index d7564cd..202a3a1 100644
--- a/client/build.gradle
+++ b/client/build.gradle
@@ -80,6 +80,7 @@ dependencies {
     testImplementation('com.squareup.okhttp3:mockwebserver:4.10.0')
     testImplementation(group: 'io.netty', name: 'netty-codec-http', version: 
'4.1.69.Final')
 
+    testFixturesApi(testFixtures(project(":common")))
     testFixturesImplementation(platform('org.junit:junit-bom:5.9.2'))
     testFixturesImplementation('org.junit.jupiter:junit-jupiter')
     testFixturesImplementation("org.assertj:assertj-core:3.24.2")
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
index 7e5dbbe..3048808 100644
--- 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
@@ -20,13 +20,21 @@
 package org.apache.cassandra.sidecar.client;
 
 import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import io.netty.handler.codec.http.HttpResponseStatus;
+import org.apache.cassandra.sidecar.client.request.AbortRestoreJobRequest;
+import org.apache.cassandra.sidecar.client.request.CreateRestoreJobRequest;
+import 
org.apache.cassandra.sidecar.client.request.CreateRestoreJobSliceRequest;
 import org.apache.cassandra.sidecar.client.request.ImportSSTableRequest;
+import org.apache.cassandra.sidecar.client.request.RestoreJobSummaryRequest;
+import org.apache.cassandra.sidecar.client.request.UpdateRestoreJobRequest;
+import org.apache.cassandra.sidecar.client.retry.CreateRestoreJobRetryPolicy;
 import org.apache.cassandra.sidecar.client.retry.IgnoreConflictRetryPolicy;
 import org.apache.cassandra.sidecar.client.retry.OncePerInstanceRetryPolicy;
 import org.apache.cassandra.sidecar.client.retry.RetryPolicy;
@@ -34,21 +42,26 @@ import 
org.apache.cassandra.sidecar.client.retry.RunnableOnStatusCodeRetryPolicy
 import org.apache.cassandra.sidecar.client.selection.InstanceSelectionPolicy;
 import 
org.apache.cassandra.sidecar.client.selection.RandomInstanceSelectionPolicy;
 import org.apache.cassandra.sidecar.common.NodeSettings;
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import 
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
+import org.apache.cassandra.sidecar.common.data.CreateSliceRequestPayload;
 import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
 import org.apache.cassandra.sidecar.common.data.HealthResponse;
 import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
+import 
org.apache.cassandra.sidecar.common.data.RestoreJobSummaryResponsePayload;
 import org.apache.cassandra.sidecar.common.data.RingResponse;
 import org.apache.cassandra.sidecar.common.data.SSTableImportResponse;
 import org.apache.cassandra.sidecar.common.data.SchemaResponse;
 import org.apache.cassandra.sidecar.common.data.TimeSkewResponse;
 import org.apache.cassandra.sidecar.common.data.TokenRangeReplicasResponse;
+import org.apache.cassandra.sidecar.common.data.UpdateRestoreJobRequestPayload;
 import org.apache.cassandra.sidecar.common.utils.HttpRange;
 import org.jetbrains.annotations.Nullable;
 
 /**
  * The SidecarClient class to perform requests
  */
-public class SidecarClient implements AutoCloseable
+public class SidecarClient implements AutoCloseable, 
SidecarClientBlobRestoreExtension
 {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(SidecarClient.class);
 
@@ -443,6 +456,75 @@ public class SidecarClient implements AutoCloseable
                                                             .build());
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CompletableFuture<CreateRestoreJobResponsePayload> 
createRestoreJob(String keyspace,
+                                                                               
String table,
+                                                                               
CreateRestoreJobRequestPayload payload)
+    {
+        Objects.requireNonNull(payload, "payload cannot be null");
+        return executor.executeRequestAsync(requestBuilder()
+                                            .retryPolicy(new 
CreateRestoreJobRetryPolicy(defaultRetryPolicy))
+                                            .request(new 
CreateRestoreJobRequest(keyspace, table, payload))
+                                            .build());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CompletableFuture<Void> updateRestoreJob(String keyspace,
+                                                    String table,
+                                                    UUID jobId,
+                                                    
UpdateRestoreJobRequestPayload payload)
+    {
+        return executor.executeRequestAsync(requestBuilder()
+                                            .request(new 
UpdateRestoreJobRequest(keyspace, table, jobId, payload))
+                                            .build());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CompletableFuture<Void> abortRestoreJob(String keyspace, String 
table, UUID jobId)
+    {
+        return executor.executeRequestAsync(requestBuilder()
+                                            .request(new 
AbortRestoreJobRequest(keyspace, table, jobId))
+                                            .build());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CompletableFuture<RestoreJobSummaryResponsePayload> 
restoreJobSummary(String keyspace,
+                                                                               
  String table,
+                                                                               
  UUID jobId)
+    {
+        return executor.executeRequestAsync(requestBuilder()
+                                            .request(new 
RestoreJobSummaryRequest(keyspace, table, jobId))
+                                            .build());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CompletableFuture<Void> createRestoreJobSlice(SidecarInstance 
instance,
+                                                         String keyspace,
+                                                         String table,
+                                                         UUID jobId,
+                                                         
CreateSliceRequestPayload payload)
+    {
+        return executor.executeRequestAsync(requestBuilder()
+                                            
.singleInstanceSelectionPolicy(instance)
+                                            .request(new 
CreateRestoreJobSliceRequest(keyspace, table, jobId, payload))
+                                            .build());
+    }
+
     /**
      * Returns a copy of the request builder with the default parameters 
configured for the client.
      *
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClientBlobRestoreExtension.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClientBlobRestoreExtension.java
new file mode 100644
index 0000000..da752a2
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClientBlobRestoreExtension.java
@@ -0,0 +1,96 @@
+/*
+ * 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.cassandra.sidecar.client;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import 
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
+import org.apache.cassandra.sidecar.common.data.CreateSliceRequestPayload;
+import 
org.apache.cassandra.sidecar.common.data.RestoreJobSummaryResponsePayload;
+import org.apache.cassandra.sidecar.common.data.UpdateRestoreJobRequestPayload;
+
+/**
+ * An extension to sidecar client interface.
+ * It includes the APIs for invoking blob based restore.
+ */
+public interface SidecarClientBlobRestoreExtension
+{
+    /**
+     * Create a new restore job
+     *
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param payload  request payload
+     * @return a completable future of {@link CreateRestoreJobResponsePayload}
+     */
+    CompletableFuture<CreateRestoreJobResponsePayload> createRestoreJob(String 
keyspace, String table,
+                                                                        
CreateRestoreJobRequestPayload payload);
+
+    /**
+     * Update an existing restore job
+     *
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param jobId    job ID of the restore job to be updated
+     * @param payload  request payload
+     * @return a completable future
+     */
+    CompletableFuture<Void> updateRestoreJob(String keyspace, String table, 
UUID jobId,
+                                             UpdateRestoreJobRequestPayload 
payload);
+
+    /**
+     * Abort an existing restore job
+     *
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param jobId    job ID of the restore job to be updated
+     * @return a completable future
+     */
+    CompletableFuture<Void> abortRestoreJob(String keyspace, String table, 
UUID jobId);
+
+    /**
+     * Get the summary of an existing restore job
+     *
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param jobId    job ID of the restore job to be updated
+     * @return a completable future of {@link RestoreJobSummaryResponsePayload}
+     */
+    CompletableFuture<RestoreJobSummaryResponsePayload> 
restoreJobSummary(String keyspace, String table, UUID jobId);
+
+    /**
+     * Create a new slice in the restore job
+     * or check the status of restore of an existing slice identified by the 
{@link CreateSliceRequestPayload}
+     *
+     * @param instance the instance where the request will be executed
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param jobId    job ID of the restore job to create slice
+     * @param payload  request payload
+     * @return a completable future
+     */
+    CompletableFuture<Void> createRestoreJobSlice(SidecarInstance instance,
+                                                  String keyspace,
+                                                  String table,
+                                                  UUID jobId,
+                                                  CreateSliceRequestPayload 
payload);
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/AbortRestoreJobRequest.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/AbortRestoreJobRequest.java
new file mode 100644
index 0000000..4a1e02e
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/AbortRestoreJobRequest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cassandra.sidecar.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+
+/**
+ * Represents a request to abort a restore job
+ */
+public class AbortRestoreJobRequest extends Request
+{
+    /**
+     * Constructs a Sidecar request with the given {@code requestURI}. 
Defaults to {@code ssl} enabled.
+     *
+     * @param keyspace the keyspace in Cassandra
+     * @param table    the table name in Cassandra
+     * @param jobId    a unique identifier for the job
+     */
+    public AbortRestoreJobRequest(String keyspace, String table, UUID jobId)
+    {
+        super(requestURI(keyspace, table, jobId));
+    }
+
+    @Override
+    public HttpMethod method()
+    {
+        return HttpMethod.POST;
+    }
+
+    static String requestURI(String keyspace, String table, UUID jobId)
+    {
+        return ApiEndpointsV1.ABORT_RESTORE_JOB_ROUTE
+               .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+               .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+               .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+    }
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobRequest.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobRequest.java
new file mode 100644
index 0000000..b120389
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobRequest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cassandra.sidecar.client.request;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import 
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
+
+/**
+ * Represents a request to create a restore job
+ */
+public class CreateRestoreJobRequest extends 
DecodableRequest<CreateRestoreJobResponsePayload>
+implements JsonPayloadRequest
+{
+    private final CreateRestoreJobRequestPayload requestPayload;
+
+    /**
+     * Constructs a decodable request with the provided parameters
+     *
+     * @param keyspace       name of the keyspace in the cluster
+     * @param table          name of the table in the cluster
+     * @param requestPayload request payload
+     */
+    public CreateRestoreJobRequest(String keyspace, String table, 
CreateRestoreJobRequestPayload requestPayload)
+    {
+        super(requestURI(keyspace, table));
+        this.requestPayload = requestPayload;
+    }
+
+    @Override
+    public HttpMethod method()
+    {
+        return HttpMethod.POST;
+    }
+
+    @Override
+    public Object json()
+    {
+        return requestPayload;
+    }
+
+    static String requestURI(String keyspace, String table)
+    {
+        return ApiEndpointsV1.CREATE_RESTORE_JOB_ROUTE
+               .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+               .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table);
+    }
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobSliceRequest.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobSliceRequest.java
new file mode 100644
index 0000000..7c40c65
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/CreateRestoreJobSliceRequest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.cassandra.sidecar.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.CreateSliceRequestPayload;
+
+/**
+ * Represents a request to create a restore job slice
+ */
+public class CreateRestoreJobSliceRequest extends Request implements 
JsonPayloadRequest
+{
+    private final CreateSliceRequestPayload payload;
+
+    /**
+     * Constructs a Sidecar request with the given {@code requestURI}
+     *
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param jobId    job ID of the restore job to create slice
+     * @param payload  request payload
+     */
+    public CreateRestoreJobSliceRequest(String keyspace, String table, UUID 
jobId, CreateSliceRequestPayload payload)
+    {
+        this(keyspace, table, jobId, payload, false);
+    }
+
+    // todo: drop me once the dev endpoint is promoted
+    public CreateRestoreJobSliceRequest(String keyspace, String table, UUID 
jobId, CreateSliceRequestPayload payload,
+                                        boolean useDevApi)
+    {
+        super(requestURI(keyspace, table, jobId, useDevApi));
+        this.payload = payload;
+    }
+
+    @Override
+    public HttpMethod method()
+    {
+        return HttpMethod.POST;
+    }
+
+    @Override
+    public Object json()
+    {
+        return payload;
+    }
+
+    static String requestURI(String keyspace, String table, UUID jobId, 
boolean useDevApi)
+    {
+        String api = useDevApi
+                     ? ApiEndpointsV1.DEV_RESTORE_JOB_SLICES_ROUTE
+                     : ApiEndpointsV1.RESTORE_JOB_SLICES_ROUTE;
+        return api
+               .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+               .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+               .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+    }
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/JsonPayloadRequest.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/JsonPayloadRequest.java
new file mode 100644
index 0000000..5a6086c
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/JsonPayloadRequest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.cassandra.sidecar.client.request;
+
+/**
+ * A request that contains json payload
+ */
+public interface JsonPayloadRequest
+{
+    /**
+     * @return the JSON payload for the request
+     */
+    Object json();
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/RestoreJobSummaryRequest.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/RestoreJobSummaryRequest.java
new file mode 100644
index 0000000..bd358a6
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/RestoreJobSummaryRequest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.cassandra.sidecar.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import 
org.apache.cassandra.sidecar.common.data.RestoreJobSummaryResponsePayload;
+
+/**
+ * Represents a request to retrieve the summary of a restore job
+ */
+public class RestoreJobSummaryRequest extends 
DecodableRequest<RestoreJobSummaryResponsePayload>
+{
+    /**
+     * Constructs a Sidecar request with the given {@code requestURI}. 
Defaults to {@code ssl} enabled.
+     *
+     * @param keyspace the keyspace in Cassandra
+     * @param table    the table name in Cassandra
+     * @param jobId    a unique identifier for the job
+     */
+    public RestoreJobSummaryRequest(String keyspace, String table, UUID jobId)
+    {
+        super(requestURI(keyspace, table, jobId));
+    }
+
+    @Override
+    public HttpMethod method()
+    {
+        return HttpMethod.GET;
+    }
+
+    static String requestURI(String keyspace, String table, UUID jobId)
+    {
+        return ApiEndpointsV1.RESTORE_JOB_ROUTE
+               .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+               .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+               .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+    }
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/request/UpdateRestoreJobRequest.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/UpdateRestoreJobRequest.java
new file mode 100644
index 0000000..eba2a0e
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/request/UpdateRestoreJobRequest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.cassandra.sidecar.client.request;
+
+import java.util.UUID;
+
+import io.netty.handler.codec.http.HttpMethod;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
+import org.apache.cassandra.sidecar.common.data.UpdateRestoreJobRequestPayload;
+
+/**
+ * Represents a request to update a restore job
+ */
+public class UpdateRestoreJobRequest extends Request implements 
JsonPayloadRequest
+{
+    private final UpdateRestoreJobRequestPayload requestPayload;
+
+    /**
+     * Constructs a Sidecar request with the given parameters.
+     *
+     * @param keyspace name of the keyspace in the cluster
+     * @param table    name of the table in the cluster
+     * @param jobId    a unique identifier for the job
+     * @param requestPayload  request payload
+     */
+    public UpdateRestoreJobRequest(String keyspace, String table, UUID jobId,
+                                   UpdateRestoreJobRequestPayload 
requestPayload)
+    {
+        super(requestURI(keyspace, table, jobId));
+        this.requestPayload = requestPayload;
+    }
+
+    @Override
+    public HttpMethod method()
+    {
+        return HttpMethod.PATCH;
+    }
+
+    @Override
+    public Object json()
+    {
+        return requestPayload;
+    }
+
+    static String requestURI(String keyspace, String table, UUID jobId)
+    {
+        return ApiEndpointsV1.RESTORE_JOB_ROUTE
+               .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keyspace)
+               .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, table)
+               .replaceAll(ApiEndpointsV1.JOB_ID_PATH_PARAM, jobId.toString());
+    }
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/retry/CreateRestoreJobRetryPolicy.java
 
b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/CreateRestoreJobRetryPolicy.java
new file mode 100644
index 0000000..6200e7f
--- /dev/null
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/CreateRestoreJobRetryPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * 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.cassandra.sidecar.client.retry;
+
+import java.util.concurrent.CompletableFuture;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.apache.cassandra.sidecar.client.HttpResponse;
+import org.apache.cassandra.sidecar.client.exception.RetriesExhaustedException;
+import org.apache.cassandra.sidecar.client.request.Request;
+
+/**
+ * A policy to handle specific status codes for the response. Delegates all 
other response codes to the
+ * provided delegate
+ */
+public class CreateRestoreJobRetryPolicy extends RetryPolicy
+{
+    private final RetryPolicy delegate;
+
+    public CreateRestoreJobRetryPolicy(RetryPolicy delegate)
+    {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void onResponse(CompletableFuture<HttpResponse> responseFuture,
+                           Request request,
+                           HttpResponse response,
+                           Throwable throwable,
+                           int attempts,
+                           boolean canRetryOnADifferentHost,
+                           RetryAction retryAction)
+    {
+        if (response != null && (response.statusCode() == 
HttpResponseStatus.CONFLICT.code() ||
+                                 response.statusCode() == 
HttpResponseStatus.BAD_REQUEST.code()))
+        {
+            logger.error("Request exhausted. response={}, attempts={}", 
response, attempts);
+            
responseFuture.completeExceptionally(RetriesExhaustedException.of(attempts, 
request, response));
+        }
+        else
+        {
+            delegate.onResponse(responseFuture, request, response, throwable, 
attempts, canRetryOnADifferentHost,
+                                retryAction);
+        }
+    }
+}
diff --git 
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
 
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
index 17e0d9f..4f5a458 100644
--- 
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
+++ 
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
@@ -28,6 +28,7 @@ import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -57,9 +58,12 @@ import org.apache.cassandra.sidecar.client.retry.RetryAction;
 import org.apache.cassandra.sidecar.client.retry.RetryPolicy;
 import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
 import org.apache.cassandra.sidecar.common.NodeSettings;
+import org.apache.cassandra.sidecar.common.data.CreateRestoreJobRequestPayload;
+import 
org.apache.cassandra.sidecar.common.data.CreateRestoreJobResponsePayload;
 import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
 import org.apache.cassandra.sidecar.common.data.HealthResponse;
 import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
+import org.apache.cassandra.sidecar.common.data.RestoreJobSecrets;
 import org.apache.cassandra.sidecar.common.data.RingEntry;
 import org.apache.cassandra.sidecar.common.data.RingResponse;
 import org.apache.cassandra.sidecar.common.data.SSTableImportResponse;
@@ -67,12 +71,15 @@ import 
org.apache.cassandra.sidecar.common.data.SchemaResponse;
 import org.apache.cassandra.sidecar.common.data.TimeSkewResponse;
 import org.apache.cassandra.sidecar.common.data.TokenRangeReplicasResponse;
 import org.apache.cassandra.sidecar.common.utils.HttpRange;
+import org.apache.cassandra.sidecar.foundataion.RestoreJobSecretsGen;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.ACCEPTED;
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
 import static 
io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
 import static io.netty.handler.codec.http.HttpResponseStatus.PARTIAL_CONTENT;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static 
org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -1124,6 +1131,52 @@ abstract class SidecarClientTest
         validateResponseServed(ApiEndpointsV1.NODE_SETTINGS_ROUTE);
     }
 
+    @Test
+    void testAcceptCreateRestoreJobRequest()
+    {
+        enqueue(new MockResponse()
+                .setResponseCode(OK.code())
+                
.setBody("{\"jobId\":\"8e5799a4-d277-11ed-8d85-6916bb9b8056\",\"status\":\"CREATED\"}"));
+
+        UUID jobId = UUID.fromString("8e5799a4-d277-11ed-8d85-6916bb9b8056");
+        long expireAt = System.currentTimeMillis() + 10000;
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        CreateRestoreJobRequestPayload requestPayload = 
CreateRestoreJobRequestPayload.builder(secrets, expireAt)
+                                                                               
       .jobId(jobId)
+                                                                               
       .build();
+        CreateRestoreJobResponsePayload responsePayload = 
client.createRestoreJob("cycling",
+                                                                               
   "rank_by_year_and_name",
+                                                                               
   requestPayload)
+                                                                .join();
+
+        assertThat(responsePayload).isNotNull();
+        assertThat(responsePayload.jobId()).isEqualTo(jobId);
+        assertThat(responsePayload.status()).isEqualTo("CREATED");
+    }
+
+    @Test
+    void testCreateRestoreJobShouldNotRetryOnDifferentHostWithBadRequest()
+    {
+        enqueue(new MockResponse()
+                .setResponseCode(BAD_REQUEST.code())
+                .setBody("{\"status\":\"Fail\"," +
+                         "\"message\":\"Error while decoding values, check 
your request body\"}"));
+
+        UUID jobId = UUID.fromString("8e5799a4-d277-11ed-8d85-6916bb9b8056");
+        long expireAt = System.currentTimeMillis() + 10000;
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        CreateRestoreJobRequestPayload requestPayload = 
CreateRestoreJobRequestPayload.builder(secrets, expireAt)
+                                                                               
       .jobId(jobId)
+                                                                               
       .build();
+        assertThatException().isThrownBy(() -> 
client.createRestoreJob("badkeyspace",
+                                                                       
"bad_table",
+                                                                       
requestPayload)
+                                                     .join())
+                             
.withCauseInstanceOf(RetriesExhaustedException.class)
+                             .withMessageContaining("Unable to complete 
request '/api/v1/keyspaces/" +
+                                                    
"badkeyspace/tables/bad_table/restore-jobs' after 1 attempt");
+    }
+
     private void enqueue(MockResponse response)
     {
         for (MockWebServer server : servers)
diff --git a/common/build.gradle b/common/build.gradle
index e9e8d08..f87fad1 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -59,7 +59,8 @@ dependencies {
     compileOnly(group: 'com.datastax.cassandra', name: 
'cassandra-driver-core', version: '3.11.3')
     compileOnly(group: 'io.netty', name: 'netty-codec-http', version: 
'4.1.69.Final')
 
-    
testImplementation("com.google.guava:guava:${project.rootProject.guavaVersion}")
+    testImplementation(group: 'com.fasterxml.jackson.core', name: 
'jackson-databind', version: "${project.jacksonVersion}")
+    testImplementation("com.google.guava:guava:${project.guavaVersion}")
     testImplementation('org.mockito:mockito-inline:4.10.0')
     testImplementation("org.assertj:assertj-core:3.24.2")
     
testImplementation("org.junit.jupiter:junit-jupiter-api:${project.junitVersion}")
diff --git 
a/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java 
b/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
index 03b631f..1df905d 100644
--- 
a/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
+++ 
b/common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
@@ -102,6 +102,8 @@ public final class ApiEndpointsV1
     public static final String PER_RESTORE_JOB = RESTORE_JOBS + "/" + 
JOB_ID_PATH_PARAM;
     public static final String CREATE_RESTORE_JOB_ROUTE = API_V1 + 
PER_KEYSPACE + PER_TABLE + RESTORE_JOBS;
     public static final String RESTORE_JOB_SLICES_ROUTE = API_V1 + 
PER_KEYSPACE + PER_TABLE + PER_RESTORE_JOB + SLICES;
+    public static final String DEV_RESTORE_JOB_SLICES_ROUTE = API + "/dev" + 
PER_KEYSPACE +
+                                                              PER_TABLE + 
PER_RESTORE_JOB + SLICES;
     public static final String RESTORE_JOB_ROUTE = API_V1 + PER_KEYSPACE + 
PER_TABLE + PER_RESTORE_JOB;
     public static final String ABORT_RESTORE_JOB_ROUTE = API_V1 + PER_KEYSPACE 
+ PER_TABLE + PER_RESTORE_JOB + ABORT;
 
diff --git 
a/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateRestoreJobRequestPayloadTest.java
 
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateRestoreJobRequestPayloadTest.java
new file mode 100644
index 0000000..cf4d6b0
--- /dev/null
+++ 
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateRestoreJobRequestPayloadTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.cassandra.sidecar.common.data;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
+import org.apache.cassandra.sidecar.foundataion.RestoreJobSecretsGen;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CreateRestoreJobRequestPayloadTest
+{
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Test
+    void testSerDeser() throws JsonProcessingException
+    {
+        String id = "e870e5dc-d25e-11ed-afa1-0242ac120002";
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        long time = System.currentTimeMillis() + 10000;
+        Date date = Date.from(Instant.ofEpochMilli(time));
+        CreateRestoreJobRequestPayload req = 
CreateRestoreJobRequestPayload.builder(secrets, time)
+                                                                           
.jobId(UUID.fromString(id))
+                                                                           
.jobAgent("agent")
+                                                                           
.build();
+        String json = MAPPER.writeValueAsString(req);
+        CreateRestoreJobRequestPayload test = MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class);
+        assertThat(test.jobId()).hasToString(id);
+        assertThat(test.jobAgent()).isEqualTo("agent");
+        assertThat(test.secrets()).isEqualTo(secrets);
+        assertThat(test.expireAtInMillis()).isEqualTo(time);
+        assertThat(test.expireAtAsDate()).isEqualTo(date);
+        
assertThat(test.importOptions()).isEqualTo(SSTableImportOptions.defaults());
+    }
+
+    @Test
+    void testReadFromJsonFailsWithUnknownFields() throws 
JsonProcessingException
+    {
+        String uuid = "e870e5dc-d25e-11ed-afa1-0242ac120002";
+        String json = "{\"jobId\":\"" + uuid + "\"," +
+                      "\"jobAgent\":\"Spark Bulk Analytics\"," +
+                      "\"status\":\"Completed\"," +
+                      "\"expireAt\":" + System.currentTimeMillis() + 1000 +
+                      ",\"secrets\":" + 
MAPPER.writeValueAsString(RestoreJobSecretsGen.genRestoreJobSecrets()) + "}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class))
+        .isInstanceOf(UnrecognizedPropertyException.class)
+        .hasMessageContaining("Unrecognized field \"status\"");
+    }
+
+    @Test
+    void testReadFromJsonWithInvalidSecrets()
+    {
+        String json = "{\"secrets\":" +
+                      
"{\"readCredentials\":{\"accessKeyId\":\"accessKeyId\"}," +
+                      
"\"writeCredentials\":{\"accessKeyId\":\"accessKeyId\"}}}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class))
+        .isInstanceOf(ValueInstantiationException.class)
+        .hasMessageContaining("Cannot construct instance");
+    }
+
+    @Test
+    void testReadFromJsonWithPartialFields() throws JsonProcessingException
+    {
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        String json = "{\"secrets\":" + MAPPER.writeValueAsString(secrets) +
+                      ", \"expireAt\":" + System.currentTimeMillis() + 1000 + 
"}";
+        CreateRestoreJobRequestPayload req = MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class);
+        assertThat(req).isNotNull();
+        assertThat(req.jobId()).isNull();
+        assertThat(req.jobAgent()).isNull();
+        assertThat(req.secrets()).isEqualTo(secrets);
+        
assertThat(req.importOptions()).isEqualTo(SSTableImportOptions.defaults());
+    }
+
+    @Test
+    void testReadFromJsonFailsWithoutSecrets()
+    {
+        String json = "{\"expireAt\":" + System.currentTimeMillis() + 1000 + 
"}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class))
+        .isInstanceOf(ValueInstantiationException.class)
+        .hasCauseInstanceOf(NullPointerException.class)
+        .hasMessageContaining("secrets cannot be null");
+    }
+
+    @Test
+    void testReadFromJsonFailsWithOutExpireAt() throws JsonProcessingException
+    {
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        String json = "{\"secrets\":" + MAPPER.writeValueAsString(secrets) + 
"}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class))
+        .isInstanceOf(ValueInstantiationException.class)
+        .hasCauseInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("expireAt cannot be absent or a time in past");
+    }
+
+    @Test
+    void testReadFromJsonFailWithInvalidExpireAt() throws 
JsonProcessingException
+    {
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        String json = "{\"secrets\":" + MAPPER.writeValueAsString(secrets) +
+                      ", \"expireAt\":" + (System.currentTimeMillis() - 1000) 
+ "}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class))
+        .isInstanceOf(ValueInstantiationException.class)
+        .hasCauseInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("expireAt cannot be absent or a time in past");
+    }
+
+    @Test
+    void testReadFromJsonFailsWithInvalidJobId() throws JsonProcessingException
+    {
+        String json = "{\"jobId\":\"12951f25-d393-4158-9e90-ec0cbe05af21\"," +
+                      "\"expireAt\":\"" + System.currentTimeMillis() + 1000 + 
"\"," +
+                      "\"secrets\":" + 
MAPPER.writeValueAsString(RestoreJobSecretsGen.genRestoreJobSecrets()) + "}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class))
+        .isInstanceOf(ValueInstantiationException.class);
+    }
+
+    @Test
+    void testReadFromJsonWithoutConsistencyLevel() throws 
JsonProcessingException
+    {
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        long time = System.currentTimeMillis() + 10000;
+        Date date = Date.from(Instant.ofEpochMilli(time));
+        String json = "{\"jobId\":\"e870e5dc-d25e-11ed-afa1-0242ac120002\"," +
+                      "\"jobAgent\":\"agent\"," +
+                      "\"expireAt\":\"" + time + "\"," +
+                      "\"secrets\":" + MAPPER.writeValueAsString(secrets) + 
"}";
+        CreateRestoreJobRequestPayload test = MAPPER.readValue(json, 
CreateRestoreJobRequestPayload.class);
+        
assertThat(test.jobId()).hasToString("e870e5dc-d25e-11ed-afa1-0242ac120002");
+        assertThat(test.jobAgent()).isEqualTo("agent");
+        assertThat(test.secrets()).isEqualTo(secrets);
+        assertThat(test.expireAtInMillis()).isEqualTo(time);
+        assertThat(test.expireAtAsDate()).isEqualTo(date);
+        
assertThat(test.importOptions()).isEqualTo(SSTableImportOptions.defaults());
+    }
+
+
+    @Test
+    void testBuilder()
+    {
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        CreateRestoreJobRequestPayload req = CreateRestoreJobRequestPayload
+                                             .builder(secrets, 
System.currentTimeMillis() + 10000)
+                                             .jobAgent("agent")
+                                             .updateImportOptions(options -> {
+                                                 options
+                                                 .resetLevel(false)
+                                                 .clearRepaired(false);
+                                             })
+                                             .build();
+        assertThat(req.secrets()).isEqualTo(secrets);
+        assertThat(req.jobAgent()).isEqualTo("agent");
+        
assertThat(req.importOptions()).isEqualTo(SSTableImportOptions.defaults()
+                                                                      
.resetLevel(false)
+                                                                      
.clearRepaired(false));
+    }
+}
diff --git 
a/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateSliceRequestPayloadTest.java
 
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateSliceRequestPayloadTest.java
new file mode 100644
index 0000000..842cf2a
--- /dev/null
+++ 
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/CreateSliceRequestPayloadTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.cassandra.sidecar.common.data;
+
+import java.math.BigInteger;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CreateSliceRequestPayloadTest
+{
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Test
+    void testReadFromJsonString() throws JsonProcessingException
+    {
+        int bucketId = 0;
+        String json = "{\"sliceId\":\"TASK_ID-RETRY_COUNT-SEQUENCE\"," +
+                      "\"bucketId\": " + bucketId + "," +
+                      "\"startToken\":\"1\"," +
+                      "\"endToken\":\"10\"," +
+                      "\"storageBucket\":\"myBucket\"," +
+                      "\"storageKey\":\"/path/to/object\"," +
+                      "\"sliceChecksum\":\"12321abc\"}";
+        CreateSliceRequestPayload req = MAPPER.readValue(json, 
CreateSliceRequestPayload.class);
+        assertThat(req).isNotNull();
+        assertThat(req.sliceId()).isEqualTo("TASK_ID-RETRY_COUNT-SEQUENCE");
+        assertThat(req.bucketId()).isEqualTo(bucketId);
+        assertThat(req.checksum()).isEqualTo("12321abc");
+        assertThat(req.bucket()).isEqualTo("myBucket");
+        assertThat(req.key()).isEqualTo("/path/to/object");
+        assertThat(req.startToken()).isEqualTo(BigInteger.ONE);
+        assertThat(req.endToken()).isEqualTo(BigInteger.TEN);
+        assertThat(req.compressedSize())
+        .describedAs("size is not present in the json")
+        .isNull();
+        assertThat(req.uncompressedSize())
+        .describedAs("uncompressed size is not present in the json")
+        .isNull();
+    }
+
+    @Test
+    void testReadFromJsonFailsWithMissingFields()
+    {
+        String json = "{\"sliceId\":\"TASK_ID-RETRY_COUNT-SEQUENCE\"}";
+        assertThatThrownBy(() -> MAPPER.readValue(json, 
CreateSliceRequestPayload.class))
+        .isInstanceOf(ValueInstantiationException.class)
+        .hasCauseInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("Invalid create slice request payload");
+    }
+
+    @Test
+    void testSerDeser() throws JsonProcessingException
+    {
+        int bucketId = 2;
+        CreateSliceRequestPayload req = new CreateSliceRequestPayload("id", 
bucketId, "bucket", "key", "checksum",
+                                                                      
BigInteger.ONE, BigInteger.TEN, 234L, 123L);
+        String json = MAPPER.writeValueAsString(req);
+        CreateSliceRequestPayload test = MAPPER.readValue(json, 
CreateSliceRequestPayload.class);
+        assertThat(test.sliceId()).isEqualTo("id");
+        assertThat(test.bucketId()).isEqualTo(bucketId);
+        assertThat(test.bucket()).isEqualTo("bucket");
+        assertThat(test.key()).isEqualTo("key");
+        assertThat(test.checksum()).isEqualTo("checksum");
+        assertThat(test.startToken()).isEqualTo(BigInteger.ONE);
+        assertThat(test.endToken()).isEqualTo(BigInteger.TEN);
+        assertThat(test.uncompressedSize()).isEqualTo(234L);
+        assertThat(test.compressedSize()).isEqualTo(123L);
+    }
+
+    @Test
+    void testSerDeserWithoutOptionalFields() throws JsonProcessingException
+    {
+        CreateSliceRequestPayload req = new CreateSliceRequestPayload("id", 1, 
"bucket", "key", "checksum",
+                                                                      
BigInteger.ONE, BigInteger.TEN, null, null);
+        String json = MAPPER.writeValueAsString(req);
+        assertThat(json).doesNotContain("uncompressedSize")
+                        .doesNotContain("compressedSize");
+        CreateSliceRequestPayload test = MAPPER.readValue(json, 
CreateSliceRequestPayload.class);
+        assertThat(test.compressedSize())
+        .describedAs("size was not set in the original object")
+        .isNull();
+        assertThat(test.uncompressedSize())
+        .describedAs("uncompressed size was not set in the original object")
+        .isNull();
+    }
+}
diff --git 
a/common/src/test/java/org/apache/cassandra/sidecar/common/data/UpdateRestoreJobRequestPayloadTest.java
 
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/UpdateRestoreJobRequestPayloadTest.java
new file mode 100644
index 0000000..580c49a
--- /dev/null
+++ 
b/common/src/test/java/org/apache/cassandra/sidecar/common/data/UpdateRestoreJobRequestPayloadTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.cassandra.sidecar.common.data;
+
+import java.time.Instant;
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.cassandra.sidecar.foundataion.RestoreJobSecretsGen;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UpdateRestoreJobRequestPayloadTest
+{
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Test
+    void testEmptySecrets() throws JsonProcessingException
+    {
+        String json = "{\"jobAgent\":\"Spark Bulk Analytics\"}";
+        UpdateRestoreJobRequestPayload req = mapper.readValue(json, 
UpdateRestoreJobRequestPayload.class);
+
+        assertThat(req).isNotNull();
+        assertThat(req.jobAgent()).isEqualTo("Spark Bulk Analytics");
+        assertThat(req.status()).isNull();
+        assertThat(req.expireAtInMillis()).isNull();
+        assertThat(req.expireAtAsDate()).isNull();
+        assertThat(req.secrets()).isNull();
+    }
+
+    @Test
+    void testExpireAt() throws JsonProcessingException
+    {
+        long time = System.currentTimeMillis() + 1000;
+        Date date = Date.from(Instant.ofEpochMilli(time));
+        String json = "{\"expireAt\":" + time + "}";
+        UpdateRestoreJobRequestPayload req = mapper.readValue(json, 
UpdateRestoreJobRequestPayload.class);
+
+        assertThat(req.expireAtAsDate()).isEqualTo(date);
+        assertThat(req.expireAtInMillis()).isEqualTo(time);
+    }
+
+    @Test
+    void testJsonSerializationShouldIgnoreUnwantedFields() throws 
JsonProcessingException
+    {
+        RestoreJobSecrets secrets = 
RestoreJobSecretsGen.genRestoreJobSecrets();
+        UpdateRestoreJobRequestPayload payload = new 
UpdateRestoreJobRequestPayload(null, secrets
+                                                                               
     , null, null);
+        String json = mapper.writeValueAsString(payload);
+        assertThat(json).contains(RestoreJobConstants.JOB_SECRETS)
+                        .doesNotContain(RestoreJobConstants.JOB_AGENT,
+                                        RestoreJobConstants.JOB_EXPIRE_AT,
+                                        RestoreJobConstants.JOB_STATUS)
+                        .doesNotContain("empty"); // ignored by @JsonIgnore
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to