Skip to content

Commit 2a94bdc

Browse files
authored
Add a command to delete feature flags (#2904)
This allows us to delete old ones to avoid confusion, and so that we can more easily clean up the codebase.
1 parent 50fa49e commit 2a94bdc

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.tools;
16+
17+
import static com.google.common.base.Preconditions.checkArgument;
18+
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
19+
20+
import com.beust.jcommander.Parameter;
21+
import com.beust.jcommander.Parameters;
22+
import google.registry.model.common.FeatureFlag;
23+
import java.util.List;
24+
25+
/**
26+
* Command to remove a {@link FeatureFlag} from the database entirely.
27+
*
28+
* <p>This should be used when a flag has been deprecated entirely, and we want to remove it, to
29+
* avoid having old invalid data in the database.
30+
*
31+
* <p>This command uses the native query format so that it is able to delete values that are no
32+
* longer part of the {@link FeatureFlag} enum.
33+
*
34+
* <p>This uses {@link ConfirmingCommand} instead of {@link MutatingCommand} because of the
35+
* nonstandard deletion flow required by the fact that the enum constant may already have been
36+
* removed.
37+
*/
38+
@Parameters(separators = " =", commandDescription = "Delete a FeatureFlag from the database")
39+
public class DeleteFeatureFlagCommand extends ConfirmingCommand {
40+
41+
@Parameter(description = "Feature flag to delete", required = true)
42+
private List<String> mainParameters;
43+
44+
@Override
45+
protected boolean checkExecutionState() {
46+
checkArgument(
47+
mainParameters != null && !mainParameters.isEmpty() && !mainParameters.getFirst().isBlank(),
48+
"Must provide a non-blank feature flag as the main parameter");
49+
boolean exists =
50+
tm().transact(
51+
() ->
52+
(long)
53+
tm().getEntityManager()
54+
.createNativeQuery(
55+
"SELECT COUNT(*) FROM \"FeatureFlag\" WHERE feature_name ="
56+
+ " :featureName",
57+
long.class)
58+
.setParameter("featureName", mainParameters.getFirst())
59+
.getSingleResult()
60+
> 0);
61+
if (!exists) {
62+
System.out.printf("No flag found with name '%s'", mainParameters.getFirst());
63+
}
64+
return exists;
65+
}
66+
67+
@Override
68+
protected String prompt() throws Exception {
69+
return String.format("Delete feature flag named '%s'?", mainParameters.getFirst());
70+
}
71+
72+
@Override
73+
protected String execute() throws Exception {
74+
String featureName = mainParameters.getFirst();
75+
tm().transact(
76+
() ->
77+
tm().getEntityManager()
78+
.createNativeQuery(
79+
"DELETE FROM \"FeatureFlag\" WHERE feature_name = :featureName")
80+
.setParameter("featureName", featureName)
81+
.executeUpdate());
82+
return String.format("Deleted feature flag with name '%s'", featureName);
83+
}
84+
}

core/src/main/java/google/registry/tools/RegistryTool.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public final class RegistryTool {
5454
.put("curl", CurlCommand.class)
5555
.put("delete_allocation_tokens", DeleteAllocationTokensCommand.class)
5656
.put("delete_domain", DeleteDomainCommand.class)
57+
.put("delete_feature_flag", DeleteFeatureFlagCommand.class)
5758
.put("delete_host", DeleteHostCommand.class)
5859
.put("delete_premium_list", DeletePremiumListCommand.class)
5960
.put("delete_reserved_list", DeleteReservedListCommand.class)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.tools;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static google.registry.model.common.FeatureFlag.FeatureName.TEST_FEATURE;
19+
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
20+
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
21+
import static google.registry.testing.DatabaseHelper.persistResource;
22+
import static google.registry.util.DateTimeUtils.START_OF_TIME;
23+
24+
import com.google.common.collect.ImmutableSortedMap;
25+
import google.registry.model.common.FeatureFlag;
26+
import org.junit.jupiter.api.Test;
27+
28+
/** Tests for {@link DeleteFeatureFlagCommand}. */
29+
public class DeleteFeatureFlagCommandTest extends CommandTestCase<DeleteFeatureFlagCommand> {
30+
31+
@Test
32+
void testSimpleSuccess() throws Exception {
33+
persistResource(
34+
new FeatureFlag()
35+
.asBuilder()
36+
.setFeatureName(TEST_FEATURE)
37+
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
38+
.build());
39+
assertThat(tm().transact(() -> FeatureFlag.isActiveNow(TEST_FEATURE))).isTrue();
40+
runCommandForced("TEST_FEATURE");
41+
assertThat(FeatureFlag.getUncached(TEST_FEATURE)).isEmpty();
42+
}
43+
44+
@Test
45+
void testSuccess_noLongerPartOfEnum() throws Exception {
46+
tm().transact(
47+
() ->
48+
tm().getEntityManager()
49+
.createNativeQuery(
50+
"INSERT INTO \"FeatureFlag\" VALUES('nonexistent',"
51+
+ " '\"1970-01-01T00:00:00.000Z\"=>\"INACTIVE\"')")
52+
.executeUpdate());
53+
assertThat(
54+
tm().transact(
55+
() ->
56+
tm().query(
57+
"SELECT COUNT(*) FROM FeatureFlag WHERE featureName ="
58+
+ " 'nonexistent'",
59+
long.class)
60+
.getSingleResult()))
61+
.isEqualTo(1L);
62+
runCommandForced("nonexistent");
63+
assertThat(
64+
tm().transact(
65+
() ->
66+
tm().query(
67+
"SELECT COUNT(*) FROM FeatureFlag WHERE featureName ="
68+
+ " 'nonexistent'",
69+
long.class)
70+
.getSingleResult()))
71+
.isEqualTo(0L);
72+
}
73+
74+
@Test
75+
void testFailure_nonExistent() throws Exception {
76+
runCommandForced("nonexistent");
77+
assertInStdout("No flag found with name 'nonexistent'");
78+
}
79+
}

0 commit comments

Comments
 (0)