Skip to content
This repository was archived by the owner on Jan 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@

# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.12"
FROM alpine:latest AS dependencies

USER root

RUN apk update && \
apk add python3 python3-dev py3-pip

RUN mkdir /root/ibm && cd /root/ibm && \
wget https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/messaging/mqdev/redist/9.4.0.6-IBM-MQC-Redist-LinuxX64.tar.gz -O - | tar xzf -
# wget https://ibm.biz/IBM-MQC-Redist-LinuxX64targz -O - | tar xzf -

FROM mcr.microsoft.com/vscode/devcontainers/python:1-${VARIANT}

RUN mkdir -p /opt/mqm

COPY --from=dependencies /root/ibm /opt/mqm

ENV LD_LIBRARY_PATH="/opt/mqm/lib64:${LD_LIBRARY_PATH}"
ENV PATH="/opt/mqm/bin:${PATH}"
ENV LANG="C.UTF-8"

# run github workflows locally
RUN curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:

- name: coverage
id: coverage
run: python -m coverage report --fail-under=90 --omit=**/__version__.py
run: python -m coverage report --fail-under=85 --omit=**/__version__.py

test-e2e:
name: "test-e2e / ${{ matrix.runs-on }} / python-${{ matrix.python-version }}"
Expand Down
94 changes: 73 additions & 21 deletions grizzly_cli/keyvault.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ def add_generic_arguments(parser: CoreArgumentParser) -> None:
help='do not write to keyvault',
)

parser.add_argument(
'-k', '--key',
action='append',
dest='keys',
type=str,
required=False,
help='filter on specified keys',
)


def create_diff_parser(sub_parser: ArgumentSubParser) -> None:
# grizzly-cli keyvault diff
Expand Down Expand Up @@ -233,6 +242,22 @@ def _build_key_name(environment: str, key: str) -> str:
return f'grizzly--{environment}--{_keyvault_normalize(key)}'


def _dict_to_yaml(file: Path, content: dict[str, Any], *, indentation: Path | int) -> None:
file.write_text('') # make sure file is empty

with file.open('w') as fd:
yaml.dump(content, fd, Dumper=IndentDumper.use_indentation(indentation), default_flow_style=False, sort_keys=False, allow_unicode=True)


def _extract_metadata(env_file: str) -> tuple[str, str | None, dict[str, Any]]:
file = Path(env_file)
configuration = load_configuration_file(file).get('configuration', {})

keyvault = configuration.get('keyvault', None)

return (configuration.get('env', None) or file.stem, keyvault, flatten(configuration))


def diff(left_file_name: str, right_file_name: str) -> int:
left_config_file = Path(left_file_name)
right_config_file = Path(right_file_name)
Expand Down Expand Up @@ -269,17 +294,22 @@ def keyvault_import(client: SecretClient, environment: str, args: Arguments, roo
configuration_unflatten: dict[str, Any] = {}

for conf_key, conf_value in configuration.items():
if args.keys is not None and conf_key not in args.keys:
continue

configuration_branch = unflatten(conf_key, conf_value)
configuration_unflatten = merge_dicts(configuration_branch, configuration_unflatten)

configuration = configuration_unflatten

keyvault_configuration, imported_secrets = load_configuration_keyvault(client, environment, root)
keyvault_configuration, imported_secrets = load_configuration_keyvault(client, environment, root, filter_keys=args.keys)

configuration = merge_dicts(keyvault_configuration, configuration)

env_file = Path(args.env_file)
_dict_to_yaml(env_file, {'configuration': configuration}, indentation=env_file)

if not args.dry_run: # do not rewrite environment file on dry-run
_dict_to_yaml(env_file, {'configuration': configuration}, indentation=env_file)

logger.info('\nimported %d secrets from %s to %s', imported_secrets, client.vault_url, env_file.as_posix())

Expand All @@ -289,6 +319,21 @@ def keyvault_import(client: SecretClient, environment: str, args: Arguments, roo
def keyvault_export(client: SecretClient, environment: str, args: Arguments, root: Path, configuration: dict[str, Any]) -> int:
"""
From grizzly to keyvault.

If the specified secret starts with `cert:`, it indicates that it should reference a keyvault certificate (the name). This value
can have metadata (secret content type) appended after the actual value the `#` separator. If the output certificate should be
password protected, `pass:` should reference a keyvault secret which contains the password.

`cert:<keyvault certificate name>[,pass:<keyvault secret name for password>][#format:[mqm|pem-public|pem-private]]`

Supported certificate output formats:
- `mqm`, MQ CMS keystore
- `pem-public`, public certificate in PEM format
- `pem-private`, private key in PEM format

If the configuration key contains `file` in the path, the configuration value will be base64 encoded and, optionally, chunked into
keyvault secrets. If the path also contains `mq`, all MQ keystore/CMS files will be encoded, chunked and then references to the
actual configuration key.
"""
secrets: list[KeyvaultSecretHolder] = []

Expand All @@ -301,10 +346,33 @@ def keyvault_export(client: SecretClient, environment: str, args: Arguments, roo
safe_configuration.update({key: secret})
continue

if args.keys is not None and key not in args.keys:
continue

key_environment = _determine_environment(args.global_configuration, environment, key)
key_name = _build_key_name(key_environment, key)

if 'file' in key:
if secret.startswith('cert:'):
if '#' in secret:
secret, content_type = secret.split('#', 1)
else:
content_type = None

if 'pass:' in secret:
_, password_ref = secret.split(',', 1)
_, password_key = password_ref.split(':', 1)
try:
client.get_secret(password_key)
except ResourceNotFoundError:
message = f'key {password_key} referenced in value for {key} does not exist'
raise ValueError(message)

secrets.append(KeyvaultSecretHolder(
name=key_name,
content_type=content_type,
value=secret,
))
elif 'file' in key:
if 'mq' in key:
secrets.extend(encode_mq_certificate(root, key_environment, key_name, secret))
else:
Expand Down Expand Up @@ -365,22 +433,6 @@ def keyvault_export(client: SecretClient, environment: str, args: Arguments, roo
return 0


def _dict_to_yaml(file: Path, content: dict[str, Any], *, indentation: Path | int) -> None:
file.write_text('')

with file.open('w') as fd:
yaml.dump(content, fd, Dumper=IndentDumper.use_indentation(indentation), default_flow_style=False, sort_keys=False, allow_unicode=True)


def _extract_metadata(env_file: str) -> tuple[str, str | None, dict[str, Any]]:
file = Path(env_file)
configuration = load_configuration_file(file).get('configuration', {})

keyvault = configuration.get('keyvault', None)

return (configuration.get('env', None) or file.stem, keyvault, flatten(configuration))


def keyvault(args: Arguments) -> int:
grizzly_context_root = get_context_root()

Expand Down Expand Up @@ -414,9 +466,9 @@ def keyvault(args: Arguments) -> int:
client = get_keyvault_client(keyvault)

try:
if args.subcommand == 'import':
if args.subcommand == 'import': # from keyvault
return keyvault_import(client, environment, args, grizzly_context_root, configuration)
elif args.subcommand == 'export':
elif args.subcommand == 'export': # to keyvault
return keyvault_export(client, environment, args, grizzly_context_root, configuration)
elif args.subcommand == 'diff':
return diff(args.env_file, args.orig_file)
Expand Down
Loading
Loading