1
1
from collections import defaultdict
2
- from sys import argv
2
+ import json
3
+ import random
3
4
import typing as t
4
- from fastapi import Depends , FastAPI , HTTPException
5
+ from fastapi import Depends , FastAPI , Header , HTTPException , Response
5
6
from fastapi .security .http import HTTPAuthorizationCredentials , HTTPBearer
6
7
from fastapi .middleware .cors import CORSMiddleware
7
8
from sqlalchemy .orm import Session
@@ -86,6 +87,23 @@ def get_drv(drv_hash: str,
86
87
def get_drv_recap (drv_hash : str , db : Session = Depends (get_db )) -> schemas .DerivationAttestation :
87
88
return get_drv_recap_or_404 (db , drv_hash )
88
89
90
+ # Suggested rebuilds
91
+ @app .get ("/reports/{name}/suggested" )
92
+ def derivations_suggested_for_rebuilding (
93
+ name : str ,
94
+ token : str = Depends (get_token ),
95
+ db : Session = Depends (get_db ),
96
+ ):
97
+ report = crud .report (db , name )
98
+ if report == None :
99
+ raise HTTPException (status_code = 404 , detail = "Report not found" )
100
+ paths = report_out_paths (report )
101
+
102
+ user = crud .get_user_with_token (db , token )
103
+ suggestions = crud .suggest (db , paths , user )
104
+ random .shuffle (suggestions )
105
+ return suggestions [:50 ]
106
+
89
107
@app .post ("/attestation/{drv_hash}" )
90
108
def record_attestation (
91
109
drv_hash : str ,
@@ -102,4 +120,204 @@ def record_attestation(
102
120
"Attestation accepted"
103
121
}
104
122
123
+ @app .get ("/attestations/by-output/{output_path}" )
124
+ def attestations_by_out (output_path : str , db : Session = Depends (get_db )):
125
+ return db .query (models .Attestation ).filter_by (output_path = "/nix/store/" + output_path ).all ()
126
+
127
+ def report_out_paths (report ):
128
+ paths = []
129
+ for component in report ['components' ]:
130
+ for prop in component ['properties' ]:
131
+ if prop ['name' ] == "nix:out_path" :
132
+ paths .append (prop ['value' ])
133
+ return paths
134
+
135
+ @app .get ("/reports" )
136
+ def reports (db : Session = Depends (get_db )):
137
+ reports = db .query (models .Report ).all ()
138
+ names = []
139
+ for report in reports :
140
+ names .append (report .name )
141
+ return names
142
+
143
+ def printtree (root , deps , results , cur_indent = 0 , seen = None ):
144
+ if seen is None :
145
+ seen = {}
146
+ if root in seen :
147
+ return " " * cur_indent + "...\n "
148
+ seen [root ] = True ;
149
+
150
+ result = " " * cur_indent + root [11 :];
151
+ if root in results :
152
+ result = result + " " + results [root ] + "\n "
153
+ else :
154
+ result = result + "\n "
155
+ for dep in deps :
156
+ if dep ['ref' ] == root and 'dependsOn' in dep :
157
+ for d in dep ['dependsOn' ]:
158
+ result += printtree (d , deps , results , cur_indent + 2 , seen )
159
+ #result = result + "\n " + d
160
+ return result
161
+
162
+ def htmltree (root , deps , results ):
163
+ def icon (result ):
164
+ if result == "No builds" :
165
+ return "❔ "
166
+ elif result == "One build" :
167
+ return "❎ "
168
+ elif result == "Partially reproduced" :
169
+ return "❕ "
170
+ elif result == "Successfully reproduced" :
171
+ return "✅ "
172
+ elif result == "Consistently nondeterministic" :
173
+ return "❌ "
174
+ else :
175
+ return ""
176
+ def generatetree (root , seen ):
177
+ if root in seen :
178
+ return f'<summary title="{ root } ">...</summary>'
179
+ seen [root ] = True ;
180
+
181
+ result = f'<summary title="{ root } ">'
182
+ if root in results :
183
+ result = result + f'<span title="{ results [root ]} ">' + icon (results [root ]) + "</span>" + root [44 :] + " "
184
+ else :
185
+ result = result + root [44 :]
186
+ result = result + "</summary>\n "
187
+ result = result + "<ul>"
188
+ for dep in deps :
189
+ if dep ['ref' ] == root and 'dependsOn' in dep :
190
+ for d in dep ['dependsOn' ]:
191
+ result += f'<li><details class="{ d } " open>'
192
+ result += generatetree (d , seen )
193
+ result += "</details></li>"
194
+ result = result + "</ul>"
195
+ return result
196
+ tree = generatetree (root , {})
197
+ return '''
198
+ <html>
199
+ <head>
200
+ <style>
201
+ .tree{
202
+ --spacing : 1.5rem;
203
+ --radius : 8px;
204
+ }
205
+
206
+ .tree li{
207
+ display : block;
208
+ position : relative;
209
+ padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
210
+ }
211
+
212
+ .tree ul{
213
+ margin-left : calc(var(--radius) - var(--spacing));
214
+ padding-left : 0;
215
+ }
216
+
217
+ .tree ul li{
218
+ border-left : 2px solid #ddd;
219
+ }
220
+
221
+ .tree ul li:last-child{
222
+ border-color : transparent;
223
+ }
224
+
225
+ .tree ul li::before{
226
+ content : '';
227
+ display : block;
228
+ position : absolute;
229
+ top : calc(var(--spacing) / -2);
230
+ left : -2px;
231
+ width : calc(var(--spacing) + 2px);
232
+ height : calc(var(--spacing) + 1px);
233
+ border : solid #ddd;
234
+ border-width : 0 0 2px 2px;
235
+ }
236
+
237
+ .tree summary{
238
+ display : block;
239
+ cursor : pointer;
240
+ }
241
+
242
+ .tree summary::marker,
243
+ .tree summary::-webkit-details-marker{
244
+ display : none;
245
+ }
246
+
247
+ .tree summary:focus{
248
+ outline : none;
249
+ }
250
+
251
+ .tree summary:focus-visible{
252
+ outline : 1px dotted #000;
253
+ }
254
+
255
+ .tree li::after,
256
+ .tree summary::before{
257
+ content : '';
258
+ display : block;
259
+ position : absolute;
260
+ top : calc(var(--spacing) / 2 - var(--radius));
261
+ left : calc(var(--spacing) - var(--radius) - 1px);
262
+ width : calc(2 * var(--radius));
263
+ height : calc(2 * var(--radius));
264
+ border-radius : 50%;
265
+ background : #ddd;
266
+ }
267
+
268
+ </style>
269
+ </head>
270
+ ''' + f'''
271
+ <body>
272
+ <ul class="tree">
273
+ <li>
274
+ { tree }
275
+ </li>
276
+ </ul>
277
+ </body>
278
+ </html>
279
+ '''
280
+
281
+ @app .get ("/reports/{name}" )
282
+ def report (
283
+ name : str ,
284
+ accept : t .Optional [str ] = Header (default = "*/*" ),
285
+ db : Session = Depends (get_db ),
286
+ ):
287
+ report = crud .report (db , name )
288
+ if report == None :
289
+ raise HTTPException (status_code = 404 , detail = "Report not found" )
290
+
291
+ if 'application/vnd.cyclonedx+json' in accept :
292
+ return Response (
293
+ content = json .dumps (report ),
294
+ media_type = 'application/vnd.cyclonedx+json' )
295
+
296
+ paths = report_out_paths (report )
105
297
298
+ root = report ['metadata' ]['component' ]['bom-ref' ]
299
+ results = crud .path_summaries (db , paths )
300
+
301
+ if 'text/html' in accept :
302
+ return Response (
303
+ content = htmltree (root , report ['dependencies' ], results ),
304
+ media_type = 'text/html' )
305
+ else :
306
+ return Response (
307
+ content = printtree (root , report ['dependencies' ], results ),
308
+ media_type = 'text/plain' )
309
+
310
+ @app .put ("/reports/{name}" )
311
+ def define_report (
312
+ name : str ,
313
+ definition : schemas .ReportDefinition ,
314
+ token : str = Depends (get_token ),
315
+ db : Session = Depends (get_db ),
316
+ ):
317
+ user = crud .get_user_with_token (db , token )
318
+ if user == None :
319
+ raise HTTPException (status_code = 401 , detail = "User not found" )
320
+ crud .define_report (db , name , definition .root )
321
+ return {
322
+ "Report defined"
323
+ }
0 commit comments