Skip to content

Commit 3659f90

Browse files
committed
feat: Update Operator Dashboard
Includes first batch of v1 product Operator charts.
1 parent 744f248 commit 3659f90

File tree

75 files changed

+4313
-3445
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+4313
-3445
lines changed

.DS_Store

6 KB
Binary file not shown.

tutoraspects/.DS_Store

6 KB
Binary file not shown.

tutoraspects/asset_command_helpers.py

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
"""
2+
Helpers for Tutor commands and "do" commands.
3+
"""
4+
from zipfile import ZipFile
5+
import glob
6+
import os
7+
import re
8+
import yaml
9+
10+
import click
11+
12+
FILE_NAME_ATTRIBUTE = "_file_name"
13+
14+
PLUGIN_PATH = os.path.dirname(os.path.abspath(__file__))
15+
ASSETS_PATH = os.path.join(PLUGIN_PATH, "templates", "openedx-assets", "assets")
16+
17+
18+
class SupersetCommandError(Exception):
19+
"""
20+
An error we can use for these methods.
21+
"""
22+
23+
24+
class Asset:
25+
"""
26+
Base class for asset types used in import.
27+
"""
28+
29+
path = None
30+
assets_path = None
31+
templated_vars = None
32+
33+
def __init__(self):
34+
if not self.path:
35+
raise NotImplementedError("Asset is an abstract class.")
36+
37+
self.assets_path = os.path.join(ASSETS_PATH, self.path)
38+
39+
def get_path(self):
40+
"""
41+
Returns the full path to the asset file type.
42+
"""
43+
if self.assets_path:
44+
return self.assets_path
45+
raise NotImplementedError
46+
47+
def get_templated_vars(self):
48+
"""
49+
Returns a list of variables which should have templated variables.
50+
51+
This allows us to alert users when they might be submitting hard coded
52+
values instead of something like {{ TABLE_NAME }}.
53+
"""
54+
return self.templated_vars or []
55+
56+
57+
class ChartAsset(Asset):
58+
"""
59+
Chart assets.
60+
"""
61+
62+
path = "charts"
63+
64+
65+
class DashboardAsset(Asset):
66+
"""
67+
Dashboard assets.
68+
"""
69+
70+
path = "dashboards"
71+
72+
73+
class DatasetAsset(Asset):
74+
"""
75+
Dataset assets.
76+
"""
77+
78+
path = "datasets"
79+
templated_vars = ["schema", "table_name", "sql"]
80+
81+
82+
class DatabaseAsset(Asset):
83+
"""
84+
Database assets.
85+
"""
86+
87+
path = "databases"
88+
templated_vars = ["sqlalchemy_uri"]
89+
90+
91+
ASSET_TYPE_MAP = {
92+
"slice_name": ChartAsset(),
93+
"dashboard_title": DashboardAsset(),
94+
"table_name": DatasetAsset(),
95+
"database_name": DatabaseAsset(),
96+
}
97+
98+
99+
def import_asset_file(asset_path, content, echo):
100+
orig_filename = os.path.basename(asset_path)
101+
out_filename = re.sub(r'(_\d*)\.yaml', '.yaml', orig_filename)
102+
content[FILE_NAME_ATTRIBUTE] = out_filename
103+
104+
out_path = None
105+
needs_review = False
106+
for key, cls in ASSET_TYPE_MAP.items():
107+
if key in content:
108+
out_path = cls.get_path()
109+
110+
for var in cls.get_templated_vars():
111+
# If this is a variable we expect to be templated,
112+
# check that it is.
113+
if (
114+
content[var]
115+
and not content[var].startswith("{{")
116+
and not content[var].startswith("{%")
117+
):
118+
echo(
119+
click.style(
120+
f"WARN: {orig_filename} has "
121+
f"{var} set to {content[var]} instead of a "
122+
f"setting.",
123+
fg="yellow",
124+
)
125+
)
126+
needs_review = True
127+
break
128+
return out_path, needs_review
129+
130+
131+
def import_superset_assets(file, echo):
132+
"""
133+
Import assets from a Superset export zip file to the openedx-assets directory.
134+
"""
135+
written_assets = []
136+
review_files = set()
137+
err = 0
138+
139+
with ZipFile(file.name) as zip_file:
140+
for asset_path in zip_file.namelist():
141+
if "metadata.yaml" in asset_path:
142+
continue
143+
with zip_file.open(asset_path) as asset_file:
144+
content = yaml.safe_load(asset_file)
145+
out_path, needs_review = import_asset_file(asset_path, content, echo)
146+
147+
# This can happen if it's an unknown asset type
148+
if not out_path:
149+
continue
150+
151+
if needs_review:
152+
review_files.add(content[FILE_NAME_ATTRIBUTE])
153+
154+
out_path = os.path.join(out_path, content[FILE_NAME_ATTRIBUTE])
155+
written_assets.append(out_path)
156+
157+
with open(out_path, "w", encoding="utf-8") as out_f:
158+
yaml.dump(content, out_f, encoding="utf-8")
159+
160+
if review_files:
161+
echo()
162+
echo(
163+
click.style(
164+
f"{len(review_files)} files had warnings and need review:", fg="red"
165+
)
166+
)
167+
for filename in review_files:
168+
echo(f" - {filename}")
169+
170+
raise SupersetCommandError(
171+
"Warnings found, please review then run "
172+
"'tutor aspects check_superset_assets'"
173+
)
174+
175+
echo()
176+
echo(f"Serialized {len(written_assets)} assets")
177+
178+
return err
179+
180+
181+
def _get_asset_files():
182+
for file_name in glob.iglob(ASSETS_PATH + "/**/*.yaml", recursive=True):
183+
with open(file_name, "r", encoding="utf-8") as file:
184+
# We have to remove the jinja for it to parse
185+
file_str = file.read()
186+
file_str = file_str.replace("{{", "").replace("}}", "")
187+
asset = yaml.safe_load(file_str)
188+
189+
# Some asset types are lists of one element for some reason
190+
if isinstance(asset, list):
191+
asset = asset[0]
192+
193+
yield file_name, asset
194+
195+
196+
def _deduplicate_asset_files(existing, found, echo):
197+
if existing["modified"] == found["modified"]:
198+
short_existing = os.path.basename(existing["file_name"])
199+
short_found = os.path.basename(found["file_name"])
200+
raise SupersetCommandError(
201+
"Modified dates are identical. You will need to "
202+
f"remove either {short_existing} or "
203+
f" {short_found} and run again."
204+
)
205+
206+
newer_file = existing if existing["modified"] > found["modified"] else found
207+
old_file = existing if existing["modified"] < found["modified"] else found
208+
209+
short_new = os.path.basename(newer_file["file_name"])
210+
short_old = os.path.basename(old_file["file_name"])
211+
212+
echo(f"{short_new} is newer, {short_old} will be deleted")
213+
214+
os.remove(old_file["file_name"])
215+
216+
return newer_file
217+
218+
219+
def deduplicate_superset_assets(echo):
220+
"""
221+
Check for duplicated UUIDs in openedx-assets, delete the older file.
222+
223+
Superset exports use the name of the asset in the filename, so if you
224+
rename a chart or dashboard a new file will be created with the same
225+
UUID, causing import issues. This tries to fix that.
226+
"""
227+
echo("De-duplicating assets...")
228+
err = 0
229+
uuid_file_map = {}
230+
231+
for file_name, asset in _get_asset_files():
232+
curr_uuid = asset["uuid"]
233+
if curr_uuid in uuid_file_map:
234+
echo()
235+
echo(
236+
click.style(f"WARN: Duplicate UUID found {asset['uuid']}", fg="yellow")
237+
)
238+
239+
new_file = {"file_name": file_name, "modified": os.stat(file_name)[8]}
240+
old_file = uuid_file_map[curr_uuid]
241+
242+
try:
243+
uuid_file_map[curr_uuid] = _deduplicate_asset_files(
244+
old_file, new_file, echo
245+
)
246+
except SupersetCommandError as ex:
247+
echo(click.style(f"ERROR: {ex}", fg="red"))
248+
err += 1
249+
continue
250+
else:
251+
uuid_file_map[asset["uuid"]] = {
252+
"file_name": file_name,
253+
"modified": os.stat(file_name)[8],
254+
}
255+
256+
if err:
257+
echo()
258+
echo()
259+
echo(click.style(f"{err} errors found!", fg="red"))
260+
261+
echo("Deduplication complete.")
262+
263+
264+
def check_asset_names(echo):
265+
"""
266+
Warn about any duplicate asset names.
267+
"""
268+
echo("Looking for duplicate names...")
269+
warn = 0
270+
271+
names = set()
272+
for file_name, asset in _get_asset_files():
273+
for k in ("slice_name", "dashboard_title", "database_name"):
274+
if k in asset:
275+
if asset[k] in names:
276+
warn += 1
277+
echo(
278+
f"WARNING: Duplicate name {asset[k]} in {file_name}, this "
279+
f"could confuse users, consider changing it."
280+
)
281+
names.add(asset[k])
282+
break
283+
284+
echo(f"{warn} duplicate names detected.")

