1
1
package org .csanchez .jenkins .plugins .kubernetes ;
2
2
3
- import com .google .common .base .Objects ;
4
- import com .google .common .cache .Cache ;
5
- import com .google .common .cache .CacheBuilder ;
6
- import com .google .common .cache .RemovalListener ;
7
-
8
- import io .fabric8 .kubernetes .client .KubernetesClient ;
9
-
10
3
import java .io .IOException ;
11
4
import java .security .KeyStoreException ;
12
5
import java .security .NoSuchAlgorithmException ;
13
6
import java .security .UnrecoverableKeyException ;
14
7
import java .security .cert .CertificateEncodingException ;
8
+ import java .util .ArrayList ;
9
+ import java .util .Collections ;
10
+ import java .util .HashSet ;
11
+ import java .util .Iterator ;
12
+ import java .util .List ;
13
+ import java .util .Set ;
15
14
import java .util .concurrent .TimeUnit ;
15
+ import java .util .logging .Level ;
16
+ import java .util .logging .Logger ;
17
+
18
+ import com .google .common .base .Objects ;
19
+ import com .google .common .cache .Cache ;
20
+ import com .google .common .cache .CacheBuilder ;
21
+ import io .fabric8 .kubernetes .client .HttpClientAware ;
22
+ import io .fabric8 .kubernetes .client .KubernetesClient ;
23
+ import okhttp3 .Dispatcher ;
24
+ import okhttp3 .OkHttpClient ;
25
+
26
+ import hudson .Extension ;
27
+ import hudson .XmlFile ;
28
+ import hudson .model .AsyncPeriodicWork ;
29
+ import hudson .model .Saveable ;
30
+ import hudson .model .TaskListener ;
31
+ import hudson .model .listeners .SaveableListener ;
32
+ import jenkins .model .Jenkins ;
16
33
17
34
/**
18
35
* Manages the Kubernetes client creation per cloud
19
36
*/
20
37
final class KubernetesClientProvider {
21
38
22
- private static final Cache <String , Client > clients ;
23
-
24
- private static final Integer CACHE_SIZE ;
25
- private static final Integer CACHE_TTL ;
26
-
27
- static {
28
- CACHE_SIZE = Integer .getInteger ("org.csanchez.jenkins.plugins.kubernetes.clients.cacheSize" , 10 );
29
- CACHE_TTL = Integer .getInteger ("org.csanchez.jenkins.plugins.kubernetes.clients.cacheTtl" , 60 );
30
- clients = CacheBuilder .newBuilder ()
31
- .maximumSize (CACHE_SIZE )
32
- .expireAfterWrite (CACHE_TTL , TimeUnit .MINUTES )
33
- .removalListener ((RemovalListener <String , Client >) removalNotification -> {
34
- // https://google.github.io/guava/releases/23.0/api/docs/com/google/common/cache/RemovalNotification.html
35
- // A notification of the removal of a single entry. The key and/or value may be null if they were already garbage collected.
36
- if (removalNotification .getValue () != null ) {
37
- removalNotification .getValue ().getClient ().close ();
39
+ private static final Logger LOGGER = Logger .getLogger (KubernetesClientProvider .class .getName ());
40
+
41
+ private static final Integer CACHE_SIZE = Integer .getInteger ("org.csanchez.jenkins.plugins.kubernetes.clients.cacheSize" , 10 );
42
+
43
+ private static final List <KubernetesClient > expiredClients = Collections .synchronizedList (new ArrayList ());
44
+
45
+ private static final Cache <String , Client > clients = CacheBuilder
46
+ .newBuilder ()
47
+ .maximumSize (CACHE_SIZE )
48
+ .removalListener (rl -> {
49
+ LOGGER .log (Level .FINE , "{0} cache : Removing entry for {1}" , new Object [] {KubernetesClient .class .getSimpleName (), rl .getKey ()});
50
+ KubernetesClient client = ((Client ) rl .getValue ()).getClient ();
51
+ if (client != null ) {
52
+ if (client instanceof HttpClientAware ) {
53
+ if (!gracefulClose (client , ((HttpClientAware ) client ).getHttpClient ())) {
54
+ expiredClients .add (client );
55
+ }
56
+ } else {
57
+ LOGGER .log (Level .WARNING , "{0} is not {1}, forcing close" , new Object [] {client .toString (), HttpClientAware .class .getSimpleName ()});
58
+ client .close ();
38
59
}
39
- })
40
- .build ();
41
- }
60
+ }
61
+
62
+ })
63
+ .build ();
42
64
43
65
private KubernetesClientProvider () {
44
66
}
45
67
46
68
static KubernetesClient createClient (KubernetesCloud cloud ) throws NoSuchAlgorithmException , UnrecoverableKeyException ,
47
69
KeyStoreException , IOException , CertificateEncodingException {
48
-
49
- final int validity = Objects .hashCode (cloud .getServerUrl (), cloud .getNamespace (), cloud .getServerCertificate (),
50
- cloud .getCredentialsId (), cloud .isSkipTlsVerify (), cloud .getConnectTimeout (), cloud .getReadTimeout (),
51
- cloud .getMaxRequestsPerHostStr ());
52
- final Client c = clients .getIfPresent (cloud .getDisplayName ());
53
-
54
- if (c != null && validity == c .getValidity ()) {
55
- return c .getClient ();
56
- } else {
57
- // expire tha cache if any of these config options have changed
58
- if (c != null ) {
59
- c .client .close ();
60
- }
70
+ String displayName = cloud .getDisplayName ();
71
+ final Client c = clients .getIfPresent (displayName );
72
+ if (c == null ) {
61
73
KubernetesClient client = new KubernetesFactoryAdapter (cloud .getServerUrl (), cloud .getNamespace (),
62
74
cloud .getServerCertificate (), cloud .getCredentialsId (), cloud .isSkipTlsVerify (),
63
75
cloud .getConnectTimeout (), cloud .getReadTimeout (), cloud .getMaxRequestsPerHost ()).createClient ();
64
- clients .put (cloud .getDisplayName (), new Client (validity , client ));
65
-
76
+ clients .put (displayName , new Client (getValidity (cloud ), client ));
66
77
return client ;
67
78
}
79
+ return c .getClient ();
80
+ }
81
+
82
+ private static int getValidity (KubernetesCloud cloud ) {
83
+ return Objects .hashCode (cloud .getServerUrl (), cloud .getNamespace (), cloud .getServerCertificate (),
84
+ cloud .getCredentialsId (), cloud .isSkipTlsVerify (), cloud .getConnectTimeout (), cloud .getReadTimeout (),
85
+ cloud .getMaxRequestsPerHostStr ());
68
86
}
69
87
70
88
private static class Client {
@@ -85,4 +103,76 @@ public int getValidity() {
85
103
}
86
104
}
87
105
106
+ @ Extension
107
+ public static class PurgeExpiredKubernetesClients extends AsyncPeriodicWork {
108
+
109
+ public PurgeExpiredKubernetesClients () {
110
+ super ("Purge expired KubernetesClients" );
111
+ }
112
+
113
+ @ Override
114
+ public long getRecurrencePeriod () {
115
+ return TimeUnit .MINUTES .toMillis (1 );
116
+ }
117
+
118
+ @ Override
119
+ protected Level getNormalLoggingLevel () {
120
+ return Level .FINE ;
121
+ }
122
+
123
+ @ Override
124
+ protected void execute (TaskListener listener ) {
125
+ for (Iterator <KubernetesClient > it = expiredClients .iterator (); it .hasNext ();) {
126
+ KubernetesClient client = it .next ();
127
+ if (client instanceof HttpClientAware ) {
128
+ if (gracefulClose (client , ((HttpClientAware ) client ).getHttpClient ())) {
129
+ it .remove ();
130
+ }
131
+ } else {
132
+ LOGGER .log (Level .WARNING , "{0} is not {1}, forcing close" , new Object [] {client .toString (), HttpClientAware .class .getSimpleName ()});
133
+ client .close ();
134
+ it .remove ();
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ private static boolean gracefulClose (KubernetesClient client , OkHttpClient httpClient ) {
141
+ Dispatcher dispatcher = httpClient .dispatcher ();
142
+ // Remove the client if there are no more users
143
+ int runningCallsCount = dispatcher .runningCallsCount ();
144
+ int queuedCallsCount = dispatcher .queuedCallsCount ();
145
+ if (runningCallsCount == 0 && queuedCallsCount == 0 ) {
146
+ LOGGER .log (Level .INFO , "Closing {0}" , client .toString ());
147
+ client .close ();
148
+ return true ;
149
+ } else {
150
+ LOGGER .log (Level .INFO , "Not closing {0}: there are still running ({1}) or queued ({2}) calls" , new Object [] {client .toString (), runningCallsCount , queuedCallsCount });
151
+ return false ;
152
+ }
153
+ }
154
+
155
+ @ Extension
156
+ public static class SaveableListenerImpl extends SaveableListener {
157
+ @ Override
158
+ public void onChange (Saveable o , XmlFile file ) {
159
+ if (o instanceof Jenkins ) {
160
+ Jenkins jenkins = (Jenkins ) o ;
161
+ Set <String > cloudDisplayNames = new HashSet <>(clients .asMap ().keySet ());
162
+ for (KubernetesCloud cloud : jenkins .clouds .getAll (KubernetesCloud .class )) {
163
+ String displayName = cloud .getDisplayName ();
164
+ Client client = clients .getIfPresent (displayName );
165
+ if (client != null && client .getValidity () == getValidity (cloud )) {
166
+ cloudDisplayNames .remove (displayName );
167
+ }
168
+ }
169
+ // Remove missing / invalid clients
170
+ for (String displayName : cloudDisplayNames ) {
171
+ clients .invalidate (displayName );
172
+ }
173
+ }
174
+ super .onChange (o , file );
175
+ }
176
+ }
177
+
88
178
}
0 commit comments