content: /*
   * 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.tools.nodetool;
  
  import java.io.IOException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.time.Instant;
  import java.util.Map;
  import java.util.Optional;
  import java.util.concurrent.TimeUnit;
  import java.util.regex.Pattern;
  import javax.management.openmbean.TabularData;
  
  import org.junit.AfterClass;
  import org.junit.Before;
  import org.junit.BeforeClass;
  import org.junit.Test;
  
  import org.apache.cassandra.cql3.CQLTester;
  import org.apache.cassandra.db.Keyspace;
  import org.apache.cassandra.io.util.File;
  import org.apache.cassandra.schema.TableMetadata;
  import org.apache.cassandra.service.snapshot.SnapshotManager;
  import org.apache.cassandra.service.snapshot.SnapshotManifest;
  import org.apache.cassandra.tools.NodeProbe;
  import org.apache.cassandra.tools.ToolRunner.ToolResult;
  
  import static java.lang.String.format;
  import static java.time.temporal.ChronoUnit.HOURS;
  import static java.time.temporal.ChronoUnit.SECONDS;
  import static java.util.Collections.emptyMap;
  import static org.apache.cassandra.config.DatabaseDescriptor.getAllDataFileLocations;
  import static org.apache.cassandra.tools.ToolRunner.invokeNodetool;
  import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.awaitility.Awaitility.await;
  import static org.junit.Assert.assertTrue;
  
  public class ClearSnapshotTest extends CQLTester
  {
      private static final Pattern DASH_PATTERN = Pattern.compile("-");
      private static NodeProbe probe;
  
      @BeforeClass
      public static void setup() throws Exception
      {
          startJMXServer();
          requireNetwork();
          probe = new NodeProbe(jmxHost, jmxPort);
      }
  
      @Before
      public void clearAllSnapshots()
      {
          SnapshotManager.instance.clearAllSnapshots();
      }
  
      @AfterClass
      public static void teardown() throws IOException
      {
          probe.close();
      }
  
      @Test
      public void testClearSnapshot_RemoveByName()
      {
          ToolResult tool = invokeNodetool("snapshot", "-t", "some-name");
          tool.assertOnCleanExit();
          assertThat(tool.getStdout()).isNotEmpty();
  
          Map<String, TabularData> snapshots_before = probe.getSnapshotDetails(emptyMap());
          assertThat(snapshots_before).containsKey("some-name");
  
          tool = invokeNodetool("clearsnapshot", "-t", "some-name");
          tool.assertOnCleanExit();
          assertThat(tool.getStdout()).isNotEmpty();
  
          Map<String, TabularData> snapshots_after = probe.getSnapshotDetails(emptyMap());
          assertThat(snapshots_after).doesNotContainKey("some-name");
      }
  
      @Test
      public void testClearSnapshot_RemoveMultiple()
      {
          ToolResult tool = invokeNodetool("snapshot", "-t", "some-name");
          tool.assertOnCleanExit();
          assertThat(tool.getStdout()).isNotEmpty();
  
          tool = invokeNodetool("snapshot", "-t", "some-other-name");
          tool.assertOnCleanExit();
          assertThat(tool.getStdout()).isNotEmpty();
  
          Map<String, TabularData> snapshots_before = probe.getSnapshotDetails(emptyMap());
          assertThat(snapshots_before).hasSize(2);
  
          tool = invokeNodetool("clearsnapshot", "--all");
          tool.assertOnCleanExit();
          assertThat(tool.getStdout()).isNotEmpty();
  
          Map<String, TabularData> snapshots_after = probe.getSnapshotDetails(emptyMap());
          assertThat(snapshots_after).isEmpty();
      }
  
      @Test
      public void testClearSnapshotWithOlderThanFlag() throws Throwable
      {
          Instant start = Instant.ofEpochMilli(currentTimeMillis());
          prepareData(start);
  
          // wait 10 seconds for the sake of the test
          await().timeout(15, TimeUnit.SECONDS).until(() -> Instant.now().isAfter(start.plusSeconds(10)));
  
          // clear all snapshots for specific keyspace older than 3 hours for a specific keyspace
          invokeNodetool("clearsnapshot", "--older-than", "3h", "--all", "--", KEYSPACE).assertOnCleanExit();
  
          await().until(() -> {
              String output = invokeNodetool("listsnapshots").getStdout();
              return !output.contains("snapshot-to-clear-ks1-tb1") &&
                     output.contains("some-other-snapshot-ks1-tb1") &&
                     output.contains("last-snapshot-ks1-tb1") &&
                     output.contains("snapshot-to-clear-ks2-tb2") &&
                     output.contains("some-other-snapshot-ks2-tb2") &&
                     output.contains("last-snapshot-ks2-tb2");
          });
  
          // clear all snapshots older than 2 hours for all keyspaces
          invokeNodetool("clearsnapshot", "--older-than", "2h", "--all").assertOnCleanExit();
  
          await().until(() -> {
              String output = invokeNodetool("listsnapshots").getStdout();
  
              return !output.contains("some-other-snapshot-ks1-tb1") &&
                     output.contains("last-snapshot-ks1-tb1") &&
                     !output.contains("snapshot-to-clear-ks2-tb2") &&
                     !output.contains("some-other-snapshot-ks2-tb2") &&
                     output.contains("last-snapshot-ks2-tb2");
          });
  
          // clear all snapshosts older than 1 second
          invokeNodetool("clearsnapshot", "--older-than", "1s", "--all", "--", currentKeyspace()).assertOnCleanExit();
  
          await().until(() -> {
              String output = invokeNodetool("listsnapshots").getStdout();
              return output.contains("last-snapshot-ks1-tb1") &&
                     !output.contains("last-snapshot-ks2-tb2");
          });
  
          invokeNodetool("clearsnapshot", "--older-than", "1s", "--all").assertOnCleanExit();
          await().until(() -> !invokeNodetool("listsnapshots").getStdout().contains("last-snapshot-ks1-tb1"));
      }
  
  
      @Test
      public void testClearSnapshotWithOlderThanTimestampFlag() throws Throwable
      {
          Instant start = Instant.ofEpochMilli(currentTimeMillis());
          prepareData(start);
  
          // wait 10 seconds for the sake of the test
          await().timeout(15, TimeUnit.SECONDS).until(() -> Instant.now().isAfter(start.plusSeconds(10)));
  
          // clear all snapshots for specific keyspace older than 3 hours for a specific keyspace
          invokeNodetool("clearsnapshot", "--older-than-timestamp",
                         Instant.now().minus(3, HOURS).toString(),
                         "--all", "--", KEYSPACE).assertOnCleanExit();
  
          await().until(() -> {
              String output = invokeNodetool("listsnapshots").getStdout();
              return !output.contains("snapshot-to-clear-ks1-tb1") &&
                     output.contains("some-other-snapshot-ks1-tb1") &&
                     output.contains("last-snapshot-ks1-tb1") &&
                     output.contains("snapshot-to-clear-ks2-tb2") &&
                     output.contains("some-other-snapshot-ks2-tb2") &&
                     output.contains("last-snapshot-ks2-tb2");
          });
  
          // clear all snapshots older than 2 hours for all keyspaces
          invokeNodetool("clearsnapshot", "--older-than-timestamp",
                         Instant.now().minus(2, HOURS).toString(),
                         "--all").assertOnCleanExit();
  
          await().until(() -> {
              String output = invokeNodetool("listsnapshots").getStdout();
  
              return !output.contains("some-other-snapshot-ks1-tb1") &&
                     output.contains("last-snapshot-ks1-tb1") &&
                     !output.contains("snapshot-to-clear-ks2-tb2") &&
                     !output.contains("some-other-snapshot-ks2-tb2") &&
                     output.contains("last-snapshot-ks2-tb2");
          });
  
          // clear all snapshots older than now for all keyspaces
          invokeNodetool("clearsnapshot", "--older-than-timestamp",
                         Instant.now().toString(),
                         "--all").assertOnCleanExit();
  
          await().until(() -> {
              String output = invokeNodetool("listsnapshots").getStdout();
              return !output.contains("last-snapshot-ks1-tb1") &&
                     !output.contains("last-snapshot-ks2-tb2");
          });
      }
  
      @Test
      public void testIncompatibleFlags()
      {
          ToolResult invalidCommand1 = invokeNodetool("clearsnapshot",
                                                      "--older-than-timestamp", Instant.now().toString(),
                                                      "--older-than", "3h",
                                                      "--all");
          invalidCommand1.asserts().failure();
          assertTrue(invalidCommand1.getStdout().contains("Specify only one of --older-than or --older-than-timestamp"));
  
          ToolResult invalidCommand2 = invokeNodetool("clearsnapshot", "-t", "some-snapshot-tag", "--all");
          invalidCommand2.asserts().failure();
          assertTrue(invalidCommand2.getStdout().contains("Specify only one of snapshot name or --all"));
  
          ToolResult invalidCommand3 = invokeNodetool("clearsnapshot", "--", "keyspace");
          invalidCommand3.asserts().failure();
          assertTrue(invalidCommand3.getStdout().contains("Specify snapshot name or --all"));
  
          ToolResult invalidCommand4 = invokeNodetool("clearsnapshot",
                                                      "--older-than-timestamp", Instant.now().toString(),
                                                      "-t", "some-snapshot-tag");
          invalidCommand4.asserts().failure();
          assertTrue(invalidCommand4.getStdout().contains("Specifying snapshot name together with --older-than-timestamp flag is not allowed"));
  
          ToolResult invalidCommand5 = invokeNodetool("clearsnapshot",
                                                      "--older-than", "3h",
                                                      "-t", "some-snapshot-tag");
          invalidCommand5.asserts().failure();
          assertTrue(invalidCommand5.getStdout().contains("Specifying snapshot name together with --older-than flag is not allowed"));
  
          ToolResult invalidCommand6 = invokeNodetool("clearsnapshot",
                                                      "--older-than-timestamp", "123",
                                                      "--all", "--", "somekeyspace");
          invalidCommand6.asserts().failure();
          assertTrue(invalidCommand6.getStdout().contains("Parameter --older-than-timestamp has to be a valid instant in ISO format."));
  
          ToolResult invalidCommand7 = invokeNodetool("clearsnapshot",
                                                      "--older-than", "3k",
                                                      "--all", "--", "somekeyspace");
          invalidCommand7.asserts().failure();
          assertTrue(invalidCommand7.getStdout().contains("Invalid duration: 3k"));
      }
  
      private void rewriteManifest(String tableId,
                                   String[] dataDirs,
                                   String keyspace,
                                   String tableName,
                                   String snapshotName,
                                   Instant createdAt) throws Exception
      {
          Path manifestPath = findManifest(dataDirs, keyspace, tableId, tableName, snapshotName);
          SnapshotManifest manifest = SnapshotManifest.deserializeFromJsonFile(new File(manifestPath));
          SnapshotManifest manifestWithEphemeralFlag = new SnapshotManifest(manifest.files, null, createdAt, false);
          manifestWithEphemeralFlag.serializeToJsonFile(new File(manifestPath));
      }
  
      private Path findManifest(String[] dataDirs, String keyspace, String tableId, String tableName, String snapshotName)
      {
          for (String dataDir : dataDirs)
          {
              Path manifest = Paths.get(dataDir)
                                   .resolve(keyspace)
                                   .resolve(format("%s-%s", tableName, tableId))
                                   .resolve("snapshots")
                                   .resolve(snapshotName)
                                   .resolve("manifest.json");
  
              if (Files.exists(manifest))
              {
                  return manifest;
              }
          }
  
          throw new IllegalStateException("Unable to find manifest!");
      }
  
      private void prepareData(Instant start) throws Throwable
      {
          String tableName = createTable(KEYSPACE, "CREATE TABLE %s (id int primary key)");
          execute("INSERT INTO %s (id) VALUES (?)", 1);
          flush(KEYSPACE);
  
          String keyspace2 = createKeyspace("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}");
          String tableName2 = createTable(keyspace2, "CREATE TABLE %s (id int primary key)");
          execute(formatQuery(keyspace2, "INSERT INTO %s (id) VALUES (?)"), 1);
          flush(keyspace2);
  
          invokeNodetool("snapshot", "-t", "snapshot-to-clear-ks1-tb1", "-cf", tableName, "--", KEYSPACE).assertOnCleanExit();
          invokeNodetool("snapshot", "-t", "some-other-snapshot-ks1-tb1", "-cf", tableName, "--", KEYSPACE).assertOnCleanExit();
          invokeNodetool("snapshot", "-t", "last-snapshot-ks1-tb1", "-cf", tableName, "--", KEYSPACE).assertOnCleanExit();
  
          invokeNodetool("snapshot", "-t", "snapshot-to-clear-ks2-tb2", "-cf", tableName2, "--", keyspace2).assertOnCleanExit();
          invokeNodetool("snapshot", "-t", "some-other-snapshot-ks2-tb2", "-cf", tableName2, "--", keyspace2).assertOnCleanExit();
          invokeNodetool("snapshot", "-t", "last-snapshot-ks2-tb2", "-cf", tableName2, "--", keyspace2).assertOnCleanExit();
  
          Optional<TableMetadata> tableMetadata = Keyspace.open(KEYSPACE).getMetadata().tables.get(tableName);
          Optional<TableMetadata> tableMetadata2 = Keyspace.open(keyspace2).getMetadata().tables.get(tableName2);
  
          String tableId = DASH_PATTERN.matcher(tableMetadata.orElseThrow(() -> new IllegalStateException(format("no metadata found for %s.%s", KEYSPACE, tableName)))
                                                .id.asUUID().toString()).replaceAll("");
  
          String tableId2 = DASH_PATTERN.matcher(tableMetadata2.orElseThrow(() -> new IllegalStateException(format("no metadata found for %s.%s", keyspace2, tableName2)))
                                                 .id.asUUID().toString()).replaceAll("");
  
          SnapshotManager.instance.close();
  
          rewriteManifest(tableId, getAllDataFileLocations(), KEYSPACE, tableName, "snapshot-to-clear-ks1-tb1", start.minus(5, HOURS));
          rewriteManifest(tableId, getAllDataFileLocations(), KEYSPACE, tableName, "some-other-snapshot-ks1-tb1", start.minus(2, HOURS));
          rewriteManifest(tableId, getAllDataFileLocations(), KEYSPACE, tableName, "last-snapshot-ks1-tb1", start.minus(1, SECONDS));
          rewriteManifest(tableId2, getAllDataFileLocations(), keyspace2, tableName2, "snapshot-to-clear-ks2-tb2", start.minus(5, HOURS));
          rewriteManifest(tableId2, getAllDataFileLocations(), keyspace2, tableName2, "some-other-snapshot-ks2-tb2", start.minus(2, HOURS));
          rewriteManifest(tableId2, getAllDataFileLocations(), keyspace2, tableName2, "last-snapshot-ks2-tb2", start.minus(1, SECONDS));
  
          SnapshotManager.instance.start(true);
      }
  }
  