diff --git a/enterprise/cloud.oracle/nbproject/project.xml b/enterprise/cloud.oracle/nbproject/project.xml index 797fc5dcdb62..ba7bece5da58 100644 --- a/enterprise/cloud.oracle/nbproject/project.xml +++ b/enterprise/cloud.oracle/nbproject/project.xml @@ -151,6 +151,14 @@ 2.29 + + org.openide.actions + + + + 6.65 + + org.openide.awt diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/NotificationUtils.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/NotificationUtils.java new file mode 100644 index 000000000000..52281b294b0e --- /dev/null +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/NotificationUtils.java @@ -0,0 +1,44 @@ +/* + * 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.netbeans.modules.cloud.oracle; + +import org.openide.DialogDisplayer; +import org.openide.NotifyDescriptor; + +/** + * + * @author Dusan Petrovic + */ +public class NotificationUtils { + + public static boolean confirmAction(String message) { + NotifyDescriptor.Confirmation msg = new NotifyDescriptor.Confirmation(message, NotifyDescriptor.YES_NO_OPTION); + Object choice = DialogDisplayer.getDefault().notify(msg); + return choice == NotifyDescriptor.YES_OPTION || choice == NotifyDescriptor.OK_OPTION; + } + + public static void showErrorMessage(String message) { + DialogDisplayer.getDefault() + .notifyLater(new NotifyDescriptor.Message(message, NotifyDescriptor.ERROR_MESSAGE)); + } + + public static void showMessage(String message) { + DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message(message)); + } +} diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/OCINode.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/OCINode.java index bfc515d76fe3..52aa6cda255a 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/OCINode.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/OCINode.java @@ -30,6 +30,7 @@ import org.openide.nodes.Node; import org.openide.util.ContextAwareAction; import org.openide.util.Lookup; +import org.openide.util.RequestProcessor; import org.openide.util.Utilities; import org.openide.util.lookup.Lookups; @@ -111,15 +112,21 @@ public static final List actionsForPath(String path, Lookup lk } public void refresh() { - if (factory != null) { - factory.refreshKeys(); - } - update(item); + RequestProcessor.getDefault().post(() -> { + if (factory != null) { + factory.refreshKeys(); + } + update(item); + }); } public void update(OCIItem item) { } + + public OCIItem getItem() { + return this.item; + } @Override public Node.Handle getHandle() { diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/developer/ContainerTagNode.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/developer/ContainerTagNode.java index 5869f53d5f8f..51a835d065a0 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/developer/ContainerTagNode.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/developer/ContainerTagNode.java @@ -19,21 +19,42 @@ package org.netbeans.modules.cloud.oracle.developer; import com.oracle.bmc.artifacts.ArtifactsClient; +import com.oracle.bmc.artifacts.requests.DeleteContainerImageRequest; import com.oracle.bmc.artifacts.requests.ListContainerImagesRequest; +import com.oracle.bmc.artifacts.responses.DeleteContainerImageResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Collectors; +import javax.swing.Action; import org.netbeans.modules.cloud.oracle.ChildrenProvider; import org.netbeans.modules.cloud.oracle.NodeProvider; +import static org.netbeans.modules.cloud.oracle.NotificationUtils.confirmAction; +import static org.netbeans.modules.cloud.oracle.NotificationUtils.showErrorMessage; +import static org.netbeans.modules.cloud.oracle.NotificationUtils.showMessage; +import org.netbeans.modules.cloud.oracle.OCIManager; import org.netbeans.modules.cloud.oracle.OCINode; import org.netbeans.modules.cloud.oracle.items.OCID; +import org.openide.actions.DeleteAction; import org.openide.nodes.Children; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.actions.SystemAction; /** * * @author Jan Horvath */ @NbBundle.Messages({ - "ContainerTagDesc=Pull URL: {0}\nVersion: {1}\nDigest: {2}" + "ContainerTagDesc=Pull URL: {0}\nVersion: {1}\nDigest: {2}", + "# {0} - [OCIItem name]", + "MSG_ConfirmDeleteAction=Are you sure that you want to delete {0}.", + "# {0} - [OCIItem name]", + "MSG_DeleteActionFailed=Failed to delete {0}.", + "# {0} - [OCIItem name]", + "MSG_DeleteActionSuccess=Successfully deleted {0}." }) public class ContainerTagNode extends OCINode { private static final String CONTAINER_TAG_ICON = "org/netbeans/modules/cloud/oracle/resources/containertag.svg"; // NOI18N @@ -49,6 +70,51 @@ public ContainerTagNode(ContainerTagItem tag) { public static NodeProvider createNode() { return ContainerTagNode::new; } + + @Override + public Action[] getActions(boolean context) { + Action[] actions = super.getActions(context); + List actionList = new ArrayList<>(Arrays.asList(actions)); + actionList.add(SystemAction.get(DeleteAction.class)); + return actionList.toArray(Action[]::new); + } + + @Override + public boolean canDestroy() { + return true; + } + + @Override + public void destroy() throws IOException { + RequestProcessor.getDefault().post(() -> { + if (!confirmAction(Bundle.MSG_ConfirmDeleteAction(this.getName()))) { + return; + } + ArtifactsClient client = OCIManager.getDefault().getActiveSession().newClient(ArtifactsClient.class); + DeleteContainerImageRequest request = DeleteContainerImageRequest.builder() + .imageId(this.getItem().getKey().getValue()) + .build(); + DeleteContainerImageResponse response; + + try { + response = client.deleteContainerImage(request); + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + showErrorMessage(Bundle.MSG_DeleteActionFailed(this.getName())); + return; + } + if (response.get__httpStatusCode__() != 204) { + showErrorMessage(Bundle.MSG_DeleteActionFailed(this.getName())); + return; + } + + if (this.getParentNode() instanceof OCINode) { + ((OCINode)this.getParentNode()).refresh(); + } + showMessage(Bundle.MSG_DeleteActionSuccess(this.getName())); + }); + + } /** * Retrieves list of Vaults belonging to a given Compartment. diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretItem.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretItem.java index a4643bb4e510..9338ab4f4dfb 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretItem.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretItem.java @@ -18,6 +18,7 @@ */ package org.netbeans.modules.cloud.oracle.vault; +import java.util.Date; import org.netbeans.modules.cloud.oracle.items.OCID; import org.netbeans.modules.cloud.oracle.items.OCIItem; @@ -26,18 +27,40 @@ * @author Jan Horvath */ public class SecretItem extends OCIItem { + + private String lifecycleState; + private Date deletionTime; - public SecretItem(OCID id, String compartmentId, String name) { + public SecretItem(OCID id, String compartmentId, String name, String lifecycleState, Date deletionTime) { super(id, compartmentId, name); + this.lifecycleState = lifecycleState; + this.deletionTime = deletionTime; } - public SecretItem() { + public SecretItem() { super(); + this.lifecycleState = null; + this.deletionTime = null; } - + @Override public int maxInProject() { return Integer.MAX_VALUE; } + public Date getDeletionTime() { + return this.deletionTime; + } + + void setDeletionTime(Date deletionTime) { + this.deletionTime = deletionTime; + } + + public String getLifecycleState() { + return this.lifecycleState; + } + + void setLifecycleState(String lifecycleState) { + this.lifecycleState = lifecycleState; + } } diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretNode.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretNode.java index f597e27a805f..002a0ae096bb 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretNode.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/vault/SecretNode.java @@ -18,36 +18,172 @@ */ package org.netbeans.modules.cloud.oracle.vault; -import com.oracle.bmc.keymanagement.KmsVaultClient; import com.oracle.bmc.vault.VaultsClient; +import com.oracle.bmc.vault.model.ScheduleSecretDeletionDetails; +import com.oracle.bmc.vault.model.Secret; +import com.oracle.bmc.vault.model.SecretSummary.LifecycleState; +import com.oracle.bmc.vault.requests.GetSecretRequest; import com.oracle.bmc.vault.requests.ListSecretsRequest; +import com.oracle.bmc.vault.requests.ScheduleSecretDeletionRequest; +import com.oracle.bmc.vault.responses.GetSecretResponse; +import com.oracle.bmc.vault.responses.ScheduleSecretDeletionResponse; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; import java.util.stream.Collectors; +import javax.swing.Action; import org.netbeans.modules.cloud.oracle.ChildrenProvider; import org.netbeans.modules.cloud.oracle.NodeProvider; -import static org.netbeans.modules.cloud.oracle.OCIManager.getDefault; +import static org.netbeans.modules.cloud.oracle.NotificationUtils.confirmAction; +import static org.netbeans.modules.cloud.oracle.NotificationUtils.showErrorMessage; +import static org.netbeans.modules.cloud.oracle.NotificationUtils.showMessage; +import org.netbeans.modules.cloud.oracle.OCIManager; import org.netbeans.modules.cloud.oracle.OCINode; import org.netbeans.modules.cloud.oracle.items.OCID; +import org.netbeans.modules.cloud.oracle.items.OCIItem; +import org.openide.actions.DeleteAction; import org.openide.nodes.Children; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.actions.SystemAction; /** * * @author Jan Horvath */ +@NbBundle.Messages({ + "SecretNodeDesc=Valut Secret: {0}\nLifecycle State: {1}", + "SecretNodeDeletingDesc=Valut Secret: {0}\nLifecycle State: {1}\nDeletion time: {2}", + "# {0} - [OCIItem name]", + "MSG_ConfirmDeleteAction=Are you sure that you want to schedule deletion of {0}", + "# {0} - [OCIItem name]", + "MSG_DeleteActionFailed=Failed to schedule deletion of {0}.", + "# {0} - [OCIItem name]", "# {1} - [Scheduled deletion time]", + "MSG_DeleteActionSuccess=Successfully scheduled deletion of {0} at {1}." +}) public class SecretNode extends OCINode { private static final String SECRET_ICON = "org/netbeans/modules/cloud/oracle/resources/secret.svg"; // NOI18N + private static final SimpleDateFormat DELETION_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public SecretNode(SecretItem vault) { super(vault, Children.LEAF); setName(vault.getName()); setDisplayName(vault.getName()); setIconBaseWithExtension(SECRET_ICON); - setShortDescription(vault.getDescription()); + setShortDescription( + createShortDescription( + vault.getLifecycleState(), + vault.getDeletionTime())); + } + + private String createShortDescription(String state, Date deletionTime) { + if (deletionTime != null) { + return Bundle.SecretNodeDeletingDesc(this.getItem().getName(), state, formatDateTime(deletionTime)); + } + return Bundle.SecretNodeDesc(this.getItem().getName(), state); + } + + private String formatDateTime(Date deletionTime) { + return DELETION_TIME_FORMAT.format(deletionTime); } public static NodeProvider createNode() { return SecretNode::new; } + + @Override + public void update(OCIItem item) { + SecretItem orig = (SecretItem) item; + VaultsClient client = OCIManager.getDefault().getActiveProfile().newClient(VaultsClient.class); + GetSecretRequest request = GetSecretRequest.builder() + .secretId(orig.getKey().getValue()) + .build(); + + GetSecretResponse response = client.getSecret(request); + Secret secret = response.getSecret(); + orig.setLifecycleState(secret.getLifecycleState().getValue()); + orig.setDeletionTime(secret.getTimeOfDeletion()); + setShortDescription( + createShortDescription( + orig.getLifecycleState(), + orig.getDeletionTime())); + } + + @Override + public Action[] getActions(boolean context) { + Action[] actions = super.getActions(context); + List actionList = new ArrayList<>(Arrays.asList(actions)); + actionList.add(SystemAction.get(DeleteAction.class)); + return actionList.toArray(Action[]::new); + } + + @Override + public boolean canDestroy() { + return ((SecretItem)this.getItem()).getLifecycleState().equals(LifecycleState.Active.getValue()); + } + + @Override + public void destroy() throws IOException { + RequestProcessor.getDefault().post(() -> { + if (!confirmAction(Bundle.MSG_ConfirmDeleteAction(this.getName()))) { + return; + } + + VaultsClient client = OCIManager.getDefault().getActiveSession().newClient(VaultsClient.class); + Date deletionTime = getDeletionTime(); + ScheduleSecretDeletionRequest request = buildScheduleDeletionRequest(deletionTime); + ScheduleSecretDeletionResponse response; + + try { + response = client.scheduleSecretDeletion(request); + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + showErrorMessage(Bundle.MSG_DeleteActionFailed(this.getName())); + return; + } + + if (response.get__httpStatusCode__() != 200) { + showErrorMessage(Bundle.MSG_DeleteActionFailed(this.getName())); + return; + } + + updateToPendingState(deletionTime); + showMessage(Bundle.MSG_DeleteActionSuccess( + this.getName(), + this.formatDateTime(deletionTime))); + }); + } + + private ScheduleSecretDeletionRequest buildScheduleDeletionRequest(Date deletionTime) { + ScheduleSecretDeletionDetails scheduleSecretDeletionDetails = ScheduleSecretDeletionDetails.builder() + .timeOfDeletion(deletionTime) + .build(); + return ScheduleSecretDeletionRequest.builder() + .secretId(this.getItem().getKey().getValue()) + .scheduleSecretDeletionDetails(scheduleSecretDeletionDetails) + .build(); + } + + private void updateToPendingState(Date deletionTime) { + ((SecretItem) this.getItem()).setLifecycleState(LifecycleState.PendingDeletion.getValue()); + setShortDescription( + createShortDescription( + LifecycleState.PendingDeletion.getValue(), + deletionTime)); + } + + private Date getDeletionTime() { + LocalDateTime tomorrow = LocalDateTime.now().plusDays(1).plusHours(1); + return Date.from(tomorrow.atZone(ZoneId.systemDefault()).toInstant()); + } + /** * Retrieves list of Secrets belonging to a given Vault. * @@ -69,7 +205,9 @@ public static ChildrenProvider.SessionAware getSecrets() .map(d -> new SecretItem( OCID.of(d.getId(), "Vault/Secret"), //NOI18N d.getCompartmentId(), - d.getSecretName()) + d.getSecretName(), + d.getLifecycleState().getValue(), + d.getTimeOfDeletion()) ) .collect(Collectors.toList()); }; diff --git a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspAssetsDecorationProvider.java b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspAssetsDecorationProvider.java index 9e2160d79da8..147b573f328a 100644 --- a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspAssetsDecorationProvider.java +++ b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspAssetsDecorationProvider.java @@ -27,6 +27,7 @@ import org.netbeans.modules.cloud.oracle.developer.ContainerRepositoryItem; import org.netbeans.modules.cloud.oracle.developer.ContainerTagItem; import org.netbeans.modules.cloud.oracle.items.OCIItem; +import org.netbeans.modules.cloud.oracle.vault.SecretItem; import org.netbeans.modules.java.lsp.server.explorer.NodeLookupContextValues; import org.netbeans.modules.java.lsp.server.explorer.api.TreeDataListener; import org.netbeans.modules.java.lsp.server.explorer.api.TreeDataProvider; @@ -49,6 +50,7 @@ public class LspAssetsDecorationProvider implements TreeDataProvider.Factory { public static final String CTXVALUE_PREFIX_IMAGE_URL = "imageUrl:"; // NOI18N public static final String CTXVALUE_PREFIX_IMAGE_COUNT = "imageCount:"; // NOI18N public static final String CTXVALUE_PREFIX_REPOSITORY_PUBLIC = "repositoryPublic:"; // NOI18N + public static final String CTXVALUE_PREFIX_SECRET_LIFECYCLE_STATE = "lifecycleState:"; // NOI18N @Override public synchronized TreeDataProvider createProvider(String treeId) { @@ -101,6 +103,11 @@ public TreeItemData createDecorations(Node n, boolean expanded) { d.addContextValues(CTXVALUE_CAP_REFERENCE_NAME); set = true; } + + if (item instanceof SecretItem) { + d.addContextValues(CTXVALUE_PREFIX_SECRET_LIFECYCLE_STATE + ((SecretItem)item).getLifecycleState()); + set = true; + } return set ? d : null; } diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json index b30768a2bf1f..f68813bdfba6 100644 --- a/java/java.lsp.server/vscode/package.json +++ b/java/java.lsp.server/vscode/package.json @@ -833,6 +833,10 @@ "command": "nbls.cloud.container.docker", "when": "false" }, + { + "command": "nbls:Edit:org.openide.actions.DeleteAction", + "when": "false" + }, { "command": "nbls:Database:netbeans.db.explorer.action.Connect", "when": "false" @@ -1111,6 +1115,11 @@ "command": "nbls.workspace.configureRunSettings", "when": "view == run-config && viewItem == configureRunSettings", "group": "inline@1" + }, + { + "command": "nbls:Edit:org.openide.actions.DeleteAction", + "when": "viewItem =~ /lifecycleState:ACTIVE*/ || (viewItem =~ /publicIp:.*/ && viewItem =~ /imageUrl:.*/)", + "group": "inline@10" } ] }, diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts index 0cc35df8c2a6..ceff0619d560 100644 --- a/java/java.lsp.server/vscode/src/extension.ts +++ b/java/java.lsp.server/vscode/src/extension.ts @@ -1407,6 +1407,11 @@ function doActivateWithJDK(specifiedJDK: string | null, context: ExtensionContex } return item; } + const lifecycleState: String = getValueAfterPrefix(item.contextValue, "lifecycleState:"); + if (lifecycleState) { + item.description = lifecycleState === "PENDING_DELETION" ? '(pending deletion)' : undefined; + } + return item; }