Skip to content

Commit 7a7ead9

Browse files
committed
implemented 'Open Dashboard' action (#617)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent bf78497 commit 7a7ead9

File tree

16 files changed

+862
-13
lines changed

16 files changed

+862
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.actions
12+
13+
import com.intellij.ide.BrowserUtil
14+
import com.intellij.openapi.actionSystem.AnActionEvent
15+
import com.intellij.openapi.diagnostic.logger
16+
import com.redhat.devtools.intellij.common.actions.StructureTreeAction
17+
import com.redhat.devtools.intellij.kubernetes.model.Notification
18+
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext
19+
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService
20+
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_RESOURCE_KIND
21+
import javax.swing.tree.TreePath
22+
23+
class OpenDashboardAction: StructureTreeAction() {
24+
25+
override fun actionPerformed(event: AnActionEvent?, path: Array<out TreePath>?, selected: Array<out Any>?) {
26+
val model = getResourceModel() ?: return
27+
val currentContext = model.getCurrentContext() ?: return
28+
run("Opening Dashboard...", true)
29+
{
30+
val telemetry = TelemetryService.instance.action("open dashboard")
31+
.property(PROP_RESOURCE_KIND, currentContext.name)
32+
try {
33+
val url = currentContext.getDashboardUrl()
34+
BrowserUtil.open(url.toString())
35+
telemetry.success().send()
36+
} catch (e: Exception) {
37+
logger<OpenDashboardAction>().warn("Could not open Dashboard", e)
38+
Notification().error("Dashboard Error", e.message ?: "")
39+
telemetry.error(e).send()
40+
}
41+
}
42+
}
43+
44+
override fun actionPerformed(event: AnActionEvent?, path: TreePath?, selected: Any?) {
45+
// not called
46+
}
47+
48+
override fun isVisible(selected: Array<out Any>?): Boolean {
49+
return selected?.any { isVisible(it) }
50+
?: false
51+
}
52+
53+
override fun isVisible(selected: Any?): Boolean {
54+
val context = selected?.getElement<IActiveContext<*,*>>()
55+
return context != null
56+
}
57+
}

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/ActiveContext.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.Reso
2323
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.ResourcesIn.ANY_NAMESPACE
2424
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.ResourcesIn.CURRENT_NAMESPACE
2525
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext.ResourcesIn.NO_NAMESPACE
26+
import com.redhat.devtools.intellij.kubernetes.model.dashboard.IDashboard
2627
import com.redhat.devtools.intellij.kubernetes.model.resource.INamespacedResourceOperator
2728
import com.redhat.devtools.intellij.kubernetes.model.resource.INonNamespacedResourceOperator
2829
import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator
@@ -52,8 +53,9 @@ import java.net.URL
5253
abstract class ActiveContext<N : HasMetadata, C : KubernetesClient>(
5354
context: NamedContext,
5455
private val modelChange: IResourceModelObservable,
55-
override val client: ClientAdapter<out C>,
56-
private var singleResourceOperator: NonCachingSingleResourceOperator = NonCachingSingleResourceOperator(client)
56+
val client: ClientAdapter<out C>,
57+
protected open val dashboard: IDashboard,
58+
private var singleResourceOperator: NonCachingSingleResourceOperator = NonCachingSingleResourceOperator(client),
5759
) : Context(context), IActiveContext<N, C> {
5860

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

475478
private inline fun <R: HasMetadata, reified O: Any> getResourceOperator(resource: R): O? {

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/IActiveContext.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
4848
}
4949
}
5050

51-
val client: ClientAdapter<out KubernetesClient>
52-
5351
/**
5452
* The scope in which resources may exist.
5553
*/
@@ -66,6 +64,10 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
6664
}
6765
}
6866

67+
val name: String?
68+
get() {
69+
return context.name
70+
}
6971
/**
7072
* The master url for this context. This is the url of the cluster for this context.
7173
*/
@@ -242,6 +244,8 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
242244
*/
243245
fun replaced(resource: HasMetadata): Boolean
244246

247+
fun getDashboardUrl(): String?
248+
245249
/**
246250
* Closes and disposes this context.
247251
*/

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContext.kt

