]>
Commit | Line | Data |
---|---|---|
1 | """ | |
2 | Deploy and configure Keycloak for Teuthology | |
3 | """ | |
4 | import contextlib | |
5 | import logging | |
6 | import os | |
7 | ||
8 | from teuthology import misc as teuthology | |
9 | from teuthology import contextutil | |
10 | from teuthology.orchestra import run | |
11 | from teuthology.exceptions import ConfigError | |
12 | ||
13 | log = logging.getLogger(__name__) | |
14 | ||
15 | def get_keycloak_version(config): | |
16 | for client, client_config in config.items(): | |
17 | if 'keycloak_version' in client_config: | |
18 | keycloak_version = client_config.get('keycloak_version') | |
19 | return keycloak_version | |
20 | ||
21 | def get_keycloak_dir(ctx, config): | |
22 | keycloak_version = get_keycloak_version(config) | |
23 | current_version = 'keycloak-'+keycloak_version | |
24 | return '{tdir}/{ver}'.format(tdir=teuthology.get_testdir(ctx),ver=current_version) | |
25 | ||
26 | def run_in_keycloak_dir(ctx, client, config, args, **kwargs): | |
27 | return ctx.cluster.only(client).run( | |
28 | args=[ 'cd', get_keycloak_dir(ctx,config), run.Raw('&&'), ] + args, | |
29 | **kwargs | |
30 | ) | |
31 | ||
32 | def get_toxvenv_dir(ctx): | |
33 | return ctx.tox.venv_path | |
34 | ||
35 | def toxvenv_sh(ctx, remote, args, **kwargs): | |
36 | activate = get_toxvenv_dir(ctx) + '/bin/activate' | |
37 | return remote.sh(['source', activate, run.Raw('&&')] + args, **kwargs) | |
38 | ||
39 | @contextlib.contextmanager | |
40 | def install_packages(ctx, config): | |
41 | """ | |
42 | Downloading the two required tar files | |
43 | 1. Keycloak | |
44 | 2. Wildfly (Application Server) | |
45 | """ | |
46 | assert isinstance(config, dict) | |
47 | log.info('Installing packages for Keycloak...') | |
48 | ||
49 | for (client, _) in config.items(): | |
50 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
51 | test_dir=teuthology.get_testdir(ctx) | |
52 | current_version = get_keycloak_version(config) | |
53 | link1 = 'https://downloads.jboss.org/keycloak/'+current_version+'/keycloak-'+current_version+'.tar.gz' | |
54 | toxvenv_sh(ctx, remote, ['wget', link1]) | |
55 | ||
56 | file1 = 'keycloak-'+current_version+'.tar.gz' | |
57 | toxvenv_sh(ctx, remote, ['tar', '-C', test_dir, '-xvzf', file1]) | |
58 | ||
59 | link2 ='https://downloads.jboss.org/keycloak/'+current_version+'/adapters/keycloak-oidc/keycloak-wildfly-adapter-dist-'+current_version+'.tar.gz' | |
60 | toxvenv_sh(ctx, remote, ['cd', '{tdir}'.format(tdir=get_keycloak_dir(ctx,config)), run.Raw('&&'), 'wget', link2]) | |
61 | ||
62 | file2 = 'keycloak-wildfly-adapter-dist-'+current_version+'.tar.gz' | |
63 | toxvenv_sh(ctx, remote, ['tar', '-C', '{tdir}'.format(tdir=get_keycloak_dir(ctx,config)), '-xvzf', '{tdr}/{file}'.format(tdr=get_keycloak_dir(ctx,config),file=file2)]) | |
64 | ||
65 | try: | |
66 | yield | |
67 | finally: | |
68 | log.info('Removing packaged dependencies of Keycloak...') | |
69 | for client in config: | |
70 | ctx.cluster.only(client).run( | |
71 | args=['rm', '-rf', '{tdir}'.format(tdir=get_keycloak_dir(ctx,config))], | |
72 | ) | |
73 | ||
74 | @contextlib.contextmanager | |
75 | def build(ctx,config): | |
76 | """ | |
77 | Build process which needs to be done before starting a server. | |
78 | """ | |
79 | assert isinstance(config, dict) | |
80 | log.info('Building Keycloak...') | |
81 | for (client,_) in config.items(): | |
82 | run_in_keycloak_dir(ctx, client, config,['cd', 'bin', run.Raw('&&'), './jboss-cli.sh', '--file=adapter-elytron-install-offline.cli']) | |
83 | try: | |
84 | yield | |
85 | finally: | |
86 | pass | |
87 | ||
88 | @contextlib.contextmanager | |
89 | def run_keycloak(ctx,config): | |
90 | """ | |
91 | This includes two parts: | |
92 | 1. Adding a user to keycloak which is actually used to log in when we start the server and check in browser. | |
93 | 2. Starting the server. | |
94 | """ | |
95 | assert isinstance(config, dict) | |
96 | log.info('Bringing up Keycloak...') | |
97 | for (client,_) in config.items(): | |
98 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
99 | ||
100 | ctx.cluster.only(client).run( | |
101 | args=[ | |
102 | '{tdir}/bin/add-user-keycloak.sh'.format(tdir=get_keycloak_dir(ctx,config)), | |
103 | '-r', 'master', | |
104 | '-u', 'admin', | |
105 | '-p', 'admin', | |
106 | ], | |
107 | ) | |
108 | ||
109 | toxvenv_sh(ctx, remote, ['cd', '{tdir}/bin'.format(tdir=get_keycloak_dir(ctx,config)), run.Raw('&&'), './standalone.sh', run.Raw('&'), 'exit']) | |
110 | try: | |
111 | yield | |
112 | finally: | |
113 | log.info('Stopping Keycloak Server...') | |
114 | ||
115 | for (client, _) in config.items(): | |
116 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
117 | toxvenv_sh(ctx, remote, ['cd', '{tdir}/bin'.format(tdir=get_keycloak_dir(ctx,config)), run.Raw('&&'), './jboss-cli.sh', '--connect', 'command=:shutdown']) | |
118 | ||
119 | @contextlib.contextmanager | |
120 | def run_admin_cmds(ctx,config): | |
121 | """ | |
122 | Running Keycloak Admin commands(kcadm commands) in order to get the token, aud value, thumbprint and realm name. | |
123 | """ | |
124 | assert isinstance(config, dict) | |
125 | log.info('Running admin commands...') | |
126 | for (client,_) in config.items(): | |
127 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
128 | ||
129 | remote.run( | |
130 | args=[ | |
131 | '{tdir}/bin/kcadm.sh'.format(tdir=get_keycloak_dir(ctx,config)), | |
132 | 'config', 'credentials', | |
133 | '--server', 'http://localhost:8080/auth', | |
134 | '--realm', 'master', | |
135 | '--user', 'admin', | |
136 | '--password', 'admin', | |
137 | '--client', 'admin-cli', | |
138 | ], | |
139 | ) | |
140 | ||
141 | realm_name='demorealm' | |
142 | realm='realm={}'.format(realm_name) | |
143 | ||
144 | remote.run( | |
145 | args=[ | |
146 | '{tdir}/bin/kcadm.sh'.format(tdir=get_keycloak_dir(ctx,config)), | |
147 | 'create', 'realms', | |
148 | '-s', realm, | |
149 | '-s', 'enabled=true', | |
150 | '-s', 'accessTokenLifespan=1800', | |
151 | '-o', | |
152 | ], | |
153 | ) | |
154 | ||
155 | client_name='my_client' | |
156 | client='clientId={}'.format(client_name) | |
157 | ||
158 | remote.run( | |
159 | args=[ | |
160 | '{tdir}/bin/kcadm.sh'.format(tdir=get_keycloak_dir(ctx,config)), | |
161 | 'create', 'clients', | |
162 | '-r', realm_name, | |
163 | '-s', client, | |
164 | '-s', 'redirectUris=["http://localhost:8080/myapp/*"]', | |
165 | ], | |
166 | ) | |
167 | ||
168 | ans1= toxvenv_sh(ctx, remote, | |
169 | [ | |
170 | 'cd', '{tdir}/bin'.format(tdir=get_keycloak_dir(ctx,config)), run.Raw('&&'), | |
171 | './kcadm.sh', 'get', 'clients', | |
172 | '-r', realm_name, | |
173 | '-F', 'id,clientId', run.Raw('|'), | |
174 | 'jq', '-r', '.[] | select (.clientId == "my_client") | .id' | |
175 | ]) | |
176 | ||
177 | pre0=ans1.rstrip() | |
178 | pre1="clients/{}".format(pre0) | |
179 | ||
180 | remote.run( | |
181 | args=[ | |
182 | '{tdir}/bin/kcadm.sh'.format(tdir=get_keycloak_dir(ctx,config)), | |
183 | 'update', pre1, | |
184 | '-r', realm_name, | |
185 | '-s', 'enabled=true', | |
186 | '-s', 'serviceAccountsEnabled=true', | |
187 | '-s', 'redirectUris=["http://localhost:8080/myapp/*"]', | |
188 | ], | |
189 | ) | |
190 | ||
191 | ans2= pre1+'/client-secret' | |
192 | ||
193 | out2= toxvenv_sh(ctx, remote, | |
194 | [ | |
195 | 'cd', '{tdir}/bin'.format(tdir=get_keycloak_dir(ctx,config)), run.Raw('&&'), | |
196 | './kcadm.sh', 'get', ans2, | |
197 | '-r', realm_name, | |
198 | '-F', 'value' | |
199 | ]) | |
200 | ||
201 | ans0= '{client}:{secret}'.format(client=client_name,secret=out2[15:51]) | |
202 | ans3= 'client_secret={}'.format(out2[15:51]) | |
203 | clientid='client_id={}'.format(client_name) | |
204 | ||
205 | out3= toxvenv_sh(ctx, remote, | |
206 | [ | |
207 | 'curl', '-k', '-v', | |
208 | '-X', 'POST', | |
209 | '-H', 'Content-Type:application/x-www-form-urlencoded', | |
210 | '-d', 'scope=openid', | |
211 | '-d', 'grant_type=client_credentials', | |
212 | '-d', clientid, | |
213 | '-d', ans3, | |
214 | 'http://localhost:8080/auth/realms/'+realm_name+'/protocol/openid-connect/token', run.Raw('|'), | |
215 | 'jq', '-r', '.access_token' | |
216 | ]) | |
217 | ||
218 | pre2=out3.rstrip() | |
219 | acc_token= 'token={}'.format(pre2) | |
220 | ans4= '{}'.format(pre2) | |
221 | ||
222 | out4= toxvenv_sh(ctx, remote, | |
223 | [ | |
224 | 'curl', '-k', '-v', | |
225 | '-X', 'GET', | |
226 | '-H', 'Content-Type:application/x-www-form-urlencoded', | |
227 | 'http://localhost:8080/auth/realms/'+realm_name+'/protocol/openid-connect/certs', run.Raw('|'), | |
228 | 'jq', '-r', '.keys[].x5c[]' | |
229 | ]) | |
230 | ||
231 | pre3=out4.rstrip() | |
232 | cert_value='{}'.format(pre3) | |
233 | start_value= "-----BEGIN CERTIFICATE-----\n" | |
234 | end_value= "\n-----END CERTIFICATE-----" | |
235 | user_data="" | |
236 | user_data+=start_value | |
237 | user_data+=cert_value | |
238 | user_data+=end_value | |
239 | ||
240 | remote.write_file( | |
241 | path='{tdir}/bin/certificate.crt'.format(tdir=get_keycloak_dir(ctx,config)), | |
242 | data=user_data | |
243 | ) | |
244 | ||
245 | out5= toxvenv_sh(ctx, remote, | |
246 | [ | |
247 | 'openssl', 'x509', | |
248 | '-in', '{tdir}/bin/certificate.crt'.format(tdir=get_keycloak_dir(ctx,config)), | |
249 | '--fingerprint', '--noout', '-sha1' | |
250 | ]) | |
251 | ||
252 | pre_ans= '{}'.format(out5[17:76]) | |
253 | ans5="" | |
254 | ||
255 | for character in pre_ans: | |
256 | if(character!=':'): | |
257 | ans5+=character | |
258 | ||
259 | out6= toxvenv_sh(ctx, remote, | |
260 | [ | |
261 | 'curl', '-k', '-v', | |
262 | '-X', 'POST', | |
263 | '-u', ans0, | |
264 | '-d', acc_token, | |
265 | 'http://localhost:8080/auth/realms/'+realm_name+'/protocol/openid-connect/token/introspect', run.Raw('|'), | |
266 | 'jq', '-r', '.aud' | |
267 | ]) | |
268 | ||
269 | ans6=out6.rstrip() | |
270 | ||
271 | os.environ['TOKEN']=ans4 | |
272 | os.environ['THUMBPRINT']=ans5 | |
273 | os.environ['AUD']=ans6 | |
274 | os.environ['KC_REALM']=realm_name | |
275 | ||
276 | try: | |
277 | yield | |
278 | finally: | |
279 | log.info('Removing certificate.crt file...') | |
280 | for (client,_) in config.items(): | |
281 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
282 | remote.run( | |
283 | args=['rm', '-f', | |
284 | '{tdir}/bin/certificate.crt'.format(tdir=get_keycloak_dir(ctx,config)), | |
285 | ], | |
286 | ) | |
287 | ||
288 | @contextlib.contextmanager | |
289 | def task(ctx,config): | |
290 | """ | |
291 | To run keycloak the prerequisite is to run the tox task. Following is the way how to run | |
292 | tox and then keycloak:: | |
293 | ||
294 | tasks: | |
295 | - tox: [ client.0 ] | |
296 | - keycloak: | |
297 | client.0: | |
298 | keycloak_version: 11.0.0 | |
299 | ||
300 | To pass extra arguments to nose (e.g. to run a certain test):: | |
301 | ||
302 | tasks: | |
303 | - tox: [ client.0 ] | |
304 | - keycloak: | |
305 | client.0: | |
306 | keycloak_version: 11.0.0 | |
307 | - s3tests: | |
308 | client.0: | |
309 | extra_attrs: ['webidentity_test'] | |
310 | ||
311 | """ | |
312 | assert config is None or isinstance(config, list) \ | |
313 | or isinstance(config, dict), \ | |
314 | "task keycloak only supports a list or dictionary for configuration" | |
315 | ||
316 | if not hasattr(ctx, 'tox'): | |
317 | raise ConfigError('keycloak must run after the tox task') | |
318 | ||
319 | all_clients = ['client.{id}'.format(id=id_) | |
320 | for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')] | |
321 | if config is None: | |
322 | config = all_clients | |
323 | if isinstance(config, list): | |
324 | config = dict.fromkeys(config) | |
325 | ||
326 | log.debug('Keycloak config is %s', config) | |
327 | ||
328 | with contextutil.nested( | |
329 | lambda: install_packages(ctx=ctx, config=config), | |
330 | lambda: build(ctx=ctx, config=config), | |
331 | lambda: run_keycloak(ctx=ctx, config=config), | |
332 | lambda: run_admin_cmds(ctx=ctx, config=config), | |
333 | ): | |
334 | yield | |
335 |