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.editor
12
+
13
+ import com.intellij.openapi.diagnostic.logger
14
+ import com.redhat.devtools.intellij.kubernetes.model.IResourceModel
15
+ import com.redhat.devtools.intellij.kubernetes.model.IResourceModelListener
16
+ import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext
17
+ import com.redhat.devtools.intellij.kubernetes.model.util.areEqual
18
+ import com.redhat.devtools.intellij.kubernetes.model.util.hasGenerateName
19
+ import com.redhat.devtools.intellij.kubernetes.model.util.hasName
20
+ import com.redhat.devtools.intellij.kubernetes.model.util.isSameResource
21
+ import com.redhat.devtools.intellij.kubernetes.model.util.toKindAndName
22
+ import com.redhat.devtools.intellij.kubernetes.model.util.toMessage
23
+ import io.fabric8.kubernetes.api.model.HasMetadata
24
+ import io.fabric8.kubernetes.client.KubernetesClient
25
+ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil
26
+ import io.fabric8.kubernetes.client.utils.Serialization
27
+ import java.util.concurrent.locks.ReentrantLock
28
+ import kotlin.concurrent.withLock
29
+
30
+ open class EditorResource (
31
+ /* * for testing purposes */
32
+ private var resource : HasMetadata ,
33
+ private val resourceModel : IResourceModel ,
34
+ private val resourceChangedListener : IResourceModelListener ? ,
35
+ // for mocking purposes
36
+ private val clusterResourceFactory : (resource: HasMetadata , context: IActiveContext <out HasMetadata , out KubernetesClient >? ) -> ClusterResource ? =
37
+ ClusterResource .Factory : :create
38
+ ) {
39
+ var disposed: Boolean = false
40
+ /* * for testing purposes */
41
+ private var state: EditorResourceState ? = null
42
+ /* * for testing purposes */
43
+ protected open val clusterResource: ClusterResource ? = createClusterResource(resource)
44
+
45
+ private var lastPushedPulled: HasMetadata ? = Serialization .clone(resource)
46
+ private var resourceVersion: String? = resource.metadata.resourceVersion
47
+
48
+ private val resourceChangeMutex = ReentrantLock ()
49
+
50
+ /* *
51
+ * Sets the resource to this instance. Only modified versions of the same resource are processed.
52
+ * Will do nothing if the given resource is a different resource in name, namespace, kind etc.
53
+ *
54
+ * @param new the new resource that should be set to this editor resource
55
+ *
56
+ * @see isSameResource
57
+ */
58
+ fun setResource (new : HasMetadata ) {
59
+ resourceChangeMutex.withLock {
60
+ val existing = this .resource
61
+ if (new.isSameResource(existing)
62
+ && ! areEqual(new, existing)) {
63
+ this .resource = new
64
+ }
65
+ }
66
+ }
67
+
68
+ /* *
69
+ * Returns the resource that is edited.
70
+ *
71
+ * @return the resource that is edited
72
+ */
73
+ fun getResource (): HasMetadata {
74
+ return resourceChangeMutex.withLock {
75
+ resource
76
+ }
77
+ }
78
+
79
+ fun getResourceOnCluster (): HasMetadata ? {
80
+ return clusterResource?.pull(true )
81
+ }
82
+
83
+ /* * for testing purposes */
84
+ protected open fun setState (state : EditorResourceState ? ) {
85
+ resourceChangeMutex.withLock {
86
+ this .state = state
87
+ }
88
+ }
89
+
90
+ /* *
91
+ * Returns the state of this editor resource.
92
+ *
93
+ * @return the state of this editor resource.
94
+ */
95
+ fun getState (): EditorResourceState {
96
+ return resourceChangeMutex.withLock {
97
+ val state = getState(resource, state)
98
+ this .state = state
99
+ state
100
+ }
101
+ }
102
+
103
+ private fun getState (resource : HasMetadata , existingState : EditorResourceState ? ): EditorResourceState {
104
+ val isOutdated = isOutdatedVersion()
105
+ val existsOnCluster = existsOnCluster()
106
+ return when {
107
+ existingState is Error ->
108
+ existingState
109
+
110
+ ! isConnected() ->
111
+ Error (" Error contacting cluster. Make sure it's reachable, current context set, etc." , null as String? )
112
+
113
+ ! hasName(resource)
114
+ && ! hasGenerateName(resource) ->
115
+ Error (" Resource has neither name nor generateName." , null as String? )
116
+
117
+ isDeleted() ->
118
+ DeletedOnCluster ()
119
+
120
+ ! existsOnCluster
121
+ || isModified(resource) ->
122
+ Modified (
123
+ existsOnCluster,
124
+ isOutdated
125
+ )
126
+
127
+ isOutdated ->
128
+ Outdated ()
129
+
130
+ existingState != null ->
131
+ existingState
132
+
133
+ else ->
134
+ Identical ()
135
+ }
136
+ }
137
+
138
+ fun pull (): EditorResourceState {
139
+ val state = try {
140
+ val cluster = clusterResource
141
+ ? : return Error (" Could not pull ${toKindAndName(resource)} " , " cluster not connected." )
142
+ val pulled = cluster.pull(true )
143
+ ? : return Error (" Could not pull ${toKindAndName(resource)} " , " resource not found on cluster." )
144
+ setResource(pulled)
145
+ setResourceVersion(KubernetesResourceUtil .getResourceVersion(pulled))
146
+ setLastPushedPulled(pulled)
147
+ Pulled ()
148
+ } catch (e: Exception ) {
149
+ logger<ResourceEditor >().warn(e)
150
+ Error (
151
+ " Could not pull ${toKindAndName(resource)} " ,
152
+ toMessage(e.cause)
153
+ )
154
+ }
155
+ setState(state)
156
+ return state
157
+ }
158
+
159
+ fun push (): EditorResourceState {
160
+ val state = try {
161
+ val cluster = clusterResource
162
+ ? : return Error (
163
+ " Could not push ${toKindAndName(resource)} to cluster." ,
164
+ " Not connected."
165
+ )
166
+ val updatedResource = cluster.push(resource)
167
+ setResourceVersion(KubernetesResourceUtil .getResourceVersion(updatedResource))
168
+ setLastPushedPulled(resource) // store resource that is pushed, not resource returned from cluster
169
+ createPushedState(resource, cluster.exists())
170
+ } catch (e: Exception ) {
171
+ Error (" Could not push ${toKindAndName(resource)} to cluster." , e)
172
+ }
173
+ setState(state)
174
+ return state
175
+ }
176
+
177
+ private fun createPushedState (resource : HasMetadata ? , exists : Boolean ): EditorResourceState {
178
+ return if (resource != null ) {
179
+ if (exists) {
180
+ Updated ()
181
+ } else {
182
+ Created ()
183
+ }
184
+ } else {
185
+ Error (
186
+ " Could not push resource to cluster." ,
187
+ " No resource present."
188
+ )
189
+ }
190
+ }
191
+
192
+ private fun setResourceVersion (version : String? ) {
193
+ resourceChangeMutex.withLock {
194
+ this .resourceVersion = version
195
+ }
196
+ }
197
+
198
+ /* * for testing purposes */
199
+ protected open fun getResourceVersion (): String? {
200
+ return resourceChangeMutex.withLock {
201
+ this .resourceVersion
202
+ }
203
+ }
204
+
205
+ /* * for testing purposes */
206
+ protected open fun setLastPushedPulled (resource : HasMetadata ? ) {
207
+ resourceChangeMutex.withLock {
208
+ lastPushedPulled = resource
209
+ }
210
+ }
211
+
212
+ /* * for testing purposes */
213
+ protected open fun getLastPushedPulled (): HasMetadata ? {
214
+ return resourceChangeMutex.withLock {
215
+ lastPushedPulled
216
+ }
217
+ }
218
+
219
+ fun isOutdatedVersion (): Boolean {
220
+ val version = resourceChangeMutex.withLock {
221
+ resourceVersion
222
+ }
223
+ return true == clusterResource?.isOutdatedVersion(version)
224
+ }
225
+
226
+ /* *
227
+ * Returns `true` if the given resource has changes that don't exist in the resource
228
+ * that was last pulled/pushed from/to the cluster.
229
+ * The following properties are not taken into account:
230
+ * * [io.fabric8.kubernetes.api.model.ObjectMeta.resourceVersion]
231
+ * * [io.fabric8.kubernetes.api.model.ObjectMeta.uid]
232
+ *
233
+ * @return true if the resource is not equal to the resource that was pulled from the cluster
234
+ */
235
+ private fun isModified (resource : HasMetadata ): Boolean {
236
+ return ! areEqual(resource, getLastPushedPulled())
237
+ }
238
+
239
+ private fun isDeleted (): Boolean {
240
+ return true == clusterResource?.isDeleted()
241
+ }
242
+
243
+ private fun existsOnCluster (): Boolean {
244
+ return true == clusterResource?.exists()
245
+ }
246
+
247
+ private fun isConnected (): Boolean {
248
+ return clusterResource != null
249
+ }
250
+
251
+ fun watch () {
252
+ clusterResource?.watch()
253
+ }
254
+
255
+ fun stopWatch () {
256
+ clusterResource?.stopWatch()
257
+ }
258
+
259
+ fun dispose () {
260
+ if (disposed) {
261
+ return
262
+ }
263
+ this .disposed = true
264
+ clusterResource?.close()
265
+ setLastPushedPulled(null )
266
+ setResourceVersion(null )
267
+ }
268
+
269
+ private fun createClusterResource (resource : HasMetadata ): ClusterResource ? {
270
+ val context = resourceModel.getCurrentContext()
271
+ return if (context != null ) {
272
+ val clusterResource = clusterResourceFactory.invoke(
273
+ resource,
274
+ context
275
+ )
276
+
277
+ val resourceChangeListener = resourceChangedListener
278
+ if (resourceChangeListener != null ) {
279
+ clusterResource?.addListener(resourceChangeListener)
280
+ }
281
+ clusterResource?.watch()
282
+ clusterResource
283
+ } else {
284
+ null
285
+ }
286
+ }
287
+
288
+ }
0 commit comments