tutoraspects/commands_v1.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22
from __future__ import annotations
33

44
import string
5+
import sys
56

67
import click
78

9+
from tutoraspects.asset_command_helpers import (
10+
check_asset_names,
11+
import_superset_assets,
12+
deduplicate_superset_assets,
13+
SupersetCommandError,
14+
)
15+
816

917
@click.command()
1018
@click.option("-n", "--num_batches", default=100)
@@ -113,10 +121,69 @@ def transform_tracking_logs(options) -> list[tuple[str, str]]:
113121
return [("lms", f"./manage.py lms transform_tracking_logs {options}")]
114122

115123

116-
COMMANDS = (
124+
@click.group()
125+
def aspects() -> None:
126+
"""
127+
Custom commands for the Aspects plugin.
128+
"""
129+
130+
131+
@aspects.command("import_superset_zip")
132+
@click.argument("file", type=click.File("r"))
133+
def serialize_zip(file):
134+
"""
135+
Script that serializes a zip file to the assets.yaml file.
136+
"""
137+
try:
138+
import_superset_assets(file, click.echo)
139+
except SupersetCommandError:
140+
click.echo()
141+
click.echo("Errors found on import. Please correct the issues, then run:")
142+
click.echo(click.style("tutor aspects check_superset_assets", fg="green"))
143+
sys.exit(-1)
144+
145+
click.echo()
146+
deduplicate_superset_assets(click.echo)
147+
148+
click.echo()
149+
check_asset_names(click.echo)
150+
151+
click.echo()
152+
click.echo("Asset merge complete!")
153+
click.echo()
154+
click.echo(
155+
click.style(
156+
"PLEASE check your diffs for exported passwords before committing!",
157+
fg="yellow",
158+
)
159+
)
160+
161+
162+
@aspects.command("check_superset_assets")
163+
def check_superset_assets():
164+
"""
165+
Deduplicate assets by UUID, and check for duplicate asset names.
166+
"""
167+
deduplicate_superset_assets(click.echo)
168+
169+
click.echo()
170+
check_asset_names(click.echo)
171+
172+
click.echo()
173+
click.echo(
174+
click.style(
175+
"PLEASE check your diffs for exported passwords before committing!",
176+
fg="yellow",
177+
)
178+
)
179+
180+
181+
DO_COMMANDS = (
117182
load_xapi_test_data,
118183
dbt,
119184
alembic,
120185
dump_courses_to_clickhouse,
121186
transform_tracking_logs,
122187
)
188+
189+
COMMANDS = (aspects,)

0 commit comments

Comments
 (0)