+14-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package com.redhat.devtools.intellij.kubernetes.model.context
1313
import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable
1414
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
1515
import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter
16+
import com.redhat.devtools.intellij.kubernetes.model.dashboard.KubernetesDashboard
1617
import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator
1718
import com.redhat.devtools.intellij.kubernetes.model.resource.OperatorFactory
1819
import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind
@@ -26,7 +27,16 @@ open class KubernetesContext(
2627
context: NamedContext,
2728
modelChange: IResourceModelObservable,
2829
client: KubeClientAdapter,
29-
) : ActiveContext<Namespace, KubernetesClient>(context, modelChange, client) {
30+
) : ActiveContext<Namespace, KubernetesClient>(
31+
context,
32+
modelChange,
33+
client,
34+
KubernetesDashboard(
35+
client.get(),
36+
context.name,
37+
client.get().masterUrl.toExternalForm()
38+
)
39+
) {
3040

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

@@ -36,4 +46,7 @@ open class KubernetesContext(
3646
}
3747

3848
override fun isOpenShift() = false
49+
override fun getDashboardUrl(): String? {
50+
return dashboard.get()
51+
}
3952
}

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/OpenShiftContext.kt

+17-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package com.redhat.devtools.intellij.kubernetes.model.context
1313
import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable
1414
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
1515
import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter
16+
import com.redhat.devtools.intellij.kubernetes.model.dashboard.OpenShiftDashboard
1617
import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator
1718
import com.redhat.devtools.intellij.kubernetes.model.resource.OperatorFactory
1819
import com.redhat.devtools.intellij.kubernetes.model.resource.openshift.ProjectsOperator
@@ -22,10 +23,19 @@ import io.fabric8.openshift.api.model.Project
2223
import io.fabric8.openshift.client.OpenShiftClient
2324

