Files
superset2/superset/cli/update.py
2026-06-16 17:50:51 -07:00

162 lines
5.5 KiB
Python
Executable File

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import json
import logging
import os
import sys
from typing import Optional
import click
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask import current_app
from flask.cli import with_appcontext
from flask_appbuilder import Model
from flask_appbuilder.api import BaseApi
from flask_appbuilder.api.manager import resolver
import superset.utils.database as database_utils
from superset.utils.decorators import transaction
from superset.utils.encrypt import ENCRYPTION_ENGINES, SecretsMigrator
logger = logging.getLogger(__name__)
@click.command()
@with_appcontext
@transaction()
@click.option("--database_name", "-d", required=True, help="Database name to change")
@click.option("--uri", "-u", required=True, help="Database URI to change")
@click.option(
"--skip_create",
"-s",
is_flag=True,
default=False,
help="Don't create the DB if it doesn't exist",
)
def set_database_uri(database_name: str, uri: str, skip_create: bool) -> None:
"""Updates a database connection URI"""
database_utils.get_or_create_db(database_name, uri, not skip_create)
@click.command()
@with_appcontext
@transaction()
def sync_tags() -> None:
"""Rebuilds special tags (owner, type, favorited by)."""
# pylint: disable=no-member
metadata = Model.metadata
# pylint: disable=import-outside-toplevel
from superset.common.tags import add_favorites, add_owners, add_types
add_types(metadata)
add_owners(metadata)
add_favorites(metadata)
@click.command()
@with_appcontext
def update_api_docs() -> None:
"""Regenerate the openapi.json file in docs"""
superset_dir = os.path.abspath(os.path.dirname(__file__))
openapi_json = os.path.join(
superset_dir, "..", "..", "docs", "static", "resources", "openapi.json"
)
api_version = "v1"
version_found = False
api_spec = APISpec(
title=current_app.appbuilder.app_name,
version=api_version,
openapi_version="3.0.2",
info={"description": current_app.appbuilder.app_name},
plugins=[MarshmallowPlugin(schema_name_resolver=resolver)],
servers=[{"url": "http://localhost:8088"}],
)
for base_api in current_app.appbuilder.baseviews:
if isinstance(base_api, BaseApi) and base_api.version == api_version:
base_api.add_api_spec(api_spec)
version_found = True
if version_found:
click.secho("Generating openapi.json", fg="green")
with open(openapi_json, "w") as outfile:
json.dump(api_spec.to_dict(), outfile, sort_keys=True, indent=2)
outfile.write("\n")
else:
click.secho("API version not found", err=True)
@click.command()
@with_appcontext
@click.option(
"--previous_secret_key",
"-a",
required=False,
help="An optional previous secret key, if PREVIOUS_SECRET_KEY "
"is not set on the config",
)
@click.option(
"--engine",
"-e",
"target_engine_name",
required=False,
type=click.Choice(sorted(ENCRYPTION_ENGINES), case_sensitive=False),
help="Re-encrypt all app-encrypted fields with this encryption engine "
"(e.g. 'aes-gcm' for authenticated encryption). The SECRET_KEY is "
"unchanged. Take a metadata-DB backup first, then set "
"SQLALCHEMY_ENCRYPTED_FIELD_ENGINE to the same value and restart.",
)
def re_encrypt_secrets(
previous_secret_key: Optional[str] = None,
target_engine_name: Optional[str] = None,
) -> None:
"""Re-encrypt every app-encrypted field via :class:`SecretsMigrator`.
Supports key rotation (``previous_secret_key``, falling back to the
``PREVIOUS_SECRET_KEY`` config) and engine migration (``target_engine_name``,
a case-insensitive ``ENCRYPTION_ENGINES`` key such as ``aes-gcm``); the two
can combine. With neither provided the command is a no-op. Exits non-zero on
failure.
"""
previous_secret_key = previous_secret_key or current_app.config.get(
"PREVIOUS_SECRET_KEY"
)
target_engine = (
ENCRYPTION_ENGINES[target_engine_name] if target_engine_name else None
)
if previous_secret_key is None and target_engine is None:
click.secho(
"No previous secret key or target engine provided; nothing to re-encrypt.",
fg="yellow",
)
return
secrets_migrator = SecretsMigrator(
previous_secret_key=previous_secret_key,
target_engine=target_engine,
)
try:
stats = secrets_migrator.run()
except Exception as exc: # pylint: disable=broad-except
click.secho(f"Re-encryption failed: {exc}", err=True)
sys.exit(1)
click.secho(
f"Re-encryption complete: {stats.re_encrypted} re-encrypted, "
f"{stats.skipped} skipped, {stats.null} null, {stats.failed} failed.",
fg="green",
)