Commit 5643c9c0 authored by Stephen Nimmo's avatar Stephen Nimmo
Browse files

Tests working locally against minikube

parent 7915ff03
Pipeline #74482 failed with stage
in 2 minutes and 54 seconds
......@@ -48,7 +48,7 @@
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>10.0.0</version>
<version>12.0.1</version>
</dependency>
<!-- Test Dependencies -->
......@@ -105,14 +105,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
......
......@@ -2,11 +2,13 @@ package org.opengroup.osdu.streaming.service;
import org.opengroup.osdu.streaming.model.StreamRecord;
import java.util.Optional;
public interface DeploymentAdminService {
StreamDeploymentStatus createStreamDeployment(StreamRecord streamRecord);
Optional<StreamDeploymentStatus> findStreamDeploymentStatus(StreamRecord streamRecord);
StreamDeploymentStatus getStreamDeploymentStatus(String id);
StreamDeploymentStatus createStreamDeployment(StreamRecord streamRecord);
StreamDeploymentStatus startStreamDeployment(StreamRecord streamRecord);
......
......@@ -3,7 +3,6 @@ package org.opengroup.osdu.streaming.service;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.AppsV1Api;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.*;
import io.kubernetes.client.util.PatchUtils;
import org.opengroup.osdu.streaming.exception.StreamAdminException;
......@@ -12,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.context.annotation.RequestScope;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
......@@ -22,41 +22,56 @@ public class DeploymentAdminServiceImpl implements DeploymentAdminService {
private static final String PATCH_REPLICAS = "[{\"op\":\"replace\",\"path\":\"/spec/replicas\",\"value\":%s}]";
private CoreV1Api coreV1Api;
private AppsV1Api appsV1Api;
private String namespace;
private String deploymentSuffix = "deployment";
private String selectorMatchLabelKey = "run";
private static final String DEPLOYMENT_NAME_SUFFIX = "-deployment";
private static final String CONTAINER_NAME_SUFFIX = "-container";
private static final String SELECTOR_MATCH_LABEL_KEY = "run";
public DeploymentAdminServiceImpl(CoreV1Api coreV1Api, AppsV1Api appsV1Api, @Value("${deployment.namespace}") String namespace) {
this.coreV1Api = coreV1Api;
this.appsV1Api = appsV1Api;
public DeploymentAdminServiceImpl(@Value("${deployment.namespace}") String namespace, AppsV1Api appsV1Api) {
this.namespace = namespace;
this.appsV1Api = appsV1Api;
}
@Override
public Optional<StreamDeploymentStatus> findStreamDeploymentStatus(StreamRecord streamRecord) {
this.validateStreamRecord(streamRecord);
String deploymentName = this.getDeploymentName(streamRecord);
Optional<V1Deployment> optional = findExistingDeployment(deploymentName);
return optional.isPresent() ? Optional.of(new StreamDeploymentStatus(
optional.get().getMetadata().getName(),
optional.get().getMetadata().getNamespace(),
optional.get().getSpec().getReplicas() > 0 ? StreamDeploymentStatus.Status.RUNNING : StreamDeploymentStatus.Status.STOPPED,
LocalDateTime.now())) : Optional.empty();
}
@Override
public StreamDeploymentStatus createStreamDeployment(StreamRecord streamRecord) {
this.validateStreamRecord(streamRecord);
String normalizedName = this.getNormalizedName(streamRecord);
String deploymentName = this.getDeploymentName(streamRecord);
if (this.findExistingDeployment(namespace, deploymentName).isPresent()) {
throw new StreamAdminException("Unable to create ");
String containerName = this.getContainerName(streamRecord);
if (this.findExistingDeployment(deploymentName).isPresent()) {
throw new StreamAdminException(String.format("Unable to create StreamDeployment. A deployment already exists for %s.", deploymentName));
}
V1DeploymentBuilder v1DeploymentBuilder = new V1DeploymentBuilder();
v1DeploymentBuilder
.withNewMetadata()
.withName(String.format("%s-%s", deploymentName, this.deploymentSuffix))
.withName(deploymentName)
.withNamespace(this.namespace)
.endMetadata()
.withNewSpec()
.withReplicas(0)
.withNewSelector()
.withMatchLabels(Map.of(this.selectorMatchLabelKey, deploymentName))
.withMatchLabels(Collections.singletonMap(SELECTOR_MATCH_LABEL_KEY, normalizedName))
.endSelector()
.withNewTemplate()
.withNewMetadata()
.withLabels(Map.of(this.selectorMatchLabelKey, deploymentName))
.withLabels(Collections.singletonMap(SELECTOR_MATCH_LABEL_KEY, normalizedName))
.endMetadata()
.withNewSpec()
.withContainers(new V1ContainerBuilder()
.withName(containerName)
.withImage(this.getImage(streamRecord))
.withEnv(this.getEnvVar(streamRecord))
.build())
......@@ -72,71 +87,38 @@ public class DeploymentAdminServiceImpl implements DeploymentAdminService {
}
}
private String getDeploymentName(StreamRecord streamRecord) {
return streamRecord.getKind();
}
private String getImage(StreamRecord streamRecord) {
String image = streamRecord.getData().getDatasetProperties().getExtensionProperties().getStreamDeployment().getImage();
if (Objects.isNull(image)) {
throw new StreamAdminException("Unable to deploy: ExtensionProperties.StreamDeployment.Image is null");
}
return image;
}
private List<V1EnvVar> getEnvVar(StreamRecord streamRecord) {
Map<String, String> envMap = streamRecord.getData().getDatasetProperties().getExtensionProperties().getStreamDeployment().getEnv();
if (envMap.isEmpty()) {
return Collections.emptyList();
}
return envMap.entrySet().stream().map(entry ->
new V1EnvVarBuilder()
.withName(entry.getKey())
.withValue(entry.getValue())
.build()
).collect(Collectors.toList());
}
@Override
public StreamDeploymentStatus getStreamDeploymentStatus(String id) {
Optional<V1Deployment> optional = findExistingDeployment(this.namespace, id);
return optional.isPresent() ? new StreamDeploymentStatus(
optional.get().getMetadata().getName(),
optional.get().getMetadata().getNamespace(),
optional.get().getSpec().getReplicas() > 0 ? StreamDeploymentStatus.Status.RUNNING : StreamDeploymentStatus.Status.STOPPED,
LocalDateTime.now()) : null;
}
@Override
public StreamDeploymentStatus startStreamDeployment(StreamRecord streamRecord) {
if (this.findExistingDeployment(this.namespace, this.getDeploymentName(streamRecord)).isEmpty()) {
throw new StreamAdminException("Unable to start StreamDeployment - Does not exist. ");
this.validateStreamRecord(streamRecord);
String deploymentName = this.getDeploymentName(streamRecord);
if (!this.findExistingDeployment(deploymentName).isPresent()) {
throw new StreamAdminException(String.format("Unable to find StreamDeployment for %s", deploymentName));
}
V1Deployment v1Deployment = this.patchReplicas(this.getDeploymentName(streamRecord), 1);
V1Deployment v1Deployment = this.patchReplicas(deploymentName, 1);
return new StreamDeploymentStatus(v1Deployment.getMetadata().getName(), v1Deployment.getMetadata().getNamespace(), StreamDeploymentStatus.Status.RUNNING, LocalDateTime.now());
}
@Override
public StreamDeploymentStatus stopStreamDeployment(StreamRecord streamRecord) {
if (this.findExistingDeployment(this.namespace, this.getDeploymentName(streamRecord)).isEmpty()) {
throw new StreamAdminException("Unable to stop StreamDeployment - Does not exist. ");
this.validateStreamRecord(streamRecord);
String deploymentName = this.getDeploymentName(streamRecord);
if (!this.findExistingDeployment(deploymentName).isPresent()) {
throw new StreamAdminException(String.format("Unable to find StreamDeployment for %s", deploymentName));
}
V1Deployment v1Deployment = this.patchReplicas(this.getDeploymentName(streamRecord), 0);
V1Deployment v1Deployment = this.patchReplicas(deploymentName, 0);
return new StreamDeploymentStatus(v1Deployment.getMetadata().getName(), v1Deployment.getMetadata().getNamespace(), StreamDeploymentStatus.Status.STOPPED, LocalDateTime.now());
}
private V1Deployment patchReplicas(String deploymentName, int replicas) {
String jsonPatchStr = String.format(PATCH_REPLICAS, 1);
String patchString = String.format(PATCH_REPLICAS, 1);
try {
V1Deployment v1Deployment =
PatchUtils.patch(
return PatchUtils.patch(
V1Deployment.class,
() ->
appsV1Api.patchNamespacedDeploymentCall(
deploymentName,
this.namespace,
new V1Patch(jsonPatchStr),
new V1Patch(patchString),
null,
null,
null, // field-manager is optional
......@@ -144,7 +126,6 @@ public class DeploymentAdminServiceImpl implements DeploymentAdminService {
null),
V1Patch.PATCH_FORMAT_JSON_PATCH,
appsV1Api.getApiClient());
return v1Deployment;
} catch (ApiException e) {
throw new StreamAdminException(e);
}
......@@ -152,20 +133,79 @@ public class DeploymentAdminServiceImpl implements DeploymentAdminService {
@Override
public void deleteStreamDeployment(StreamRecord streamRecord) {
this.validateStreamRecord(streamRecord);
try {
appsV1Api.deleteNamespacedDeployment(this.getDeploymentName(streamRecord), this.namespace, null, null, null, null, null, null);
String deploymentName = this.getDeploymentName(streamRecord);
appsV1Api.deleteNamespacedDeployment(deploymentName, this.namespace, null, null, null, null, null, null);
} catch (ApiException e) {
throw new StreamAdminException(e);
}
}
private Optional<V1Deployment> findExistingDeployment(String namespace, String name) {
private Optional<V1Deployment> findExistingDeployment(String deploymentName) {
try {
V1DeploymentList v1DeploymentList = appsV1Api.listNamespacedDeployment(namespace, null, null, null, null, null, null, null, null, null);
return v1DeploymentList.getItems().stream().filter(v1Deployment -> v1Deployment.getMetadata().getName().equals(name)).findFirst();
V1DeploymentList v1DeploymentList = appsV1Api.listNamespacedDeployment(this.namespace, null, null, null, null, null, null, null, null, null, null);
return v1DeploymentList.getItems().stream().filter(v1Deployment -> v1Deployment.getMetadata().getName().equals(deploymentName)).findFirst();
} catch (ApiException e) {
throw new StreamAdminException(e);
}
}
private void validateStreamRecord(@NotNull StreamRecord streamRecord) {
if (streamRecord.getId() == null) {
throw new StreamAdminException("StreamRecord.id is null");
}
if (streamRecord.getKind() == null) {
throw new StreamAdminException("StreamRecord.kind is null");
}
if (streamRecord.getData() == null) {
throw new StreamAdminException("StreamRecord.data is null");
}
if (streamRecord.getData().getDatasetProperties() == null) {
throw new StreamAdminException("StreamRecord.data.datasetProperties is null");
}
if (streamRecord.getData().getDatasetProperties().getExtensionProperties() == null) {
throw new StreamAdminException("StreamRecord.data.datasetProperties.extensionProperties is null");
}
if (streamRecord.getData().getDatasetProperties().getExtensionProperties().getStreamDeployment() == null) {
throw new StreamAdminException("StreamRecord.data.datasetProperties.extensionProperties.streamDeployment is null");
}
if (streamRecord.getData().getDatasetProperties().getExtensionProperties().getStreamDeployment().getImage() == null) {
throw new StreamAdminException("StreamRecord.data.datasetProperties.extensionProperties.streamDeployment.image is null");
}
}
private String getNormalizedName(StreamRecord streamRecord) {
return streamRecord.getId().substring(streamRecord.getId().lastIndexOf(':') + 1);
}
private String getDeploymentName(StreamRecord streamRecord) {
return String.format("%s%s", this.getNormalizedName(streamRecord), DEPLOYMENT_NAME_SUFFIX);
}
private String getContainerName(StreamRecord streamRecord) {
return String.format("%s%s", this.getNormalizedName(streamRecord), CONTAINER_NAME_SUFFIX);
}
private String getImage(StreamRecord streamRecord) {
String image = streamRecord.getData().getDatasetProperties().getExtensionProperties().getStreamDeployment().getImage();
if (Objects.isNull(image)) {
throw new StreamAdminException("Unable to deploy: ExtensionProperties.StreamDeployment.Image is null");
}
return image;
}
private List<V1EnvVar> getEnvVar(StreamRecord streamRecord) {
Map<String, String> envMap = streamRecord.getData().getDatasetProperties().getExtensionProperties().getStreamDeployment().getEnv();
if (envMap.isEmpty()) {
return Collections.emptyList();
}
return envMap.entrySet().stream().map(entry ->
new V1EnvVarBuilder()
.withName(entry.getKey())
.withValue(entry.getValue())
.build()
).collect(Collectors.toList());
}
}
......@@ -25,7 +25,6 @@ import org.opengroup.osdu.core.common.storage.IStorageService;
import org.opengroup.osdu.streaming.model.StreamRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.context.annotation.RequestScope;
......@@ -41,17 +40,20 @@ public class StreamingAdminServiceImpl implements StreamingAdminService {
private static final Logger logger = LoggerFactory.getLogger(StreamingAdminServiceImpl.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private DpsHeaders headers;
@Autowired
private IStorageFactory storageFactory;
@Autowired
private TopicAdminService topicAdminService;
private DeploymentAdminService deploymentAdminService;
public StreamingAdminServiceImpl(ObjectMapper objectMapper, DpsHeaders headers, IStorageFactory storageFactory,
TopicAdminService topicAdminService, DeploymentAdminService deploymentAdminService) {
this.objectMapper = objectMapper;
this.headers = headers;
this.storageFactory = storageFactory;
this.topicAdminService = topicAdminService;
this.deploymentAdminService = deploymentAdminService;
}
@Override
public StreamRecord getStream(String streamRecordId) {
......@@ -108,6 +110,7 @@ public class StreamingAdminServiceImpl implements StreamingAdminService {
// create topic
this.topicAdminService.createTopic(streamRecord);
this.deploymentAdminService.createStreamDeployment(streamRecord);
} catch (StorageException e) {
logger.error("Got exception: " + e.getMessage() + "\nFull HTTP Response:" + e.getHttpResponse());
} catch (JsonProcessingException e) {
......
......@@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.kafka.config.TopicBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
......@@ -31,7 +31,7 @@ public class TopicAdminServiceImpl implements TopicAdminService {
@Override
public Optional<TopicDescription> getTopic(StreamRecord streamRecord) {
try {
Map<String, KafkaFuture<TopicDescription>> kafkaFutureMap = adminClient.describeTopics(List.of(streamRecord.getKind())).values();
Map<String, KafkaFuture<TopicDescription>> kafkaFutureMap = adminClient.describeTopics(Collections.singletonList(streamRecord.getKind())).values();
return kafkaFutureMap.containsKey(streamRecord.getKind()) ? Optional.of(kafkaFutureMap.get(streamRecord.getKind()).get()) : Optional.empty();
} catch (ExecutionException e) {
if (e.getCause() instanceof UnknownTopicOrPartitionException) {
......@@ -56,7 +56,7 @@ public class TopicAdminServiceImpl implements TopicAdminService {
//.partitions(10)
//.replicas(3)
.build();
adminClient.createTopics(List.of(newTopic)).all().get();
adminClient.createTopics(Collections.singletonList(newTopic)).all().get();
return getTopic(streamRecord).get();
} catch (InterruptedException | ExecutionException e) {
throw new StreamAdminException(e);
......@@ -65,7 +65,7 @@ public class TopicAdminServiceImpl implements TopicAdminService {
@Override
public void deleteTopic(StreamRecord streamRecord) {
adminClient.deleteTopics(List.of(streamRecord.getKind()));
adminClient.deleteTopics(Collections.singletonList(streamRecord.getKind()));
}
}
......@@ -15,6 +15,7 @@ public class KubernetesAdminConfiguration {
public KubernetesAdminConfiguration() throws IOException {
ApiClient client = Config.defaultClient();
io.kubernetes.client.openapi.Configuration.setDefaultApiClient(client);
client.setDebugging(true);
}
@Bean
......
package org.opengroup.osdu.streaming.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.opengroup.osdu.streaming.model.StreamRecord;
import org.junit.jupiter.api.Test;
import org.opengroup.osdu.streaming.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
@SpringBootTest
public class DeploymentAdminServiceTest {
@Autowired
private DeploymentAdminService deploymentAdminService;
@Test
public void createStream() {
StreamRecord streamRecord = new StreamRecord();
StreamRecord updatedStreamRecord = deploymentAdminService.createStream(streamRecord);
assertThat(updatedStreamRecord).isNull();
public void getStreamDeploymentStatusNotDeployed() {
StreamRecord streamRecord = this.createRandomStreamRecord();
Optional<StreamDeploymentStatus> streamDeploymentStatus = deploymentAdminService.findStreamDeploymentStatus(streamRecord);
assertThat(streamDeploymentStatus.isPresent()).isFalse();
}
@Test
public void getStreamInfo() {
StreamRecord streamRecord = deploymentAdminService.getStreamInfo("test");
assertThat(streamRecord).isNull();
public void createStreamDeploymentAndStartStreamDeploymentAndDelete() {
StreamRecord streamRecord = null;
try {
streamRecord = this.createRandomStreamDeployment();
StreamDeploymentStatus streamDeploymentStatus = deploymentAdminService.startStreamDeployment(streamRecord);
assertThat(streamDeploymentStatus).isNotNull();
assertThat(streamDeploymentStatus.getStatus()).isEqualTo(StreamDeploymentStatus.Status.RUNNING);
} finally {
deploymentAdminService.deleteStreamDeployment(streamRecord);
}
}
@Test
public void startStream() {
StreamRecord streamRecord = new StreamRecord();
deploymentAdminService.startStream(streamRecord);
public void createStreamDeploymentAndStopStreamDeploymentAndDelete() {
StreamRecord streamRecord = null;
try {
streamRecord = this.createRandomStreamDeployment();
StreamDeploymentStatus streamDeploymentStatus = deploymentAdminService.stopStreamDeployment(streamRecord);
assertThat(streamDeploymentStatus).isNotNull();
assertThat(streamDeploymentStatus.getStatus()).isEqualTo(StreamDeploymentStatus.Status.STOPPED);
} finally {
deploymentAdminService.deleteStreamDeployment(streamRecord);
}
}
@Test
public void stopStream() {
StreamRecord streamRecord = new StreamRecord();
deploymentAdminService.stopStream(streamRecord);
private StreamRecord createRandomStreamDeployment() {
StreamRecord streamRecord = this.createRandomStreamRecord();
StreamDeploymentStatus streamDeploymentStatus = deploymentAdminService.createStreamDeployment(streamRecord);
assertThat(streamDeploymentStatus).isNotNull();
assertThat(streamDeploymentStatus.getStatus()).isEqualTo(StreamDeploymentStatus.Status.STOPPED);
return streamRecord;
}
@Test
public void deleteStream() {
private StreamRecord createRandomStreamRecord() {
StreamRecord streamRecord = new StreamRecord();
deploymentAdminService.deleteStream(streamRecord);
streamRecord.setId(this.createRandomId());
streamRecord.setKind(UUID.randomUUID().toString());
StreamDataset streamDataset = new StreamDataset();
streamDataset.setName(UUID.randomUUID().toString());
StreamDatasetDatasetProperties streamDatasetDatasetProperties = new StreamDatasetDatasetProperties();
StreamDatasetDatasetPropertiesExtensionProperties streamDatasetDatasetPropertiesExtensionProperties = new StreamDatasetDatasetPropertiesExtensionProperties();
StreamDatasetDatasetPropertiesExtensionPropertiesStreamDeployment deployment = new StreamDatasetDatasetPropertiesExtensionPropertiesStreamDeployment();
deployment.setImage("busybox");
deployment.setEnv(Collections.singletonMap("ENV1", "ENV1_VALUE"));
streamDatasetDatasetPropertiesExtensionProperties.setStreamDeployment(deployment);
streamDatasetDatasetProperties.setExtensionProperties(streamDatasetDatasetPropertiesExtensionProperties);
streamDataset.setDatasetProperties(streamDatasetDatasetProperties);
streamRecord.setData(streamDataset);
return streamRecord;
}
private String createRandomId() {
return "test--blah:" + UUID.randomUUID();
}
}
......@@ -4,7 +4,6 @@ import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.KafkaAdminClient;
import org.apache.kafka.clients.admin.TopicDescription;
import org.junit.Rule;
import org.junit.jupiter.api.Test;
import org.opengroup.osdu.streaming.model.StreamRecord;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -54,7 +53,7 @@ public class TopicAdminServiceTest {
assertThat(topicDescription).isNotNull();
topicAdminService.deleteTopic(streamRecord);
Optional<TopicDescription> optionalTopicDescription = topicAdminService.getTopic(streamRecord);
assertThat(optionalTopicDescription.isEmpty()).isTrue();
assertThat(optionalTopicDescription.isPresent()).isFalse();
}
@Test
......@@ -74,16 +73,14 @@ public class TopicAdminServiceTest {
public KafkaAdmin kafkaAdmin() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
KafkaAdmin kafkaAdmin = new KafkaAdmin(configs);
return kafkaAdmin;
return new KafkaAdmin(configs);
}
@Bean
public AdminClient adminClient() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
AdminClient adminClient = KafkaAdminClient.create(configs);
return adminClient;
return KafkaAdminClient.create(configs);
}
}
......
package org.opengroup.osdu.streaming.service;
package org.opengroup.osdu.streaming.util;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
......@@ -8,12 +8,12 @@ import io.kubernetes.client.openapi.models.V1ContainerBuilder;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1DeploymentBuilder;
import io.kubernetes.client.util.Config;
import org.opengroup.osdu.streaming.exception.StreamAdminException;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
public class KubeApiTest {
public class KubeApiTestMain {
public static void main(String[] args) throws IOException, ApiException {
ApiClient apiClient = Config.defaultClient();
......@@ -31,11 +31,11 @@ public class KubeApiTest {
.withNewSpec()
.withReplicas(0)
.withNewSelector()