Skip to content

Commit

Permalink
implemented 'Open Dashboard' action (#617)
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Dietisheim <adietish@redhat.com>
  • Loading branch information
adietish committed Jul 12, 2023
1 parent bf78497 commit 54ee800
Show file tree
Hide file tree
Showing 16 changed files with 862 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.intellij.kubernetes.actions

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.logger
import com.redhat.devtools.intellij.common.actions.StructureTreeAction
import com.redhat.devtools.intellij.kubernetes.model.Notification
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_RESOURCE_KIND
import javax.swing.tree.TreePath

class OpenDashboardAction: StructureTreeAction() {

override fun actionPerformed(event: AnActionEvent?, path: Array<out TreePath>?, selected: Array<out Any>?) {
val model = getResourceModel() ?: return
val currentContext = model.getCurrentContext() ?: return
run("Opening Dashboard...", true)
{
val telemetry = TelemetryService.instance.action("open dashboard")
.property(PROP_RESOURCE_KIND, currentContext.name)
try {
val url = currentContext.getDashboardUrl()
BrowserUtil.open(url.toString())
telemetry.success().send()
} catch (e: Exception) {
logger<OpenDashboardAction>().warn("Could not open Dashboard", e)
Notification().error("Dashboard Error", e.message ?: "")
telemetry.error(e).send()
}
}
}

override fun actionPerformed(event: AnActionEvent?, path: TreePath?, selected: Any?) {
// not called
}

override fun isVisible(selected: Array<out Any>?): Boolean {
return selected?.any { isVisible(it) }
?: false
}

override fun isVisible(selected: Any?): Boolean {
val context = selected?.getElement<IActiveContext<*,*>>()
return context != null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.Reso
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.ResourcesIn.ANY_NAMESPACE
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.ResourcesIn.CURRENT_NAMESPACE
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.ResourcesIn.NO_NAMESPACE
import com.redhat.devtools.intellij.kubernetes.model.dashboard.IDashboard
import com.redhat.devtools.intellij.kubernetes.model.resource.INamespacedResourceOperator
import com.redhat.devtools.intellij.kubernetes.model.resource.INonNamespacedResourceOperator
import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator
Expand Down Expand Up @@ -52,8 +53,9 @@ import java.net.URL
abstract class ActiveContext<N : HasMetadata, C : KubernetesClient>(
context: NamedContext,
private val modelChange: IResourceModelObservable,
override val client: ClientAdapter<out C>,
private var singleResourceOperator: NonCachingSingleResourceOperator = NonCachingSingleResourceOperator(client)
val client: ClientAdapter<out C>,
protected open val dashboard: IDashboard,
private var singleResourceOperator: NonCachingSingleResourceOperator = NonCachingSingleResourceOperator(client),
) : Context(context), IActiveContext<N, C> {

override val active: Boolean = true
Expand Down Expand Up @@ -470,6 +472,7 @@ abstract class ActiveContext<N : HasMetadata, C : KubernetesClient>(
override fun close() {
logger<ActiveContext<*, *>>().debug("Closing context ${context.name}.")
watch.close()
dashboard.close()
}

private inline fun <R: HasMetadata, reified O: Any> getResourceOperator(resource: R): O? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
}
}

val client: ClientAdapter<out KubernetesClient>

/**
* The scope in which resources may exist.
*/
Expand All @@ -66,6 +64,10 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
}
}

val name: String?
get() {
return context.name
}
/**
* The master url for this context. This is the url of the cluster for this context.
*/
Expand Down Expand Up @@ -242,6 +244,8 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
*/
fun replaced(resource: HasMetadata): Boolean

fun getDashboardUrl(): String?

/**
* Closes and disposes this context.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package com.redhat.devtools.intellij.kubernetes.model.context
import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter
import com.redhat.devtools.intellij.kubernetes.model.dashboard.KubernetesDashboard
import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator
import com.redhat.devtools.intellij.kubernetes.model.resource.OperatorFactory
import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind
Expand All @@ -26,7 +27,16 @@ open class KubernetesContext(
context: NamedContext,
modelChange: IResourceModelObservable,
client: KubeClientAdapter,
) : ActiveContext<Namespace, KubernetesClient>(context, modelChange, client) {
) : ActiveContext<Namespace, KubernetesClient>(
context,
modelChange,
client,
KubernetesDashboard(
client.get(),
context.name,
client.get().masterUrl.toExternalForm()
)
) {

override val namespaceKind : ResourceKind<Namespace> = NamespacesOperator.KIND

Expand All @@ -36,4 +46,7 @@ open class KubernetesContext(
}

override fun isOpenShift() = false
override fun getDashboardUrl(): String? {
return dashboard.get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package com.redhat.devtools.intellij.kubernetes.model.context
import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter
import com.redhat.devtools.intellij.kubernetes.model.dashboard.OpenShiftDashboard
import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator
import com.redhat.devtools.intellij.kubernetes.model.resource.OperatorFactory
import com.redhat.devtools.intellij.kubernetes.model.resource.openshift.ProjectsOperator
Expand All @@ -22,10 +23,19 @@ import io.fabric8.openshift.api.model.Project
import io.fabric8.openshift.client.OpenShiftClient

open class OpenShiftContext(
context: NamedContext,
modelChange: IResourceModelObservable,
client: OSClientAdapter,
) : ActiveContext<Project, OpenShiftClient>(context, modelChange, client) {
context: NamedContext,
modelChange: IResourceModelObservable,
client: OSClientAdapter,
) : ActiveContext<Project, OpenShiftClient>(
context,
modelChange,
client,
OpenShiftDashboard(
client.get(),
context.name,
client.get().masterUrl.toExternalForm()
)
) {

override val namespaceKind = ProjectsOperator.KIND

Expand All @@ -34,4 +44,7 @@ open class OpenShiftContext(
}

override fun isOpenShift() = true
override fun getDashboardUrl(): String? {
return dashboard.get()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.intellij.kubernetes.model.dashboard

import com.redhat.devtools.intellij.kubernetes.model.util.PluginException
import io.fabric8.kubernetes.client.KubernetesClient
import io.fabric8.kubernetes.client.http.HttpStatusMessage


/**
* An abstract factory that can determine the url of the dashboard for a cluster.
*/
abstract class AbstractDashboard<C : KubernetesClient>(
protected val client: C,
private val contextName: String,
private val clusterUrl: String?,
/** for testing purposes */
protected val httpRequest: HttpRequest
): IDashboard {

private var url: String? = null

override fun get(): String {
val url = this.url ?: connect()
this.url = url
return url
}

private fun connect(): String {
val status = try {
doConnect()
} catch (e: Exception) {
throw PluginException("Could not find Dashboard for cluster $contextName at $clusterUrl: ${e.message}")
} ?: throw PluginException("Could not find Dashboard for cluster $contextName at $clusterUrl")

if (status.isSuccessful
|| status.isForbidden
) {
return status.url
} else {
throw PluginException(
"Could not reach dashboard for cluster $contextName ${
if (clusterUrl.isNullOrEmpty()) {
""
} else {
"at $clusterUrl"
}
}" + "${
if (status.status == null) {
""
} else {
". Responded with ${
HttpStatusMessage.getMessageForStatus(status.status)
}"
}
}."
)
}
}

