Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Key Vault Keys] LRO refactoring #11717

Merged
merged 6 commits into from
Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 45 additions & 215 deletions sdk/keyvault/keyvault-keys/src/index.ts

Large diffs are not rendered by default.

28 changes: 0 additions & 28 deletions sdk/keyvault/keyvault-keys/src/keysModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,6 @@ export type KeyOperation =
*/
export type KeyType = "EC" | "EC-HSM" | "RSA" | "RSA-HSM" | "oct" | "oct-HSM";

/**
* @internal
* @ignore
* An interface representing the KeyClient. For internal use.
*/
export interface KeyClientInterface {
/**
* Recovers the deleted key in the specified vault. This operation can only be performed on a
* soft-delete enabled vault.
*/
recoverDeletedKey(name: string, options?: RecoverDeletedKeyOptions): Promise<KeyVaultKey>;
/**
* The get method gets a specified key and is applicable to any key stored in Azure Key Vault.
* This operation requires the keys/get permission.
*/
getKey(name: string, options?: GetKeyOptions): Promise<KeyVaultKey>;
/**
* The delete operation applies to any key stored in Azure Key Vault. Individual versions
* of a key can not be deleted, only all versions of a given key at once.
*/
deleteKey(name: string, options?: DeleteKeyOptions): Promise<DeletedKey>;
/**
* The getDeletedKey method returns the specified deleted key along with its properties.
* This operation requires the keys/get permission.
*/
getDeletedKey(name: string, options?: GetDeletedKeyOptions): Promise<DeletedKey>;
}

/**
* The latest supported Key Vault service API version
*/
Expand Down
191 changes: 104 additions & 87 deletions sdk/keyvault/keyvault-keys/src/lro/delete/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,110 +2,127 @@
// Licensed under the MIT license.

import { AbortSignalLike } from "@azure/abort-controller";
import { PollOperationState, PollOperation } from "@azure/core-lro";
import { RequestOptionsBase } from "@azure/core-http";
import { DeletedKey, KeyClientInterface } from "../../keysModels";
import { operationOptionsToRequestOptionsBase, RequestOptionsBase } from "@azure/core-http";
import { KeyVaultClient } from "../../generated/keyVaultClient";
import { DeleteKeyResponse, GetDeletedKeyResponse } from "../../generated/models";
import { DeletedKey, DeleteKeyOptions, GetDeletedKeyOptions } from "../../keysModels";
import { createSpan, setParentSpan } from "../../tracing";
import { getKeyFromKeyBundle } from "../../transformations";
import { KeyVaultKeyPollOperation, KeyVaultKeyPollOperationState } from "../keyVaultKeyPoller";

