Michel Thebeau 2b58eb7ff2 Update OIDC upgrade script for 22.12
These scripts perform helm override check, backup of helm overrides,
conversion of helm overrides and upgrade of the oidc-auth-apps
application.

The original scripts are restored from commit 8293b0af, which was
originally written for 21.12 to 22.06 upgrade.  This cherry-picks commit
8293b0af, but also adjusts the accepted releases.  We will support
upgrade of 21.12 to 22.12 release.

The script is neuter from 22.06 to 22.12 upgrade.

The changes from original commit include:
 - accept TO_RELEASE 22.12 instead
 - when from_release and to_release do not match the return with a
   softer warning, and return 0 instead 1
 - use /var/opt/oidc-auth-apps instead of /opt/oidc-auth-apps
 - use yaml.safe_load()

Test Plan:
PASS: unit test on python3
PASS: unit test of both scripts
PASS: unit test health-check, start, activate
PASS: unit test yaml.safeload health-check, start, migrate
PASS: unit test helm overrides before/after switch to yaml.safeload
PASS: conversion of 21.12 configuration
PASS: absent helm overrides
PASS: AIO-SX provision 22.12
PASS: AIO-DX provision 22.12
PASS: confirm permissions of /var/opt/oidc-auth-apps/, and scripts
PASS: simulate 21.12 to 22.12 upgrade env, upgrade oidc application
PASS: simulate 22.06 to 22.12 upgrade env, observe neuter scripts
N/A: AIO-SX/AIO-DX upgrade 21.12 to 22.12
N/A: AIO-SX/AIO-DX upgrade 22.06 to 22.12 (neuter scripts)

Story: 2009303
Task: 46677

Depends-on: https://review.opendev.org/c/starlingx/config/+/863656
Depends-on: https://review.opendev.org/c/starlingx/oidc-auth-armada-app/+/863436

Change-Id: I53ae6fbf1669cd8fbfca6082716333433d32ab80
Signed-off-by: Michel Thebeau <Michel.Thebeau@windriver.com>
2022-11-10 14:23:31 -05:00

