-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathAllContexts.kt
276 lines (249 loc) · 9.17 KB
/
AllContexts.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/*******************************************************************************
* Copyright (c) 2020 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
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger
import com.redhat.devtools.intellij.common.utils.ConfigHelper
import com.redhat.devtools.intellij.common.utils.ConfigWatcher
import com.redhat.devtools.intellij.common.utils.ExecHelper
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
import com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig
import com.redhat.devtools.intellij.kubernetes.model.context.Context
import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext
import com.redhat.devtools.intellij.kubernetes.model.context.IContext
import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind
import com.redhat.devtools.intellij.kubernetes.model.util.ResettableLazyProperty
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.NAME_PREFIX_CONTEXT
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_IS_OPENSHIFT
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_KUBERNETES_VERSION
import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_OPENSHIFT_VERSION
import io.fabric8.kubernetes.api.model.HasMetadata
import io.fabric8.kubernetes.client.Config
import io.fabric8.kubernetes.client.KubernetesClient
import java.nio.file.Paths
import java.util.concurrent.CompletionException
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
interface IAllContexts {
/**
* The current context. Is also contained in [all].
*/
val current: IActiveContext<out HasMetadata, out KubernetesClient>?
/**
* All contexts that are available. These are loaded from the kube-config file.
*/
val all: List<IContext>
/**
* Sets the given context as current context. The old context is closed.
*
* @param context the context to set as current context
* @return new active context
*
* @see current
*/
fun setCurrentContext(context: IContext): IActiveContext<out HasMetadata, out KubernetesClient>?
/**
* Sets the given namespace as current namespace.
* A new context is created and set. This is done because the fabric8 client is not able to switch namespace.
*
* @param namespace the namespace to set as current namespace
* @return new active context with the given namespace as current namespace
*
* @see current
*/
fun setCurrentNamespace(namespace: String): IActiveContext<out HasMetadata, out KubernetesClient>?
/**
* Invalidates all contexts and the current context.
* Causes them to be reloaded from config when accessed.
*/
fun refresh()
}
open class AllContexts(
private val contextFactory: (ClientAdapter<out KubernetesClient>, IResourceModelObservable) -> IActiveContext<out HasMetadata, out KubernetesClient>? =
IActiveContext.Factory::create,
private val modelChange: IResourceModelObservable,
private val clientFactory: (
namespace: String?,
context: String?
) -> ClientAdapter<out KubernetesClient>
= { namespace, context -> ClientAdapter.Factory.create(namespace, context) }
) : IAllContexts {
init {
watchKubeConfig()
}
private val lock = ReentrantReadWriteLock()
private val client = ResettableLazyProperty {
lock.write {
clientFactory.invoke(null, null)
}
}
override val current: IActiveContext<out HasMetadata, out KubernetesClient>?
get() {
return findActive(all)
}
private val _all: MutableList<IContext> = mutableListOf()
override val all: List<IContext>
get() {
lock.write {
if (_all.isEmpty()) {
try {
val all = createContexts(client.get(), client.get()?.config)
_all.addAll(all)
} catch (e: Exception) {
//
}
}
return _all
}
}
override fun setCurrentContext(context: IContext): IActiveContext<out HasMetadata, out KubernetesClient>? {
if (current == context) {
return current
}
val newClient = clientFactory.invoke(context.context.context.namespace, context.context.name)
val new = setCurrentContext(newClient, emptyList())
if (new != null) {
modelChange.fireAllContextsChanged()
}
return new
}
override fun setCurrentNamespace(namespace: String): IActiveContext<out HasMetadata, out KubernetesClient>? {
val old = this.current ?: return null
val newClient = clientFactory.invoke(namespace, old.context.name)
val new = setCurrentContext(newClient, old.getWatched())
if (new != null) {
modelChange.fireCurrentNamespaceChanged(new, old)
}
return new
}
private fun setCurrentContext(
newClient: ClientAdapter<out KubernetesClient>,
toWatch: Collection<ResourceKind<out HasMetadata>>?,
) : IActiveContext<out HasMetadata, out KubernetesClient>? {
lock.write {
try {
replaceClient(newClient, this.client.get())
newClient.config.save().join()
current?.close()
clearAllContexts() // causes reload of all contexts when accessed afterwards
val newCurrent = current // gets new current from all
if (toWatch != null) {
newCurrent?.watchAll(toWatch)
}
return newCurrent
} catch (e: CompletionException) {
val cause = e.cause ?: throw e
throw cause
}
}
}
private fun clearAllContexts() {
_all.clear()
}
override fun refresh() {
lock.write {
this.current?.close()
clearAllContexts() // latter access will cause reload
}
modelChange.fireAllContextsChanged()
}
private fun findActive(all: List<IContext>): IActiveContext<out HasMetadata, out KubernetesClient>? {
return if (all.isNotEmpty()) {
val activeContext = all.firstOrNull { it.active }
activeContext as? IActiveContext<out HasMetadata, out KubernetesClient>
} else {
null
}
}
private fun createContexts(client: ClientAdapter<out KubernetesClient>?, config: ClientConfig?)
: List<IContext> {
if (client == null
|| config == null
) {
return emptyList()
}
lock.read {
return config.allContexts
.map {
if (config.isCurrent(it)) {
createActiveContext(client) ?: Context(it)
} else {
Context(it)
}
}
}
}
private fun replaceClient(new: ClientAdapter<out KubernetesClient>, old: ClientAdapter<out KubernetesClient>?)
: ClientAdapter<out KubernetesClient> {
old?.close()
this.client.set(new)
return new
}
private fun createActiveContext(client: ClientAdapter<out KubernetesClient>?)
: IActiveContext<out HasMetadata, out KubernetesClient>? {
if (client == null) {
return null
}
val context = contextFactory.invoke(client, modelChange) ?: return null
reportTelemetry(context)
return context
}
protected open fun reportTelemetry(context: IActiveContext<out HasMetadata, out KubernetesClient>) {
ExecHelper.submit {
val telemetry = TelemetryService.instance.action(NAME_PREFIX_CONTEXT + "use")
.property(PROP_IS_OPENSHIFT, context.isOpenShift().toString())
try {
telemetry
.property(PROP_KUBERNETES_VERSION, context.version.kubernetesVersion)
.property(PROP_OPENSHIFT_VERSION, context.version.openshiftVersion)
.send()
} catch (e: RuntimeException) {
telemetry
.property(PROP_KUBERNETES_VERSION, "error retrieving")
.property(PROP_OPENSHIFT_VERSION, "error retrieving")
.send()
logger<AllContexts>().warn("Could not report context/cluster versions", e)
}
}
}
protected open fun watchKubeConfig() {
val path = Paths.get(Config.getKubeconfigFilename())
/**
* [ConfigWatcher] cannot add/remove listeners nor can it get closed (and stop the [java.nio.file.WatchService]).
* We therefore have to create a single instance in here rather than using it in a shielded/private way within
* [com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig].
* Closing/Recreating [ConfigWatcher] is needed when used within [com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig].
* The latter gets closed/recreated whenever the context changes in
* [com.redhat.devtools.intellij.kubernetes.model.client.KubeConfigAdapter].
*/
val watcher = ConfigWatcher(path) { _, config: io.fabric8.kubernetes.api.model.Config? -> onKubeConfigChanged(config) }
runAsync(watcher::run)
}
protected open fun onKubeConfigChanged(fileConfig: io.fabric8.kubernetes.api.model.Config?) {
lock.read {
fileConfig ?: return
val client = client.get() ?: return
val clientConfig = client.config.configuration
if (ConfigHelper.areEqual(fileConfig, clientConfig)) {
return
}
this.client.reset() // create new client when accessed
client.close()
}
refresh()
}
/** for testing purposes */
protected open fun runAsync(runnable: () -> Unit) {
ApplicationManager.getApplication().executeOnPooledThread(runnable)
}
}