protected abstract fun doConnect(): HttpRequest.HttpStatusCode?

override fun close() {
// noop default impl
}
}

interface IDashboard {
fun get(): String
fun close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.intellij.kubernetes.model.dashboard

import io.fabric8.kubernetes.client.http.HttpResponse
import java.net.HttpURLConnection
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.CertificateParsingException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response

class HttpRequest {

companion object {
private val trustAllTrustManager = object : X509TrustManager {

@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>?, authType: String?) {
// ignore aka trust
}

override fun getAcceptedIssuers(): Array<X509Certificate> {
return emptyArray()
}

@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {
// ignore aka trust
}
}
}

fun request(url: String): HttpStatusCode {
return requestHttpStatusCode(url)
}

fun request(host: String, port: Int): HttpStatusCode {
var status = requestHttpStatusCode("http://$host:$port")
if (status.isSuccessful) {
return status
} else {
status = requestHttpStatusCode("https://$host:$port")
if (status.isSuccessful) {
return status
}
}
return status
}

/**
* Requests the https status code for the given url.
* Return [HttpStatusCode] and throws if connecting fails.
*
* @param url the url to request the http status code for
*
* The implementation is ignores (private) SSL certificates and doesn't verify the hostname.
* All that matters is whether we can connect successfully or not.
* OkHttp is used because it allows to set a [javax.net.ssl.HostnameVerifier] on a per connection base.
*/
private fun requestHttpStatusCode(url: String): HttpStatusCode {
val sslContext = createSSLContext()
var response: Response? = null
try {
response = OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllTrustManager)
.hostnameVerifier { _, _ -> true }
.build()
.newCall(
Request.Builder()
.url(url)
.build()
)
.execute()
return HttpStatusCode(url, response.code)
} catch (e: SSLHandshakeException) {
if (e.cause is CertificateParsingException) {
/**
* Fake 200 OK response in case ssl handshake certificate could not be parsed.
* This happens with azure dashboard where a certificate is used that the jdk cannot handle:
* ```
* javax.net.ssl.SSLHandshakeException: Failed to parse server certificates
* java.security.cert.CertificateParsingException: Empty issuer DN not allowed in X509Certificates
* ```
* @see [Stackoverflow question 65692099](https://stackoverflow.com/questions/65692099/java-empty-issuer-dn-not-allowed-in-x509certificate-libimobiledevice-implementa)
* @see [kubernetes cert-manager issue #3634](https://github.com/cert-manager/cert-manager/issues/3634)
*/
return HttpStatusCode(url, HttpURLConnection.HTTP_OK)
} else {
throw e
}
} finally {
response?.close()
}
}

private fun createSSLContext(): SSLContext {
val sslContext: SSLContext = SSLContext.getDefault()
sslContext.init(null, arrayOf<TrustManager>(trustAllTrustManager), SecureRandom())
return sslContext
}

class HttpStatusCode(val url: String, val status: Int?) {
val isSuccessful: Boolean
get() {
return status != null
&& HttpResponse.isSuccessful(status)
}
val isForbidden: Boolean
get() {
return status != null
&& HttpURLConnection.HTTP_FORBIDDEN == status
}
}
}
Loading

0 comments on commit 54ee800

Please sign in to comment.