497 lines
17 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
# Conversion of oidc-auth-apps configuration
#
# Verify the supported configuration during health-query-upgrade.
# Backup overrides at upgrade start.
# Convert the configuration during upgrade activate.
from controllerconfig.common import log
import copy
import os
import psycopg2
from psycopg2.extras import RealDictCursor
import sys
import yaml
LOG = log.get_logger(__name__)
log.configure()
# This script is only valid for to/from releases:
ACCEPTED_FROM = ['21.12']
ACCEPTED_TO = ['22.12']
ACCEPTED_ACTIONS = ['health-check', 'start', 'migrate']
# this path should have been created by stx-oidc-auth-helm package
# with ownership assigned to postgres:postgres
BACKUP_PATH = '/var/opt/oidc-auth-apps'
# list of charts in oidc-auth-apps; for sanity check only
oidc_charts = ['dex', 'oidc-client', 'secret-observer']
# Hard-coded chart values; matching the fluxcd manifest defaults
DEFAULT_HTTPSTLS_MOUNT = '/etc/dex/tls'
DEFAULT_HTTPSTLS_MODE = 420
DEFAULT_HTTPSTLS_SECRET = 'local-dex.tls'
# A dictionary of values selected from the overrides yaml during
# validate_overrides(). The selected values are used to convert
# the yaml from old dex to new dex
DEFINES = {}
# validate yaml, instructions for what configurations accepted
validation_yaml = """
name: "supported"
validation: "children"
optional: False
accepted: ["extraVolumeMounts", "extraVolumes", "config", "certs"]
children:
- name: "extraVolumeMounts"
validation: "any"
optional: True
define: "volumeMounts"
- name: "extraVolumes"
validation: "any"
optional: True
define: "volumes"
- name: "config"
validation: "children"
optional: False
define: "dex_config"
children:
- name: "web"
validation: "children"
optional: True
children:
- name: "tlsCert"
validation: "exact"
optional: True
define: "dex_https_tlsCert"
- name: "tlsKey"
validation: "exact"
optional: True
define: "dex_https_tlsKey"
- name: "certs"
validation: "children"
optional: True
accepted: ["grpc", "web"]
children:
- name: "grpc"
validation: "children"
optional: True
accepted: ["secret"]
children:
- name: "secret"
validation: "children"
optional: False
accepted: ["caName", "clientTlsName", "serverTlsName"]
children:
- name: "caName"
validation: "exact"
optional: False
define: "tls_secret"
- name: "clientTlsName"
validation: "exact"
optional: False
define: "tls_secret"
- name: "serverTlsName"
validation: "exact"
optional: False
define: "tls_secret"
- name: "web"
validation: "children"
optional: False
accepted: ["secret"]
children:
- name: "secret"
validation: "children"
optional: False
accepted: ["caName", "tlsName"]
children:
- name: "caName"
validation: "exact"
optional: False
define: "tls_secret"
- name: "tlsName"
validation: "exact"
optional: False
define: "tls_secret"
"""
# sql to fetch the user_overrides from DB for oidc-auth-apps
sql_overrides = ("SELECT helm_overrides.name, user_overrides"
" FROM helm_overrides"
" LEFT OUTER JOIN kube_app"
" ON helm_overrides.app_id = kube_app.id"
" WHERE kube_app.name = 'oidc-auth-apps'")
sql_update = ("UPDATE helm_overrides"
" SET user_overrides = '%s'"
" FROM kube_app"
" WHERE helm_overrides.app_id = kube_app.id"
" AND kube_app.name = 'oidc-auth-apps'"
" AND helm_overrides.name = 'dex'")
def get_overrides(conn):
"""Fetch helm overrides from DB"""
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(sql_overrides)
return cur.fetchall()
def backup_overrides(overrides, action='debug'):
"""Dump helm overrides from DB to files in BACKUP_PATH"""
backup_path = os.path.join(BACKUP_PATH, action)
if not os.path.exists(backup_path):
os.makedirs(backup_path)
field = 'user_overrides'
for chart in overrides:
name = chart['name']
if name not in oidc_charts:
LOG.warning("oidc-auth-apps: mismatch chart name '%s'", name)
if chart[field]:
document = yaml.safe_load(chart[field])
if not document:
LOG.debug("oidc-auth-apps: %s empty document", name)
continue
backup_f = '_'.join([name, field])
backup_f = '.'.join([backup_f, 'yaml'])
backup_f = os.path.join(backup_path, backup_f)
try:
with open(backup_f, 'w') as file:
yaml.dump(document, file, default_flow_style=False)
except IOError as e:
LOG.error("oidc-auth-apps: IOError: %s; file: %s", e, backup_f)
return 1
LOG.info("oidc-auth-apps: user_overrides backed up to %s", backup_path)
return 0
def validate_value(instruction, value):
"""Verify a value"""
if instruction['validation'] == 'exact':
if type(value) not in [str, bool, int, float]:
LOG.error("oidc-auth-apps: value type %s not supported",
type(value))
return False
if 'define' in instruction:
if instruction['define'] in DEFINES:
if DEFINES[instruction['define']] != value:
LOG.error("oidc-auth-apps: defined value is"
" mismatched '%s': '%s' != '%s'",
instruction['define'],
DEFINES[instruction['define']],
value)
LOG.error("oidc-auth-apps: instruction: %s", instruction)
return False
else:
DEFINES[instruction['define']] = value
LOG.debug("oidc-auth-apps: define: '%s' == '%s'",
instruction['define'], value)
if 'values' in instruction:
LOG.error("oidc-auth-apps: validation exact values"
" not implemented")
return False
else:
LOG.error("oidc-auth-apps: validation %s not supported",
instruction['validation'])
return False
LOG.debug("oidc-auth-apps: accept %s: %s: %s",
instruction['validation'], instruction, value)
return True
def printable_item(item):
"""remove children from item to make it printable"""
printable = {}
printable['validation'] = item['validation']
printable['name'] = item['name']
printable['optional'] = item['optional']
if 'define' in item:
printable['define'] = item['define']
return printable
def define_complex_value(item, yaml_doc):
"""Subroutine to fill DEFINES for complex values"""
if 'define' in item and item['validation'] != 'exact':
# Handle saving of complex values
if item['define'] in DEFINES:
LOG.error("oidc-auth-apps: complex values comparison"
" is not supported: %s", printable_item(item))
return False
else:
DEFINES[item['define']] = copy.deepcopy(yaml_doc[item['name']])
LOG.debug("oidc-auth-apps: define: '%s'",
item['define'])
return True
def validate_item(item, yaml_doc):
"""Handle one list item from instruction"""
print_item = printable_item(item)
# If neither present nor optional: fail
# If not present, but optional: pass
optional = True
if 'optional' in item:
optional = item['optional']
present = item['name'] in yaml_doc
if not (present or optional):
LOG.error("oidc-auth-apps: overrides omit required value:"
" %s", print_item)
return False
elif not present:
# pass
return True
if not define_complex_value(item, yaml_doc):
return False
if item['validation'] == 'any':
# pass
LOG.debug("oidc-auth-apps: accept instruction: %s", print_item)
elif item['validation'] == 'exact':
if not validate_value(item, yaml_doc[item['name']]):
return False
elif item['validation'] == 'children':
accepted_keys = ['*']
if 'accepted' in item:
if not validate_accepted(item['accepted'], yaml_doc[item['name']]):
return False
else:
accepted_keys = [x for x in yaml_doc[item['name']]]
if not recurse_validate_document(item['children'],
yaml_doc[item['name']]):
LOG.error("oidc-auth-apps: instruction: %s", print_item)
return False
else:
LOG.debug("oidc-auth-apps: accept instruction: %s: %s",
print_item, accepted_keys)
else:
LOG.error("oidc-auth-apps: instruction %s not implemented",
item['validation'])
return False
return True
def validate_accepted(accepted, yaml_doc):
"""Check that each item in yaml is expected"""
if type(yaml_doc) is not dict:
LOG.error("oidc-auth-apps: accepting from list not implemented")
return False
error = False
for key in yaml_doc:
if key not in accepted:
error = True
LOG.error("oidc-auth-apps: key is not accepted: %s", key)
return not error
def recurse_validate_document(instruction, yaml_doc):
"""Recursively verify the document against validation yaml"""
if type(instruction) is not list:
LOG.error("oidc-auth-apps: non-list instruction not implemented")
return False
for item in instruction:
if type(item) is not dict:
LOG.error("oidc-auth-apps: non-dict instruction item"
" not implemented")
return False
elif 'validation' not in item:
LOG.error("oidc-auth-apps: instruction missing validation")
return False
elif 'name' not in item:
LOG.error("oidc-auth-apps: instruction missing name")
return False
elif not validate_item(item, yaml_doc):
return False
return True
def validate_document(validation, document):
"""Top level, verify the document against validation yaml"""
LOG.info("oidc-auth-apps: validating %s", validation['name'])
if validation['validation'] != 'children':
LOG.warning("oidc-auth-apps: root validation should be"
" children not %s", validation['validation'])
result = recurse_validate_document(validation['children'], document)
if 'accepted' in validation:
if not validate_accepted(validation['accepted'], document):
return False
if validation['optional']:
LOG.warning("oidc-auth-apps: root validation is optional")
return True
return result
def get_chart_override(overrides, chart):
"""Get a specific set of overrides from the db value"""
chart_ov = None
for chart_ov in overrides:
if 'name' in chart_ov and chart_ov['name'] == chart:
break
else:
chart_ov = None
if not (chart_ov and 'user_overrides' in chart_ov):
return None
if not chart_ov['user_overrides']:
# A sanity check. Really shouldn't see this if oidc-auth-apps
# does not have dex overrides - either because the app is not
# applied, or because it failed to apply without overrides
return None
# convert the string to python structures
return yaml.safe_load(chart_ov['user_overrides'])
def validate_overrides(overrides):
"""Check if the user_overrides are supported"""
DEFINES.clear()
if not overrides:
# dex without overrides isn't configured correctly
LOG.error("oidc-auth-apps: no overrides to validate")
return False
elif type(overrides) is not list:
# this shouldn't happen
LOG.error("oidc-auth-apps: overrides not list type")
return False
# Find dex; only dex helm needs conversion
document = get_chart_override(overrides, 'dex')
if not document:
LOG.error("oidc-auth-apps: no dex user_overrides to validate")
return False
validate = yaml.safe_load(validation_yaml)
return validate_document(validate, document)
def get_httpstls_mount():
"""Use the default unless the end-user had overridden it"""
if 'dex_https_tlsCert' in DEFINES:
return os.path.dirname(DEFINES['dex_https_tlsCert'])
# The default matches oic-auth-apps flucd manifest defaults
return DEFAULT_HTTPSTLS_MOUNT
def get_httpstls_secret():
"""Use the default unless the end-user had overridden it"""
if 'tls_secret' in DEFINES:
return DEFINES['tls_secret']
# The default matches oic-auth-apps flucd manifest defaults
return DEFAULT_HTTPSTLS_SECRET
def merge_new_overrides():
"""Read DEFINES and prepare new overrides yaml"""
# Take the dex config as is:
new_doc = {'config': copy.deepcopy(DEFINES['dex_config'])}
# Convert old dex certs.web.secret to https-tls volume/volumeMounts
mount = {'mountPath': get_httpstls_mount(), 'name': 'https-tls'}
vol = {'secret': {'secretName': get_httpstls_secret(),
'defaultMode': DEFAULT_HTTPSTLS_MODE},
'name': 'https-tls'}
# Take 'extra' volumes and mounts that may exist in old dex
# This is expected to be the WAD certificate
volumes = []
volumeMounts = []
if 'volumes' in DEFINES:
volumes = copy.deepcopy(DEFINES['volumes'])
if 'volumeMounts' in DEFINES:
volumeMounts = copy.deepcopy(DEFINES['volumeMounts'])
# only add volumes/mounts if 'extra' was specified, or
# if there was non-default mount
if volumes or 'tls_secret' in DEFINES:
volumes.append(vol)
if volumeMounts or 'dex_https_tlsCert' in DEFINES:
volumeMounts.append(mount)
if volumes:
new_doc['volumes'] = volumes
if volumeMounts:
new_doc['volumeMounts'] = volumeMounts
return new_doc
def convert_overrides(overrides, conn):
"""Convert the user_overrides from old dex to new"""
LOG.info("oidc-auth-apps: converting dex overrides")
if not validate_overrides(overrides):
return 1
new_doc = merge_new_overrides()
res = backup_overrides(overrides, action='migrate')
if res != 0:
return res
# replace the dex user overrides
new_str = yaml.dump(new_doc, default_flow_style=False)
for override in overrides:
if override['name'] == 'dex':
override['user_overrides'] = new_str
res = backup_overrides(overrides, action='converted')
return res
def main():
action = None
from_release = None
to_release = None
arg = 1
while arg < len(sys.argv):
if arg == 1:
from_release = sys.argv[arg]
elif arg == 2:
to_release = sys.argv[arg]
elif arg == 3:
action = sys.argv[arg]
else:
print("Invalid option %s." % sys.argv[arg])
return 1
arg += 1
if action not in ACCEPTED_ACTIONS:
LOG.debug("oidc-auth-apps: omit %s, %s, %s",
from_release, to_release, action)
return 0
elif from_release not in ACCEPTED_FROM:
LOG.warning("oidc-auth-apps: not valid from release %s",
from_release)
return 0
elif to_release not in ACCEPTED_TO:
LOG.warning("oidc-auth-apps: not valid to release %s",
to_release)
return 0
try:
conn = psycopg2.connect("dbname=sysinv user=postgres")
overrides = get_overrides(conn)
except Exception as ex:
LOG.exception("oidc-auth-apps: %s", ex)
return 1
if not overrides:
LOG.error("oidc-auth-apps: failed to fetch overrides")
return 1
elif not get_chart_override(overrides, 'dex'):
LOG.info("oidc-auth-apps: no dex overrides to convert")
return 0
if action == 'health-check':
if validate_overrides(overrides):
LOG.info("oidc-auth-apps: upgrade script health-check: success")
return 0
return 1
elif action == 'start':
return backup_overrides(overrides, action='start')
elif action == 'migrate':
convert_overrides(overrides, conn)
# A failure of oidc-auth-apps overrides conversion is unhandled.
# A patch for 21.12 release is needed to pre-test the
# compatibility of user overrides with expected configurations.
# 22.06 version of oidc-auth-apps will fail to apply if overrides
# are not converted.
return 0
if __name__ == "__main__":
sys.exit(main())