1+ #!/usr/bin/env python3
2+ # Author: Corey Osman <[email protected] > 3+ # Purpose: Geneate a list in specified format of all the linkerd charts by app version
4+ # Usage: ./generate_release_matrix.py [--format=json|table|yaml] [--update_repo]
5+ # Notes: Help determine which charts go with which app versions
6+ # Notes: This is primary aimed at the stable release only, although a slight modification
7+ # could yeild support for edge and Enterprise releases too. Right now edge versions
8+ # follow date schemes and the manual mappings do not work
9+
10+
11+ import subprocess
12+ import json
13+ import yaml
14+ import argparse
15+
16+
17+ charts = [
18+ # "linkerd2", # This makes the search return results for everything
19+ "linkerd2-cni" ,
20+ "linkerd-viz" ,
21+ "linkerd-control-plane" ,
22+ "linkerd-jaeger" ,
23+ "linkerd-multicluster" ,
24+ "linkerd-failover" ,
25+ ]
26+
27+ # these versions are old and do not have the proper mappings anyways
28+ ignored_y_versions = [6 , 7 , 8 , 9 ]
29+
30+ # Manually map in the old chart, otherwise we get duplicates mixed in
31+ linkerd2_map = {
32+ '2.11' : {
33+ "linkerd2" : {
34+ "chart_name" : "linkerd2" ,
35+ "chart_version" : "2.11.5" ,
36+ "chart_url" : "https://artifacthub.io/packages/helm/linkerd2/linkerd2/2.11.5"
37+ }
38+ },
39+ '2.10' : {
40+ "linkerd2" : {
41+ "chart_name" : "linkerd2" ,
42+ "chart_version" : "2.10.2" ,
43+ "chart_url" : "https://artifacthub.io/packages/helm/linkerd2/linkerd2/2.10.2"
44+ }
45+ }
46+ }
47+ # Manually map in the crds because there is no app version associated with them
48+ crds_map = {
49+ "2.12" : {
50+ "linkerd-crds" : {
51+ "chart_name" : "linkerd-crds" ,
52+ "chart_version" : "1.6.1" ,
53+ "chart_url" : "https://artifacthub.io/packages/helm/linkerd2/linkerd-crds/1.6.1"
54+ }
55+ },
56+ "2.13" : {
57+ "linkerd-crds" : {
58+ "chart_name" : "linkerd-crds" ,
59+ "chart_version" : "1.6.1" ,
60+ "chart_url" : "https://artifacthub.io/packages/helm/linkerd2/linkerd-crds/1.6.1"
61+ }
62+ },
63+ "2.14" : {
64+ "linkerd-crds" : {
65+ "chart_name" : "linkerd-crds" ,
66+ "chart_version" : "1.8.0" ,
67+ "chart_url" : "https://artifacthub.io/packages/helm/linkerd2/linkerd-crds/1.8.0"
68+ }
69+ },
70+ }
71+
72+
73+ def find_newest_versions (versions ):
74+ """
75+ Finds the newest version with the highest X value for a given Y version
76+
77+ Parameters:
78+ versions (list): A list of version objects
79+ Example: [('linkerd2/linkerd2', '2.11.5', 'stable-2.11.5'), ('linkerd2/linkerd2', '2.11.4', 'stable-2.11.4')
80+
81+ Returns:
82+ list: A list of version objects
83+ Example: [('linkerd2/linkerd2', '2.11.5', 'stable-2.11.5'),
84+ ('linkerd2/linkerd2', '2.10.2', 'stable-2.10.2'),
85+ ('linkerd2/linkerd2', '30.12.1', 'stable-2.14.3'),
86+ ('linkerd2/linkerd2', '30.8.5', 'stable-2.13.7'),
87+ ('linkerd2/linkerd2', '30.3.8', 'stable-2.12.6'),
88+ ('linkerd2/linkerd2', '2.11.5', 'stable-2.11.5'),
89+ ('linkerd2/linkerd2', '2.10.2', 'stable-2.10.2')]
90+
91+ """
92+ winners = {}
93+ for entry in versions :
94+ _ , _ , version , _ = entry
95+ try :
96+ x , y , z = map (int , version .split ("-" )[1 ].split ("." ))
97+ if not y in ignored_y_versions :
98+ current_winner = winners .get (
99+ f"{ x } .{ y } .Z" , {"x" : x , "y" : y , "z" : z , version : version }
100+ )
101+ if current_winner ["y" ] == y and z >= current_winner ["z" ]:
102+ # new winner
103+ winners [f"{ x } .{ y } .Z" ] = {"x" : x , "y" : y , "z" : z , "version" : version }
104+
105+ except IndexError :
106+ next
107+ except UnboundLocalError :
108+ next
109+
110+ latest_versions = [v ["version" ] for v in winners .values ()]
111+ common_versions = []
112+
113+ for version in versions :
114+ if version [2 ] in latest_versions :
115+ common_versions .append (version )
116+
117+ return common_versions
118+
119+
120+ def combine_charts_by_app_version (versions ):
121+ """
122+ Gathers all charts tuples under a single app version
123+
124+ Parameters:
125+ versions (list): Raw list of versions tuples
126+ [('linkerd2/linkerd-control-plane', '1.12.7', 'stable-2.13.7'),
127+ ('linkerd2/linkerd-control-plane', '1.16.4', 'stable-2.14.3'),
128+ ('linkerd2/linkerd-control-plane', '1.9.8', 'stable-2.12.6')]
129+
130+ Returns:
131+ dict: versions object after combing under an app version.
132+ {'stable-2.13.7':
133+ {'linkerd2-crds':
134+ {'chart_name': 'linkerd2/linkerd2-crds', 'chart_version': '1.6.1'},
135+ 'linkerd-jaeger': {
136+ 'chart_name': 'linkerd2/linkerd-jaeger', 'chart_version': '30.8.7'}
137+ }
138+ }
139+
140+ """
141+ combined_charts = {}
142+ for chart , version , app_version , link in versions :
143+ name = chart # chart.split("/")[1]
144+
145+ if not app_version :
146+ app_version = version
147+ if not combined_charts .get (app_version ):
148+ combined_charts [app_version ] = {}
149+
150+ combined_charts [app_version ][name ] = {
151+ "chart_name" : chart ,
152+ "chart_version" : version ,
153+ "chart_url" : link
154+ }
155+ # merge in the crds chart info if it exist for the version
156+ x , y , z = map (int , app_version .split ("-" )[1 ].split ("." ))
157+ try :
158+ if y > 11 and x == 2 :
159+ combined_charts [app_version ] = {** combined_charts [app_version ], ** crds_map [f"{ x } .{ y } " ]}
160+ elif x == 2 :
161+ combined_charts [app_version ] = {** combined_charts [app_version ], ** linkerd2_map [f"{ x } .{ y } " ]}
162+ except KeyError as e :
163+ print (e )
164+
165+ # sorted(combined_charts.items(), key=lambda x: x[0] != "stable-2.11.5")
166+ return combined_charts
167+
168+ def find_repo_name (release_type ):
169+ if release_type == "stable" :
170+ repo_name = "linkerd2"
171+ elif release_type == "edge" :
172+ repo_name = "linkerd2-edge"
173+ else :
174+ repo_name = "linkerd2"
175+
176+ return repo_name
177+
178+ def add_repo (release_type , helm_url , repo_name ):
179+ command = ["helm" , "repo" , "add" , repo_name , f"{ helm_url } /{ release_type } " ]
180+
181+ try :
182+ process = subprocess .Popen (
183+ command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True
184+ )
185+ stdout , stderr = process .communicate ()
186+
187+ except OSError as e :
188+ print (f"Error: { e } " )
189+
190+ return repo_name
191+
192+
193+ def update_repo (release_type , helm_url , repo_name ):
194+ add_repo (release_type , helm_url , repo_name )
195+
196+ command = ["helm" , "repo" , "update" , repo_name ]
197+
198+ try :
199+ process = subprocess .Popen (
200+ command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True
201+ )
202+ stdout , stderr = process .communicate ()
203+
204+ except OSError as e :
205+ print (f"Error: { e } " )
206+
207+ return repo_name
208+
209+ def list_chart_versions (charts , repo_name , latest_only = True ):
210+ """
211+ Fetches all the linkerd charts
212+
213+ Parameters:
214+ charts (list): A list of chart names to fetch
215+
216+ Returns:
217+ dict: A list of chart version tuples
218+ Example: {('linkerd2/linkerd2', '2.11.5', 'stable-2.11.5')}
219+
220+ """
221+ all_versions = set ()
222+
223+
224+ for chart in charts :
225+ # helm repo update linkerd2
226+ search_term = f"{ repo_name } /{ chart } "
227+ command = ["helm" , "search" , "repo" , search_term , "--versions" , "--output" , "json" ]
228+ try :
229+ process = subprocess .Popen (
230+ command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True
231+ )
232+ stdout , stderr = process .communicate ()
233+
234+ if process .returncode == 0 :
235+ chart_data = json .loads (stdout )
236+ versions = [
237+ (chart , item ["version" ], item ["app_version" ], f"https://artifacthub.io/packages/helm/{ repo_name } /{ chart } /{ item ['version' ]} " ) for item in chart_data
238+ ]
239+ if latest_only :
240+ latest_versions = find_newest_versions (versions )
241+ else :
242+ latest_versions = versions
243+
244+ all_versions .update (latest_versions )
245+ else :
246+ print (f"Error: Failed to list chart versions for { chart } " )
247+ print (stderr )
248+ except OSError as e :
249+ print (f"Error: { e } " )
250+
251+ return sorted (all_versions )
252+
253+
254+ def print_output (data , format ):
255+ if format == "json" :
256+ print (json .dumps (data , indent = 4 ))
257+ elif format == "yaml" :
258+ print (yaml .dump (data , indent = 4 ))
259+ else :
260+ try :
261+ from tabulate import tabulate
262+ except ImportError as e :
263+ print ("Please install tabulate: pip3 install tabulate" )
264+ exit (1 )
265+ headers = ["App Version" , "Chart Name" , "Chart Version" ]
266+ table = []
267+ for app_version , charts in sorted (
268+ data .items (), reverse = True
269+ ):
270+ for chart_name , chart_data in charts .items ():
271+ table .append (
272+ [app_version , chart_data ["chart_name" ], chart_data ["chart_version" ]]
273+ )
274+ print (tabulate (table , headers = headers , tablefmt = "github" ))
275+ print ("\n " )
276+ table = []
277+
278+
279+ parser = argparse .ArgumentParser (description = "List linkerd chart versions in various formats" )
280+ parser .add_argument (
281+ "--format" , default = "table" , choices = ["table" , "json" , "yaml" ], help = "Desired Output format, defaults to table"
282+ )
283+ parser .add_argument ('--release_type' , choices = ["stable" , "edge" ], default = "stable" , help = "Use the specific release type, defaults to stable" )
284+ parser .add_argument ('--helm_url' , choices = ["https://helm.linkerd.io" ], default = "https://helm.linkerd.io" , help = "The helm url to use" )
285+ args = parser .parse_args ()
286+
287+ repo_name = find_repo_name (args .release_type )
288+ update_repo (args .release_type , args .helm_url , repo_name )
289+
290+ all_versions = list_chart_versions (charts , repo_name )
291+ combined_charts_by_app_version = combine_charts_by_app_version (all_versions )
292+ print_output (combined_charts_by_app_version , args .format )
0 commit comments