diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2d9863c1e71..0adda3250366 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -416,11 +416,25 @@ jobs: - name: Build nbms run: ant $OPTS build-nbms - # 13-14 min + # 13-14 min for javadoc; JDK version must be synced with nb-javac + - name: Set up JDK 23 for javadoc + if: env.test_javadoc == 'true' && success() + uses: actions/setup-java@v4 + with: + java-version: 23 + distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }} + - name: Build javadoc if: env.test_javadoc == 'true' && success() run: ant $OPTS build-javadoc + - name: Set up JDK ${{ matrix.java }} + if: env.test_javadoc == 'true' && success() + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }} + # runs only in PRs if requested; ~18 min - name: Build all Tests if: env.test_tests == 'true' && github.event_name == 'pull_request' && success() diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/ConfigMapProvider.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/ConfigMapProvider.java index 99e7d84745c3..52a7190db05c 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/ConfigMapProvider.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/ConfigMapProvider.java @@ -64,8 +64,9 @@ public String getMicronautConfigFiles() { public void createConfigMap() { KubernetesUtils.runWithClient(cluster, client -> { - boolean configMapExist = checkIfConfigMapExist(client); - if (configMapExist) { + ConfigMapList cmList = client.configMaps().inNamespace(cluster.getNamespace()).list(); + ConfigMap configMap = (ConfigMap) KubernetesUtils.findResource(client, cmList, projectName); + if (configMap != null) { updateConfigMap(client); return; } @@ -87,16 +88,6 @@ public ConfigMapVolumeSource getVolumeSource() { .build(); } - private boolean checkIfConfigMapExist(KubernetesClient client) { - ConfigMapList cmList = client.configMaps().inNamespace(cluster.getNamespace()).list(); - for (ConfigMap cm : cmList.getItems()) { - if (projectName.equals(cm.getMetadata().getName())) { - return true; - } - } - return false; - } - private void updateConfigMap(KubernetesClient client) { Map applicationProperties = propertiesGenerator.getApplication(); Map bootstrapProperties = propertiesGenerator.getBootstrap(); diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/CreateSecretRotationCronJobCommand.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/CreateSecretRotationCronJobCommand.java new file mode 100644 index 000000000000..665d2de59270 --- /dev/null +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/CreateSecretRotationCronJobCommand.java @@ -0,0 +1,303 @@ +/* + * 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.assets; + +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretList; +import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.fabric8.kubernetes.api.model.ServiceAccountBuilder; +import io.fabric8.kubernetes.api.model.ServiceAccountList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.netbeans.spi.lsp.CommandProvider; +import org.openide.util.lookup.ServiceProvider; +import io.fabric8.kubernetes.api.model.batch.v1.CronJobList; +import io.fabric8.kubernetes.api.model.batch.v1.CronJob; +import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder; +import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder; +import io.fabric8.kubernetes.api.model.rbac.ClusterRole; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBindingBuilder; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBindingList; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleList; +import io.fabric8.kubernetes.client.KubernetesClient; +import java.util.Calendar; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import org.netbeans.modules.cloud.oracle.NotificationUtils; +import org.netbeans.modules.cloud.oracle.compute.ClusterItem; +import org.openide.util.NbBundle; + +/** + * + * @author Dusan Petrovic + */ +@NbBundle.Messages({ + "CronJobCreationError=Error while creating secret rotation CronJob" +}) +@ServiceProvider(service = CommandProvider.class) +public class CreateSecretRotationCronJobCommand implements CommandProvider { + + private static final String COMMAND_CREATE_CRONJOB = "nbls.cloud.assets.cluster.cronjob.create"; //NOI18N + private static final String SECRET_NAME = "docker-bearer-vscode-generated-ocirsecret"; //NOI18N + private static final String CRONJOB_NAME = "secret-rotation-cronjob"; //NOI18N + private static final String CLUSTER_ROLE_BINDING_NAME = "secret-manager-binding"; //NOI18N + private static final String CLUSTER_ROLE_NAME = "secret-manager"; //NOI18N + private static final String SERVICE_ACCOUNT_NAME = "create-secret-svc-account"; //NOI18N + private static final String BASE_IMAGE = "ghcr.io/oracle/oci-cli:latest"; //NOI18N + private static final String CONTAINER_NAME = "create-secret"; //NOI18N + private static final int WAITING_TIMEOUT = 60; + + private static final Set COMMANDS = new HashSet<>(Arrays.asList( + COMMAND_CREATE_CRONJOB + )); + + private ClusterItem cluster; + + @Override + public Set getCommands() { + return Collections.unmodifiableSet(COMMANDS); + } + + @Override + public CompletableFuture runCommand(String command, List arguments) { + return createSecretRotationCronJob(); + } + + public CompletableFuture createSecretRotationCronJob() { + CompletableFuture completableFuture = new CompletableFuture(); + this.cluster = CloudAssets.getDefault().getItem(ClusterItem.class); + KubernetesUtils.runWithClient(cluster, client -> { + try { + ServiceAccount serviceAccount = createServiceAccountIfNotExist(client); + createClusterRoleIfNotExist(client); + createClusterRoleBindingIfNotExist(client); + createCronJobIfNotExist(client, serviceAccount); + completableFuture.complete(null); + } catch(Exception ex) { + completableFuture.completeExceptionally(ex); + NotificationUtils.showErrorMessage(Bundle.CronJobCreationError()); + } + }); + return completableFuture; + } + + private void createCronJobIfNotExist(KubernetesClient client, ServiceAccount serviceAccount) { + CronJobList existingCronJobs = client.batch().v1().cronjobs().inNamespace(cluster.getNamespace()).list(); + CronJob cronJob = (CronJob) KubernetesUtils.findResource(client, existingCronJobs, CRONJOB_NAME); + if (cronJob != null) { + if (!secretExist(client)) { + invokeCronJob(client, cronJob); + } + return; + } + cronJob = new CronJobBuilder() + .withNewMetadata() + .withName(CRONJOB_NAME) + .withNamespace(cluster.getNamespace()) + .endMetadata() + .withNewSpec() + .withSchedule(getCronExpression()) + .withNewJobTemplate() + .withNewSpec() + .withBackoffLimit(0) + .withTemplate(cronJobPodTemplate(serviceAccount)) + .endSpec() + .endJobTemplate() + .endSpec() + .build(); + + client.batch().v1() + .cronjobs() + .inNamespace(cluster.getNamespace()) + .resource(cronJob) + .create(); + + invokeCronJob(client, cronJob); + } + + private boolean secretExist(KubernetesClient client) { + SecretList existingSecrets = client.secrets().inNamespace(cluster.getNamespace()).list(); + Secret secret = (Secret) KubernetesUtils.findResource(client, existingSecrets, SECRET_NAME); + return secret != null; + } + + private void invokeCronJob(KubernetesClient client, CronJob cronJob) { + client.batch().v1() + .jobs() + .inNamespace(cluster.getNamespace()) + .resource(new JobBuilder() + .withNewMetadata() + .withName("cronjob-invocation-" + UUID.randomUUID()) //NOI18N + .endMetadata() + .withSpec(cronJob.getSpec().getJobTemplate().getSpec()) + .build()) + .create(); + + waitForConditionWithTimeout(() -> { + return secretExist(client); + }, WAITING_TIMEOUT).join(); + } + + private CompletableFuture waitForConditionWithTimeout(Supplier condition, long timeout) { + CompletableFuture future = new CompletableFuture<>(); + + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + ScheduledFuture checkTask = executor.scheduleAtFixedRate(() -> { + if (condition.get()) { + future.complete(null); + } + }, 0, 5, TimeUnit.SECONDS); + + executor.schedule(() -> { + if (!future.isDone()) { + future.completeExceptionally(new TimeoutException("Condition was not met within the timeout.")); //NOI18N + } + checkTask.cancel(true); + executor.shutdown(); + }, timeout, TimeUnit.SECONDS); + + return future; + } + + private String getCronExpression() { + Calendar calendar = Calendar.getInstance(); + int currentMinute = calendar.get(Calendar.MINUTE); + return currentMinute + " * * * *"; + } + + private PodTemplateSpec cronJobPodTemplate(ServiceAccount serviceAccount) { + return new PodTemplateSpecBuilder() + .withNewSpec() + .withHostNetwork(Boolean.TRUE) + .addNewContainer() + .withName(CONTAINER_NAME) + .withImage(BASE_IMAGE) + .addNewEnv() + .withName("OCI_CLI_AUTH") //NOI18N + .withValue("instance_principal") //NOI18N + .endEnv() + .withCommand("/bin/bash", "-c", createSecretCommand()) //NOI18N + .endContainer() + .withRestartPolicy("Never") //NOI18N + .withServiceAccountName(serviceAccount.getMetadata().getName()) + .endSpec() + .build(); + } + + private ServiceAccount createServiceAccountIfNotExist(KubernetesClient client) { + ServiceAccountList existingServiceAccounts = client.serviceAccounts().inNamespace(cluster.getNamespace()).list(); + ServiceAccount serviceAccount = (ServiceAccount) KubernetesUtils.findResource(client, existingServiceAccounts, SERVICE_ACCOUNT_NAME); + if (serviceAccount != null) { + return serviceAccount; + } + serviceAccount = new ServiceAccountBuilder() + .withNewMetadata() + .withName(SERVICE_ACCOUNT_NAME) + .endMetadata() + .build(); + + return client.serviceAccounts() + .inNamespace(cluster.getNamespace()) + .resource(serviceAccount) + .create(); + } + + private void createClusterRoleIfNotExist(KubernetesClient client) { + ClusterRoleList existingClusterRole = client.rbac().clusterRoles().list(); + ClusterRole clusterRole = (ClusterRole) KubernetesUtils.findResource(client, existingClusterRole, CLUSTER_ROLE_NAME); + if (clusterRole != null) { + return; + } + clusterRole = new ClusterRoleBuilder() + .withNewMetadata() + .withName(CLUSTER_ROLE_NAME) + .endMetadata() + .addNewRule() + .withApiGroups("") + .withResources("secrets") //NOI18N + .withVerbs("create", "get", "patch", "delete") //NOI18N + .endRule() + .build(); + + client.rbac().clusterRoles() + .resource(clusterRole) + .create(); + } + + private void createClusterRoleBindingIfNotExist(KubernetesClient client) { + ClusterRoleBindingList existingClusterRoleBinding = client.rbac().clusterRoleBindings().list(); + ClusterRoleBinding clusterRoleBinding = (ClusterRoleBinding) KubernetesUtils.findResource(client, existingClusterRoleBinding, CLUSTER_ROLE_BINDING_NAME); + if (clusterRoleBinding != null) { + return; + } + clusterRoleBinding = new ClusterRoleBindingBuilder() + .withNewMetadata() + .withName(CLUSTER_ROLE_BINDING_NAME) + .endMetadata() + .addNewSubject() + .withName(SERVICE_ACCOUNT_NAME) + .withKind("ServiceAccount") //NOI18N + .withNamespace(cluster.getNamespace()) + .endSubject() + .withNewRoleRef() + .withKind("ClusterRole") //NOI18N + .withName(CLUSTER_ROLE_NAME) + .withApiGroup("rbac.authorization.k8s.io") //NOI18N + .endRoleRef() + .build(); + + client.rbac().clusterRoleBindings() + .resource(clusterRoleBinding) + .create(); + } + + private String createSecretCommand() { + String repoEndpoint = cluster.getRegionCode() + ".ocir.io"; //NOI18N + return + "KUBECTL_VERSION=\"v1.27.4\"\n" + //NOI18N + "case \"$(uname -m)\" in\n" + //NOI18N + " x86_64) ARCHITECTURE=\"amd64\" ;;\n" + //NOI18N + " aarch64) ARCHITECTURE=\"arm64\" ;;\n" + //NOI18N + " *) ARCHITECTURE=\"Unknown architecture\" ;;\n" + //NOI18N + "esac\n" + //NOI18N + "KUBECTL_URL=\"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCHITECTURE}/kubectl\"\n" + //NOI18N + "mkdir -p /tmp/bin\n" + //NOI18N + "curl -LO \"${KUBECTL_URL}\"\n" + //NOI18N + "chmod +x ./kubectl\n" + //NOI18N + "mv ./kubectl /tmp/bin/kubectl\n" + //NOI18N + "export PATH=$PATH:/tmp/bin\n" + //NOI18N + "TOKEN=$(oci raw-request --http-method GET --target-uri https://" + repoEndpoint + "/20180419/docker/token | jq -r '.data.token')\n" + //NOI18N + "kubectl create secret --save-config --dry-run=client docker-registry " + SECRET_NAME + " --docker-server=" + repoEndpoint + " --docker-username=BEARER_TOKEN --docker-password=\"$TOKEN\" -o yaml | kubectl apply -f - "; //NOI18N + } + +} diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/KubernetesUtils.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/KubernetesUtils.java index 5c88b56dd95c..37ac304999a1 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/KubernetesUtils.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/KubernetesUtils.java @@ -25,6 +25,11 @@ import com.oracle.bmc.http.client.Method; import com.oracle.bmc.http.client.jersey.JerseyHttpProvider; import com.oracle.bmc.http.signing.RequestSigningFilter; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResource; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.batch.v1.CronJob; +import io.fabric8.kubernetes.api.model.batch.v1.CronJobList; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; @@ -140,4 +145,15 @@ private static String getBearerToken(ClusterItem cluster) { throw new RuntimeException(ex); } } + + public static KubernetesResource findResource(KubernetesClient client, KubernetesResourceList existingResources, String resourceName) { + if (resourceName == null) return null; + + for (HasMetadata resource : existingResources.getItems()) { + if (resourceName.equals(resource.getMetadata().getName())) { + return resource; + } + } + return null; + } } diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/RunInClusterAction.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/RunInClusterAction.java index d0d480b6a796..c3762e9600fb 100644 --- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/RunInClusterAction.java +++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/assets/RunInClusterAction.java @@ -56,7 +56,8 @@ @NbBundle.Messages({ "RunInCluster=Run in OKE Cluster", - "Deploying=Deploying project \"{0}\" to the cluster \"{1}\"" + "Deploying=Deploying project \"{0}\" to the cluster \"{1}\"", + "CreatingSecretRotationCronJob=Creating secret rotation CronJob" }) public class RunInClusterAction implements ActionListener { @@ -92,14 +93,14 @@ private void runInCluster() { } try { h.start(); + + CreateSecretRotationCronJobCommand srcc = new CreateSecretRotationCronJobCommand(); + h.progress(Bundle.CreatingSecretRotationCronJob()); + srcc.createSecretRotationCronJob().join(); + KubernetesUtils.runWithClient(cluster, client -> { - Deployment existingDeployment = null; DeploymentList dList = client.apps().deployments().inNamespace(cluster.getNamespace()).list(); - for (Deployment deployment : dList.getItems()) { - if (projectName.equals(deployment.getMetadata().getName())) { - existingDeployment = deployment; - } - } + Deployment existingDeployment = (Deployment) KubernetesUtils.findResource(client, dList, projectName); ConfigMapProvider configMapProvider = new ConfigMapProvider(projectName, cluster); configMapProvider.createConfigMap(); diff --git a/ide/jumpto/src/org/netbeans/modules/jumpto/type/GoToTypeAction.java b/ide/jumpto/src/org/netbeans/modules/jumpto/type/GoToTypeAction.java index ac145bdbba1f..bbe39a3e4eae 100644 --- a/ide/jumpto/src/org/netbeans/modules/jumpto/type/GoToTypeAction.java +++ b/ide/jumpto/src/org/netbeans/modules/jumpto/type/GoToTypeAction.java @@ -613,6 +613,8 @@ private List getTypeNames(final String text, int[] ret final TypeProvider.Result result = TypeProviderAccessor.DEFAULT.createResult(items, message, context); provider.computeTypeNames(context, result); retry[0] = mergeRetryTimeOut(retry[0], TypeProviderAccessor.DEFAULT.getRetry(result)); + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Provider ''" + provider.getDisplayName() + "'' yields an exception", ex); } finally { current = null; } diff --git a/java/java.j2seplatform/src/org/netbeans/modules/java/j2seplatform/libraries/J2SEVolumeCustomizer.form b/java/java.j2seplatform/src/org/netbeans/modules/java/j2seplatform/libraries/J2SEVolumeCustomizer.form index 0ef9a6b1c696..7b174977bb5f 100644 --- a/java/java.j2seplatform/src/org/netbeans/modules/java/j2seplatform/libraries/J2SEVolumeCustomizer.form +++ b/java/java.j2seplatform/src/org/netbeans/modules/java/j2seplatform/libraries/J2SEVolumeCustomizer.form @@ -1,4 +1,4 @@ - + + + diff --git a/platform/o.n.swing.laf.flatlaf/src/org/netbeans/swing/laf/flatlaf/FlatLightLaf.properties b/platform/o.n.swing.laf.flatlaf/src/org/netbeans/swing/laf/flatlaf/FlatLightLaf.properties index 95606044b90f..0ba3f1707318 100644 --- a/platform/o.n.swing.laf.flatlaf/src/org/netbeans/swing/laf/flatlaf/FlatLightLaf.properties +++ b/platform/o.n.swing.laf.flatlaf/src/org/netbeans/swing/laf/flatlaf/FlatLightLaf.properties @@ -80,3 +80,4 @@ nb.quicksearch.border=1,1,1,1,@background # output nb.output.selectionBackground=#89BCED +nb.output.warning.foreground=#FF9900