2425
open class OpenShiftContext(
25-
context: NamedContext,
26-
modelChange: IResourceModelObservable,
27-
client: OSClientAdapter,
28-
) : ActiveContext<Project, OpenShiftClient>(context, modelChange, client) {
26+
context: NamedContext,
27+
modelChange: IResourceModelObservable,
28+
client: OSClientAdapter,
29+
) : ActiveContext<Project, OpenShiftClient>(
30+
context,
31+
modelChange,
32+
client,
33+
OpenShiftDashboard(
34+
client.get(),
35+
context.name,
36+
client.get().masterUrl.toExternalForm()
37+
)
38+
) {
2939

3040
override val namespaceKind = ProjectsOperator.KIND
3141

@@ -34,4 +44,7 @@ open class OpenShiftContext(
3444
}
3545

3646
override fun isOpenShift() = true
47+
override fun getDashboardUrl(): String? {
48+
return dashboard.get()
49+
}
3750
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.model.dashboard
12+
13+
import com.redhat.devtools.intellij.kubernetes.model.util.PluginException
14+
import io.fabric8.kubernetes.client.KubernetesClient
15+
import io.fabric8.kubernetes.client.http.HttpStatusMessage
16+
17+
18+
/**
19+
* An abstract factory that can determine the url of the dashboard for a cluster.
20+
*/
21+
abstract class AbstractDashboard<C : KubernetesClient>(
22+
protected val client: C,
23+
private val contextName: String,
24+
private val clusterUrl: String?,
25+
/** for testing purposes */
26+
protected val httpRequest: HttpRequest
27+
): IDashboard {
28+
29+
private var url: String? = null
30+
31+
override fun get(): String {
32+
val url = this.url ?: connect()
33+
this.url = url
34+
return url
35+
}
36+
37+
private fun connect(): String {
38+
val status = try {
39+
doConnect()
40+
} catch (e: Exception) {
41+
throw PluginException("Could not find Dashboard for cluster $contextName at $clusterUrl: ${e.message}")
42+
} ?: throw PluginException("Could not find Dashboard for cluster $contextName at $clusterUrl")
43+
44+
if (status.isSuccessful
45+
|| status.isForbidden
46+
) {
47+
return status.url
48+
} else {
49+
throw PluginException(
50+
"Could not reach dashboard for cluster $contextName ${
51+
if (clusterUrl.isNullOrEmpty()) {
52+
""
53+
} else {
54+
"at $clusterUrl"
55+
}
56+
}" + "${
57+
if (status.status == null) {
58+
""
59+
} else {
60+
". Responded with ${
61+
HttpStatusMessage.getMessageForStatus(status.status)
62+
}"
63+
}
64+
}."
65+
)
66+
}
67+
}
68+
69+
protected abstract fun doConnect(): HttpRequest.HttpStatusCode?
70+
71+
override fun close() {
72+
// noop default impl
73+
}
74+
}
75+
76+
interface IDashboard {
77+
fun get(): String
78+
fun close()
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.model.dashboard
12+
13+
import io.fabric8.kubernetes.client.http.HttpResponse
14+
import java.net.HttpURLConnection
15+
import java.security.SecureRandom
16+
import java.security.cert.CertificateException
17+
import java.security.cert.CertificateParsingException
18+
import java.security.cert.X509Certificate
19+
import javax.net.ssl.SSLContext
20+
import javax.net.ssl.SSLHandshakeException
21+
import javax.net.ssl.TrustManager
22+
import javax.net.ssl.X509TrustManager
23+
import okhttp3.OkHttpClient
24+
import okhttp3.Request
25+
import okhttp3.Response
26+
27+
class HttpRequest {
28+
29+
companion object {
30+
private val trustAllTrustManager = object : X509TrustManager {
31+
32+
@Throws(CertificateException::class)
33+
override fun checkServerTrusted(chain: Array<X509Certificate>?, authType: String?) {
34+
// ignore aka trust
35+
}
36+
37+
override fun getAcceptedIssuers(): Array<X509Certificate> {
38+
return emptyArray()
39+
}
40+
41+
@Throws(CertificateException::class)
42+
override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {
43+
// ignore aka trust
44+
}
45+
}
46+
}
47+
48+
fun request(url: String): HttpStatusCode {
49+
return requestHttpStatusCode(url)
50+
}
51+
52+
fun request(host: String, port: Int): HttpStatusCode {
53+
var status = requestHttpStatusCode("http://$host:$port")
54+
if (status.isSuccessful) {
55+
return status
56+
} else {
57+
status = requestHttpStatusCode("https://$host:$port")
58+
if (status.isSuccessful) {
59+
return status
60+
}
61+
}
62+
return status
63+
}
64+
65+
/**
66+
* Requests the https status code for the given url.
67+
* Return [HttpStatusCode] and throws if connecting fails.
68+
*
69+
* @param url the url to request the http status code for
70+
*
71+
* The implementation is ignores (private) SSL certificates and doesn't verify the hostname.
72+
* All that matters is whether we can connect successfully or not.
73+
* OkHttp is used because it allows to set a [javax.net.ssl.HostnameVerifier] on a per connection base.
74+
*/
75+
private fun requestHttpStatusCode(url: String): HttpStatusCode {
76+
val sslContext = createSSLContext()
77+
var response: Response? = null
78+
try {
79+
response = OkHttpClient.Builder()
80+
.sslSocketFactory(sslContext.socketFactory, trustAllTrustManager)
81+
.hostnameVerifier { _, _ -> true }
82+
.build()
83+
.newCall(
84+
Request.Builder()
85+
.url(url)
86+
.build()
87+
)
88+
.execute()
89+
return HttpStatusCode(url, response.code)
90+
} catch (e: SSLHandshakeException) {
91+
if (e.cause is CertificateParsingException) {
92+
/**
93+
* Fake 200 OK response in case ssl handshake certificate could not be parsed.
94+
* This happens with azure dashboard where a certificate is used that the jdk cannot handle:
95+
* ```
96+
* javax.net.ssl.SSLHandshakeException: Failed to parse server certificates
97+
* java.security.cert.CertificateParsingException: Empty issuer DN not allowed in X509Certificates
98+
* ```
99+
* @see [Stackoverflow question 65692099](https://stackoverflow.com/questions/65692099/java-empty-issuer-dn-not-allowed-in-x509certificate-libimobiledevice-implementa)
100+
* @see [kubernetes cert-manager issue #3634](https://github.com/cert-manager/cert-manager/issues/3634)
101+
*/
102+
return HttpStatusCode(url, HttpURLConnection.HTTP_OK)
103+
} else {
104+
throw e
105+
}
106+
} finally {
107+
response?.close()
108+
}
109+
}
110+
111+
private fun createSSLContext(): SSLContext {
112+
val sslContext: SSLContext = SSLContext.getDefault()
113+
sslContext.init(null, arrayOf<TrustManager>(trustAllTrustManager), SecureRandom())
114+
return sslContext
115+
}
116+
117+
class HttpStatusCode(val url: String, val status: Int?) {
118+
val isSuccessful: Boolean
119+
get() {
120+
return status != null
121+
&& HttpResponse.isSuccessful(status)
122+
}
123+
val isForbidden: Boolean
124+
get() {
125+
return status != null
126+
&& HttpURLConnection.HTTP_FORBIDDEN == status
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)