/**
* An interface representing the state of a delete key's poll operation
*/
export interface DeleteKeyPollOperationState extends PollOperationState<DeletedKey> {
/**
* The name of the key.
*/
name: string;
export interface DeleteKeyPollOperationState extends KeyVaultKeyPollOperationState<DeletedKey> {}

export class DeleteKeyPollOperation extends KeyVaultKeyPollOperation<
DeleteKeyPollOperationState,
DeletedKey
> {
constructor(
public state: DeleteKeyPollOperationState,
private vaultUrl: string,
private client: KeyVaultClient,
private requestOptions: RequestOptionsBase = {}
) {
super(state, "Canceling the deletion of a key is not supported.");
}

/**
* Options for the core-http requests.
* Sends a delete request for the given Key Vault Key's name to the Key Vault service.
* Since the Key Vault Key won't be immediately deleted, we have {@link beginDeleteKey}.
* @param {string} name The name of the Key Vault Key.
* @param {DeleteKeyOptions} [options] Optional parameters for the underlying HTTP request.
*/
requestOptions?: RequestOptionsBase;
private async deleteKey(name: string, options: DeleteKeyOptions = {}): Promise<DeletedKey> {
const requestOptions = operationOptionsToRequestOptionsBase(options);
const span = createSpan("deleteKey", requestOptions);

let response: DeleteKeyResponse;
try {
response = await this.client.deleteKey(
this.vaultUrl,
name,
setParentSpan(span, requestOptions)
);
} finally {
span.end();
}

return getKeyFromKeyBundle(response);
}

/**
* An interface representing a KeyClient. For internal use.
* The getDeletedKey method returns the specified deleted key along with its properties.
* This operation requires the keys/get permission.
* @summary Gets the specified deleted key.
* @param {string} name The name of the key.
* @param {GetDeletedKeyOptions} [options] The optional parameters.
*/
client: KeyClientInterface;
}
private async getDeletedKey(
name: string,
options: GetDeletedKeyOptions = {}
): Promise<DeletedKey> {
const responseOptions = operationOptionsToRequestOptionsBase(options);
const span = createSpan("getDeletedKey", responseOptions);

/**
* An interface representing a delete key's poll operation
*/
export interface DeleteKeyPollOperation
extends PollOperation<DeleteKeyPollOperationState, DeletedKey> {}

/**
* @summary Reaches to the service and updates the delete key's poll operation.
* @param [options] The optional parameters, which are an abortSignal from @azure/abort-controller and a function that triggers the poller's onProgress function.
*/
async function update(
this: DeleteKeyPollOperation,
options: {
abortSignal?: AbortSignalLike;
fireProgress?: (state: DeleteKeyPollOperationState) => void;
} = {}
): Promise<DeleteKeyPollOperation> {
const state = this.state;
const { name, client } = state;
let response: GetDeletedKeyResponse;
try {
response = await this.client.getDeletedKey(
this.vaultUrl,
name,
setParentSpan(span, responseOptions)
);
} finally {
span.end();
}

const requestOptions = state.requestOptions || {};
if (options.abortSignal) {
requestOptions.abortSignal = options.abortSignal;
return getKeyFromKeyBundle(response);
}

if (!state.isStarted) {
const deletedKey = await client.deleteKey(name, requestOptions);
state.isStarted = true;
state.result = deletedKey;
if (!deletedKey.properties.recoveryId) {
state.isCompleted = true;
/**
* @summary Reaches to the service and updates the delete key's poll operation.
* @param [options] The optional parameters, which are an abortSignal from @azure/abort-controller and a function that triggers the poller's onProgress function.
*/
public async update(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern of managing the client seems pretty nice, and definitely less leaky than before. It feels like a good balance between ruthless abstraction and the levels of duplication that we had previously, but it doesn't seem like it's much less verbose than what we had before.

It still feels bizarre to me that we have this state machine model where update can return a different operation, but I can't think of anywhere that we use it. That API suggests to me that returning a different kind of operation should be the way that we make the pollers progress in state, but instead we just have monolithic update functions that handle everything and then return this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand how this might be weird! I wonder if it would make sense to open an issue to have a discussion over time of how we could improve this in core-lro 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we should consider if we want this to be stateless or not (no strong feelings from me) and either we lean fully into stateless (get rid of this) or we lean fully away from it (don't bother returning this, because caller already has a copy of us)

options: {
abortSignal?: AbortSignalLike;
fireProgress?: (state: DeleteKeyPollOperationState) => void;
} = {}
): Promise<DeleteKeyPollOperation> {
const state = this.state;
const { name } = state;

if (options.abortSignal) {
this.requestOptions.abortSignal = options.abortSignal;
}
}

if (!state.isCompleted) {
try {
state.result = await client.getDeletedKey(name, { requestOptions });
state.isCompleted = true;
} catch (error) {
if (error.statusCode === 403) {
// At this point, the resource exists but the user doesn't have access to it.
state.isCompleted = true;
} else if (error.statusCode !== 404) {
state.error = error;
if (!state.isStarted) {
const deletedKey = await this.deleteKey(name, this.requestOptions);
state.isStarted = true;
state.result = deletedKey;
if (!deletedKey.properties.recoveryId) {
state.isCompleted = true;
}
}
}

return makeDeleteKeyPollOperation(state);
}

/**
* @summary Reaches to the service and cancels the key's operation, also updating the key's poll operation
* @param [options] The optional parameters, which is only an abortSignal from @azure/abort-controller
*/
async function cancel(this: DeleteKeyPollOperation): Promise<DeleteKeyPollOperation> {
throw new Error("Canceling the deletion of a key is not supported.");
}

/**
* @summary Serializes the create key's poll operation
*/
function toString(this: DeleteKeyPollOperation): string {
return JSON.stringify({
state: this.state
});
}
if (!state.isCompleted) {
try {
state.result = await this.getDeletedKey(name, {
requestOptions: this.requestOptions
});
state.isCompleted = true;
} catch (error) {
if (error.statusCode === 403) {
// At this point, the resource exists but the user doesn't have access to it.
state.isCompleted = true;
} else if (error.statusCode !== 404) {
state.error = error;
state.isCompleted = true;
}
}
}

/**
* @summary Builds a create key's poll operation
* @param [state] A poll operation's state, in case the new one is intended to follow up where the previous one was left.
*/
export function makeDeleteKeyPollOperation(
state: DeleteKeyPollOperationState
): DeleteKeyPollOperation {
return {
state: {
...state
},
update,
cancel,
toString
};
return this;
}
}
50 changes: 15 additions & 35 deletions sdk/keyvault/keyvault-keys/src/lro/delete/poller.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { delay, RequestOptionsBase } from "@azure/core-http";
import { Poller } from "@azure/core-lro";
import { DeleteKeyPollOperationState, makeDeleteKeyPollOperation } from "./operation";
import { DeletedKey, KeyClientInterface } from "../../keysModels";

export interface DeleteKeyPollerOptions {
client: KeyClientInterface;
name: string;
requestOptions?: RequestOptionsBase;
intervalInMs?: number;
resumeFrom?: string;
}
import { DeleteKeyPollOperation, DeleteKeyPollOperationState } from "./operation";
import { DeletedKey } from "../../keysModels";
import { KeyVaultKeyPoller, KeyVaultKeyPollerOptions } from "../keyVaultKeyPoller";

/**
* Class that deletes a poller that waits until a key finishes being deleted
*/
export class DeleteKeyPoller extends Poller<DeleteKeyPollOperationState, DeletedKey> {
/**
* Defines how much time the poller is going to wait before making a new request to the service.
* @memberof DeleteKeyPoller
*/
public intervalInMs: number;

constructor(options: DeleteKeyPollerOptions) {
const { client, name, requestOptions, intervalInMs = 2000, resumeFrom } = options;
export class DeleteKeyPoller extends KeyVaultKeyPoller<DeleteKeyPollOperationState, DeletedKey> {
constructor(options: KeyVaultKeyPollerOptions) {
const { vaultUrl, client, name, requestOptions, intervalInMs = 2000, resumeFrom } = options;

let state: DeleteKeyPollOperationState | undefined;

if (resumeFrom) {
state = JSON.parse(resumeFrom).state;
}

const operation = makeDeleteKeyPollOperation({
...state,
name,
requestOptions,
client
});
const operation = new DeleteKeyPollOperation(
{
...state,
name
},
vaultUrl,
client,
requestOptions
);

super(operation);

this.intervalInMs = intervalInMs;
}

/**
* The method used by the poller to wait before attempting to update its operation.
* @memberof DeleteKeyPoller
*/
async delay(): Promise<void> {
return delay(this.intervalInMs);
}
}
Loading