From 1e15fe5da68f91aa26aeaefb4ba0df7d9cb691c2 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 19 Feb 2021 07:39:48 -0500 Subject: [PATCH] [ML] add new delete trained model aliases API (#69195) (#69221) In addition to creating and re-assigning model aliases, users should be able to delete existing and unused model aliases. --- .../delete-trained-models-aliases.asciidoc | 62 +++++++++ .../ml/df-analytics/apis/index.asciidoc | 1 + .../apis/ml-df-analytics-apis.asciidoc | 1 + .../action/DeleteTrainedModelAliasAction.java | 85 ++++++++++++ ...teTrainedModelAliasActionRequestTests.java | 31 +++++ .../ml/qa/ml-with-security/build.gradle | 2 + .../ml/integration/InferenceProcessorIT.java | 21 +++ .../xpack/ml/MachineLearning.java | 7 +- .../TransportDeleteTrainedModelAction.java | 4 +- ...ransportDeleteTrainedModelAliasAction.java | 126 ++++++++++++++++++ .../RestDeleteTrainedModelAliasAction.java | 56 ++++++++ .../xpack/security/operator/Constants.java | 1 + .../api/ml.delete_trained_model_alias.json | 34 +++++ .../rest-api-spec/test/ml/inference_crud.yml | 41 ++++++ 14 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 docs/reference/ml/df-analytics/apis/delete-trained-models-aliases.asciidoc create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasAction.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasActionRequestTests.java create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAliasAction.java create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/inference/RestDeleteTrainedModelAliasAction.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model_alias.json diff --git a/docs/reference/ml/df-analytics/apis/delete-trained-models-aliases.asciidoc b/docs/reference/ml/df-analytics/apis/delete-trained-models-aliases.asciidoc new file mode 100644 index 0000000000000..f3c8602219181 --- /dev/null +++ b/docs/reference/ml/df-analytics/apis/delete-trained-models-aliases.asciidoc @@ -0,0 +1,62 @@ +[role="xpack"] +[testenv="platinum"] +[[delete-trained-models-aliases]] += Delete Trained Model Aliases API +[subs="attributes"] +++++ +Delete Trained Model Aliases +++++ + +Deletes a trained model alias. + +beta::[] + +[[ml-delete-trained-models-aliases-request]] +== {api-request-title} + +`DELETE _ml/trained_models//model_aliases/` + + +[[ml-delete-trained-models-aliases-prereq]] +== {api-prereq-title} + +If the {es} {security-features} are enabled, you must have the following +built-in roles and privileges: + +* `machine_learning_admin` + +For more information, see <>, <>, and +{ml-docs-setup-privileges}. + +[[ml-delete-trained-models-aliases-desc]] +== {api-description-title} + +This API deletes an existing model alias that refers to a trained model. + +If the model alias is missing or refers to a model other than the one identified by +the `model_id`, this API will return an error. + +[[ml-delete-trained-models-aliases-path-params]] +== {api-path-parms-title} + +`model_id`:: +(Required, string) +The trained model ID to which the model alias refers. + +`model_alias`:: +(Required, string) +The model alias to delete. + +[[ml-delete-trained-models-aliases-example]] +== {api-examples-title} + +[[ml-delete-trained-models-aliases-example-delete]] +=== Deleting a model alias + +The following example shows how to delete a model alias for a trained model ID. + +[source,console] +-------------------------------------------------- +DELETE _ml/trained_models/flight-delay-prediction-1574775339910/model_aliases/flight_delay_model +-------------------------------------------------- +// TEST[skip:setup kibana sample data] diff --git a/docs/reference/ml/df-analytics/apis/index.asciidoc b/docs/reference/ml/df-analytics/apis/index.asciidoc index 958298f027874..dcf35454f5b3b 100644 --- a/docs/reference/ml/df-analytics/apis/index.asciidoc +++ b/docs/reference/ml/df-analytics/apis/index.asciidoc @@ -8,6 +8,7 @@ include::update-dfanalytics.asciidoc[leveloffset=+2] //DELETE include::delete-dfanalytics.asciidoc[leveloffset=+2] include::delete-trained-models.asciidoc[leveloffset=+2] +include::delete-trained-models-aliases.asciidoc[leveloffset=+2] //EVALUATE include::evaluate-dfanalytics.asciidoc[leveloffset=+2] //ESTIMATE_MEMORY_USAGE diff --git a/docs/reference/ml/df-analytics/apis/ml-df-analytics-apis.asciidoc b/docs/reference/ml/df-analytics/apis/ml-df-analytics-apis.asciidoc index 7c485f6c35f48..14e0c8012c89a 100644 --- a/docs/reference/ml/df-analytics/apis/ml-df-analytics-apis.asciidoc +++ b/docs/reference/ml/df-analytics/apis/ml-df-analytics-apis.asciidoc @@ -23,6 +23,7 @@ You can use the following APIs to perform {infer} operations. * <> * <> * <> +* <> You can deploy a trained model to make predictions in an ingest pipeline or in an aggregation. Refer to the following documentation to learn more. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasAction.java new file mode 100644 index 0000000000000..d53ab36ff10ae --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasAction.java @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ml.action; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.core.ml.inference.TrainedModelConfig; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; + +import java.io.IOException; +import java.util.Objects; + + +public class DeleteTrainedModelAliasAction extends ActionType { + + public static final DeleteTrainedModelAliasAction INSTANCE = new DeleteTrainedModelAliasAction(); + public static final String NAME = "cluster:admin/xpack/ml/inference/model_aliases/delete"; + + private DeleteTrainedModelAliasAction() { + super(NAME, AcknowledgedResponse::readFrom); + } + + public static class Request extends AcknowledgedRequest { + + public static final String MODEL_ALIAS = "model_alias"; + + private final String modelAlias; + private final String modelId; + + public Request(String modelAlias, String modelId) { + this.modelAlias = ExceptionsHelper.requireNonNull(modelAlias, MODEL_ALIAS); + this.modelId = ExceptionsHelper.requireNonNull(modelId, TrainedModelConfig.MODEL_ID); + } + + public Request(StreamInput in) throws IOException { + super(in); + this.modelAlias = in.readString(); + this.modelId = in.readString(); + } + + public String getModelAlias() { + return modelAlias; + } + + public String getModelId() { + return modelId; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(modelAlias); + out.writeString(modelId); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return Objects.equals(modelAlias, request.modelAlias) + && Objects.equals(modelId, request.modelId); + } + + @Override + public int hashCode() { + return Objects.hash(modelAlias, modelId); + } + + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasActionRequestTests.java new file mode 100644 index 0000000000000..cf45cde927c9b --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/DeleteTrainedModelAliasActionRequestTests.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.ml.action; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAliasAction.Request; + + +public class DeleteTrainedModelAliasActionRequestTests extends AbstractWireSerializingTestCase { + + @Override + protected Request createTestInstance() { + return new Request(randomAlphaOfLength(10), randomAlphaOfLength(10)); + } + + @Override + protected Writeable.Reader instanceReader() { + return Request::new; + } + + public void testCtor() { + expectThrows(Exception.class, () -> new Request(null, randomAlphaOfLength(10))); + expectThrows(Exception.class, () -> new Request(randomAlphaOfLength(10), null)); + } + +} diff --git a/x-pack/plugin/ml/qa/ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle index e26ab4bbd5792..dcaea1e2aae51 100644 --- a/x-pack/plugin/ml/qa/ml-with-security/build.gradle +++ b/x-pack/plugin/ml/qa/ml-with-security/build.gradle @@ -149,6 +149,8 @@ tasks.named("yamlRestTest").configure { 'ml/inference_crud/Test update model alias with bad alias', 'ml/inference_crud/Test update model alias where alias exists but old model id is different inference type', 'ml/inference_crud/Test update model alias where alias exists but reassign is false', + 'ml/inference_crud/Test delete model alias with missing alias', + 'ml/inference_crud/Test delete model alias where alias points to different model', 'ml/inference_processor/Test create processor with missing mandatory fields', 'ml/inference_stats_crud/Test get stats given missing trained model', 'ml/inference_stats_crud/Test get stats given expression without matches and allow_no_match is false', diff --git a/x-pack/plugin/ml/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceProcessorIT.java b/x-pack/plugin/ml/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceProcessorIT.java index 960514b6e1cc0..593017aca99e8 100644 --- a/x-pack/plugin/ml/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceProcessorIT.java +++ b/x-pack/plugin/ml/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceProcessorIT.java @@ -226,6 +226,27 @@ public void testDeleteModelWhileAliasReferencedByPipeline() throws Exception { waitForStats(); } + public void testDeleteModelAliasWhileAliasReferencedByPipeline() throws Exception { + putRegressionModel(); + putModelAlias("regression_to_delete", MODEL_ID); + createdPipelines.add("first_pipeline"); + putPipeline("regression_to_delete", "first_pipeline"); + Exception ex = expectThrows(Exception.class, + () -> client().performRequest( + new Request( + "DELETE", + "_ml/trained_models/" + MODEL_ID + "/model_aliases/regression_to_delete" + ) + )); + assertThat( + ex.getMessage(), + containsString("Cannot delete model_alias [regression_to_delete] as it is still referenced by ingest processors") + ); + infer("first_pipeline"); + deletePipeline("first_pipeline"); + waitForStats(); + } + public void testDeleteModelWhileReferencedByPipeline() throws Exception { putRegressionModel(); createdPipelines.add("first_pipeline"); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java index 468ad6b114b2e..43498fc91e7c3 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java @@ -83,6 +83,7 @@ import org.elasticsearch.xpack.core.ml.action.DeleteJobAction; import org.elasticsearch.xpack.core.ml.action.DeleteModelSnapshotAction; import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAction; +import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAliasAction; import org.elasticsearch.xpack.core.ml.action.EstimateModelMemoryAction; import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; import org.elasticsearch.xpack.core.ml.action.ExplainDataFrameAnalyticsAction; @@ -157,6 +158,7 @@ import org.elasticsearch.xpack.ml.action.TransportDeleteJobAction; import org.elasticsearch.xpack.ml.action.TransportDeleteModelSnapshotAction; import org.elasticsearch.xpack.ml.action.TransportDeleteTrainedModelAction; +import org.elasticsearch.xpack.ml.action.TransportDeleteTrainedModelAliasAction; import org.elasticsearch.xpack.ml.action.TransportEstimateModelMemoryAction; import org.elasticsearch.xpack.ml.action.TransportEvaluateDataFrameAction; import org.elasticsearch.xpack.ml.action.TransportExplainDataFrameAnalyticsAction; @@ -299,6 +301,7 @@ import org.elasticsearch.xpack.ml.rest.filter.RestPutFilterAction; import org.elasticsearch.xpack.ml.rest.filter.RestUpdateFilterAction; import org.elasticsearch.xpack.ml.rest.inference.RestDeleteTrainedModelAction; +import org.elasticsearch.xpack.ml.rest.inference.RestDeleteTrainedModelAliasAction; import org.elasticsearch.xpack.ml.rest.inference.RestGetTrainedModelsAction; import org.elasticsearch.xpack.ml.rest.inference.RestGetTrainedModelsStatsAction; import org.elasticsearch.xpack.ml.rest.inference.RestPutTrainedModelAction; @@ -932,6 +935,7 @@ public List getRestHandlers(Settings settings, RestController restC new RestPutTrainedModelAction(), new RestUpgradeJobModelSnapshotAction(), new RestPutTrainedModelAliasAction(), + new RestDeleteTrainedModelAliasAction(), // CAT Handlers new RestCatJobsAction(), new RestCatTrainedModelsAction(), @@ -1011,7 +1015,8 @@ public List getRestHandlers(Settings settings, RestController restC new ActionHandler<>(GetTrainedModelsStatsAction.INSTANCE, TransportGetTrainedModelsStatsAction.class), new ActionHandler<>(PutTrainedModelAction.INSTANCE, TransportPutTrainedModelAction.class), new ActionHandler<>(UpgradeJobModelSnapshotAction.INSTANCE, TransportUpgradeJobModelSnapshotAction.class), - new ActionHandler<>(PutTrainedModelAliasAction.INSTANCE, TransportPutTrainedModelAliasAction.class) + new ActionHandler<>(PutTrainedModelAliasAction.INSTANCE, TransportPutTrainedModelAliasAction.class), + new ActionHandler<>(DeleteTrainedModelAliasAction.INSTANCE, TransportDeleteTrainedModelAliasAction.class) ); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java index 087ddb95b6ec3..4a37a49d20e47 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAction.java @@ -75,7 +75,7 @@ protected void masterOperation(DeleteTrainedModelAction.Request request, ActionListener listener) { String id = request.getId(); IngestMetadata currentIngestMetadata = state.metadata().custom(IngestMetadata.TYPE); - Set referencedModels = getReferencedModelKeys(currentIngestMetadata); + Set referencedModels = getReferencedModelKeys(currentIngestMetadata, ingestService); if (referencedModels.contains(id)) { listener.onFailure(new ElasticsearchStatusException("Cannot delete model [{}] as it is still referenced by ingest processors", @@ -140,7 +140,7 @@ public ClusterState execute(final ClusterState currentState) { }); } - private Set getReferencedModelKeys(IngestMetadata ingestMetadata) { + static Set getReferencedModelKeys(IngestMetadata ingestMetadata, IngestService ingestService) { Set allReferencedModelKeys = new HashSet<>(); if (ingestMetadata == null) { return allReferencedModelKeys; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAliasAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAliasAction.java new file mode 100644 index 0000000000000..adcd519951642 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteTrainedModelAliasAction.java @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ml.action; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.action.support.master.AcknowledgedTransportMasterNodeAction; +import org.elasticsearch.cluster.AckedClusterStateUpdateTask; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.ingest.IngestMetadata; +import org.elasticsearch.ingest.IngestService; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAliasAction; +import org.elasticsearch.xpack.core.ml.inference.ModelAliasMetadata; +import org.elasticsearch.xpack.ml.notifications.InferenceAuditor; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.xpack.ml.action.TransportDeleteTrainedModelAction.getReferencedModelKeys; + +public class TransportDeleteTrainedModelAliasAction extends AcknowledgedTransportMasterNodeAction { + + private static final Logger logger = LogManager.getLogger(TransportDeleteTrainedModelAliasAction.class); + + private final InferenceAuditor auditor; + private final IngestService ingestService; + + @Inject + public TransportDeleteTrainedModelAliasAction( + TransportService transportService, + ClusterService clusterService, + ThreadPool threadPool, + ActionFilters actionFilters, + InferenceAuditor auditor, + IngestService ingestService, + IndexNameExpressionResolver indexNameExpressionResolver) { + super( + DeleteTrainedModelAliasAction.NAME, + transportService, + clusterService, + threadPool, + actionFilters, + DeleteTrainedModelAliasAction.Request::new, + indexNameExpressionResolver, + ThreadPool.Names.SAME + ); + this.auditor = auditor; + this.ingestService = ingestService; + } + + @Override + protected void masterOperation( + DeleteTrainedModelAliasAction.Request request, + ClusterState state, + ActionListener listener + ) throws Exception { + clusterService.submitStateUpdateTask("delete-model-alias", new AckedClusterStateUpdateTask(request, listener) { + @Override + public ClusterState execute(final ClusterState currentState) { + return deleteModelAlias(currentState, ingestService, auditor, request); + } + }); + } + + static ClusterState deleteModelAlias(final ClusterState currentState, + final IngestService ingestService, + final InferenceAuditor inferenceAuditor, + final DeleteTrainedModelAliasAction.Request request) { + final ModelAliasMetadata currentMetadata = ModelAliasMetadata.fromState(currentState); + final String referencedModel = currentMetadata.getModelId(request.getModelAlias()); + if (referencedModel == null) { + throw new ElasticsearchStatusException("model_alias [{}] could not be found", RestStatus.NOT_FOUND, request.getModelAlias()); + } + if (referencedModel.equals(request.getModelId()) == false) { + throw new ElasticsearchStatusException( + "model_alias [{}] does not refer to provided model_id [{}]", + RestStatus.CONFLICT, + request.getModelAlias(), + request.getModelId() + ); + } + IngestMetadata currentIngestMetadata = currentState.metadata().custom(IngestMetadata.TYPE); + Set referencedModels = getReferencedModelKeys(currentIngestMetadata, ingestService); + if (referencedModels.contains(request.getModelAlias())) { + throw new ElasticsearchStatusException( + "Cannot delete model_alias [{}] as it is still referenced by ingest processors", + RestStatus.CONFLICT, + request.getModelAlias() + ); + } + final ClusterState.Builder builder = ClusterState.builder(currentState); + final Map newMetadata = new HashMap<>(currentMetadata.modelAliases()); + logger.info("deleting model_alias [{}] that refers to model [{}]", request.getModelAlias(), request.getModelId()); + inferenceAuditor.info(referencedModel, String.format(Locale.ROOT, "deleting model_alias [%s]", request.getModelAlias())); + + newMetadata.remove(request.getModelAlias()); + final ModelAliasMetadata modelAliasMetadata = new ModelAliasMetadata(newMetadata); + builder.metadata(Metadata.builder(currentState.getMetadata()).putCustom(ModelAliasMetadata.NAME, modelAliasMetadata).build()); + return builder.build(); + } + + @Override + protected ClusterBlockException checkBlock(DeleteTrainedModelAliasAction.Request request, ClusterState state) { + return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); + } +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/inference/RestDeleteTrainedModelAliasAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/inference/RestDeleteTrainedModelAliasAction.java new file mode 100644 index 0000000000000..c66ba0e769fcb --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/inference/RestDeleteTrainedModelAliasAction.java @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.ml.rest.inference; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAliasAction; +import org.elasticsearch.xpack.core.ml.inference.TrainedModelConfig; +import org.elasticsearch.xpack.ml.MachineLearning; + +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.elasticsearch.rest.RestRequest.Method.DELETE; + +public class RestDeleteTrainedModelAliasAction extends BaseRestHandler { + + @Override + public List routes() { + return singletonList( + new Route( + DELETE, + MachineLearning.BASE_PATH + + "trained_models/{" + + TrainedModelConfig.MODEL_ID.getPreferredName() + + "}/model_aliases/{" + + DeleteTrainedModelAliasAction.Request.MODEL_ALIAS + + "}" + + ) + ); + } + + @Override + public String getName() { + return "ml_delete_trained_model_alias_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { + final String modelId = restRequest.param(TrainedModelConfig.MODEL_ID.getPreferredName()); + final String modelAlias = restRequest.param(DeleteTrainedModelAliasAction.Request.MODEL_ALIAS); + return channel -> client.execute( + DeleteTrainedModelAliasAction.INSTANCE, + new DeleteTrainedModelAliasAction.Request(modelAlias, modelId), + new RestToXContentListener<>(channel) + ); + } +} diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index 8d828bd7fd592..525d1f0c3ce89 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -135,6 +135,7 @@ public class Constants { "cluster:admin/xpack/ml/inference/delete", "cluster:admin/xpack/ml/inference/put", "cluster:admin/xpack/ml/inference/model_aliases/put", + "cluster:admin/xpack/ml/inference/model_aliases/delete", "cluster:admin/xpack/ml/job/close", "cluster:admin/xpack/ml/job/data/post", "cluster:admin/xpack/ml/job/delete", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model_alias.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model_alias.json new file mode 100644 index 0000000000000..1e51ceea1aee1 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ml.delete_trained_model_alias.json @@ -0,0 +1,34 @@ +{ + "ml.delete_trained_model_alias":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-trained-models-aliases.html", + "description":"Deletes a model alias that refers to the trained model" + }, + "stability":"beta", + "visibility":"public", + "headers":{ + "accept": [ "application/json"], + "content_type": ["application/json"] + }, + "url":{ + "paths":[ + { + "path":"/_ml/trained_models/{model_id}/model_aliases/{model_alias}", + "methods":[ + "DELETE" + ], + "parts":{ + "model_alias":{ + "type":"string", + "description":"The trained model alias to delete" + }, + "model_id": { + "type": "string", + "description": "The trained model where the model alias is assigned" + } + } + } + ] + } + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/inference_crud.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/inference_crud.yml index 0994bdf33319c..dafe223bfd60d 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/inference_crud.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/inference_crud.yml @@ -913,3 +913,44 @@ setup: model_alias: "regression-model" model_id: "a-regression-model-1" reassign: false +--- +"Test delete model alias": + - do: + ml.put_trained_model_alias: + model_alias: "regression-model" + model_id: "a-regression-model-0" + - do: + ml.get_trained_models: + model_id: "regression-model" + + - match: { count: 1 } + - length: { trained_model_configs: 1 } + - match: { trained_model_configs.0.model_id: "a-regression-model-0" } + + - do: + ml.delete_trained_model_alias: + model_alias: "regression-model" + model_id: "a-regression-model-0" + + - do: + catch: missing + ml.get_trained_models: + model_id: "regression-model" +--- +"Test delete model alias with missing alias": + - do: + catch: missing + ml.delete_trained_model_alias: + model_alias: "regression-model" + model_id: "a-regression-model-0" +--- +"Test delete model alias where alias points to different model": + - do: + ml.put_trained_model_alias: + model_alias: "regression-model" + model_id: "a-regression-model-1" + - do: + catch: conflict + ml.delete_trained_model_alias: + model_alias: "regression-model" + model_id: "a-regression-model-0"