Skip to content

Commit

Permalink
Merge pull request #200 from aadityasinha-dotcom/props_from_env_varia…
Browse files Browse the repository at this point in the history
…bles

Load profile properties from env variables
  • Loading branch information
t1m0thyj authored Aug 4, 2023
2 parents 6d351f6 + 20875ce commit 558edfb
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

All notable changes to the Zowe Client Python SDK will be documented in this file.

## Recent Changes

- Feature: Added method to load profile properties from environment variables
53 changes: 53 additions & 0 deletions src/core/zowe/core_for_zowe_sdk/config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import base64
import os.path
import re
import json
import requests
import sys
import warnings
from dataclasses import dataclass, field
Expand Down Expand Up @@ -69,6 +71,7 @@ class ConfigFile:
4. Contents of the file.
4.1 Profiles
4.2 Defaults
4.3 Schema Property
5. Secure Properties associated with the file.
"""

Expand All @@ -78,6 +81,7 @@ class ConfigFile:
profiles: Optional[dict] = None
defaults: Optional[dict] = None
secure_props: Optional[dict] = None
schema_property: Optional[dict] = None
_missing_secure_props: list = field(default_factory=list)

@property
Expand All @@ -101,6 +105,10 @@ def filepath(self) -> Optional[str]:
def location(self) -> Optional[str]:
return self._location

@property
def schema_path(self) -> Optional[str]:
self.schema_property

@location.setter
def location(self, dirname: str) -> None:
if os.path.isdir(dirname):
Expand All @@ -121,12 +129,57 @@ def init_from_file(self) -> None:

self.profiles = profile_jsonc.get("profiles", {})
self.defaults = profile_jsonc.get("defaults", {})
self.schema_property = profile_jsonc.get("$schema", None)

# loading secure props is done in load_profile_properties
# since we want to try loading secure properties only when
# we know that the profile has saved properties
# self.load_secure_props()

def schema_list(
self,
) -> list:
"""
Loads the schema properties
in a sorted order according to the priority
Returns
-------
Dictionary
Returns the profile properties from schema (prop: value)
"""

schema = self.schema_property
if schema is None:
return []

if schema.startswith("https://") or schema.startswith("http://"):
schema_json = requests.get(schema).json()

elif not os.path.isabs(schema):
schema = os.path.join(self.location, schema)
with open(schema) as f:
schema_json = json.load(f)

elif os.path.isfile(schema):
with open(schema) as f:
schema_json = json.load(f)
else:
return []

profile_props:dict = {}
schema_json = dict(schema_json)

for props in schema_json['properties']['profiles']['patternProperties']["^\\S*$"]["allOf"]:
props = props["then"]

while "properties" in props:
props = props.pop("properties")
profile_props = props

return profile_props

def get_profile(
self,
profile_name: Optional[str] = None,
Expand Down
54 changes: 54 additions & 0 deletions src/core/zowe/core_for_zowe_sdk/profile_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import os.path
import os
import warnings
from typing import Optional

Expand Down Expand Up @@ -109,6 +110,50 @@ def config_filepath(self) -> Optional[str]:
"""Get the full Zowe z/OSMF Team Project Config filepath"""
return self.project_config.filepath

@staticmethod
def get_env(
cfg: ConfigFile,
) -> dict:
"""
Maps the env variables to the profile properties
Returns
-------
Dictionary
Containing profile properties from env variables (prop: value)
"""

props = cfg.schema_list()
if props == []:
return {}

env, env_var = {}, {}

for var in list(os.environ.keys()):
if var.startswith("ZOWE_OPT"):
env[var[len("ZOWE_OPT_"):].lower()] = os.environ.get(var)

for k, v in env.items():
word = k.split("_")

if len(word) > 1:
k = word[0]+word[1].capitalize()
else:
k = word[0]

if k in list(props.keys()):
if props[k]['type'] == "number":
env_var[k] = int(v)

elif props[k]['type'] == "string":
env_var[k] = str(v)

elif props[k]['type'] == "boolean":
env_var[k] = bool(v)

return env_var

@staticmethod
def get_profile(
cfg: ConfigFile,
Expand Down Expand Up @@ -175,6 +220,7 @@ def load(
profile_name: Optional[str] = None,
profile_type: Optional[str] = None,
check_missing_props: bool = True,
override_with_env: Optional[bool] = False,
) -> dict:
"""Load connection details from a team config profile.
Returns
Expand Down Expand Up @@ -209,6 +255,7 @@ def load(
"Global Config": self.global_config,
}
profile_props: dict = {}
env_var: dict = {}

missing_secure_props = [] # track which secure props were not loaded

Expand All @@ -226,6 +273,9 @@ def load(

missing_secure_props.extend(profile_loaded.missing_secure_props)

if override_with_env:
env_var = {**self.get_env(cfg)}

if i == 1 and profile_props:
break # Skip loading from global config if profile was found in project config

Expand All @@ -246,4 +296,8 @@ def load(

warnings.resetwarnings()

for k, v in profile_props.items():
if k in env_var:
profile_props[k] = env_var[k]

return profile_props
35 changes: 35 additions & 0 deletions tests/unit/test_zowe_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,13 @@ def setUp(self):
self.original_nested_file_path = os.path.join(
FIXTURES_PATH, "nested.zowe.config.json"
)
self.original_schema_file_path = os.path.join(
FIXTURES_PATH, "zowe.schema.json"
)
self.fs.add_real_file(self.original_file_path)
self.fs.add_real_file(self.original_user_file_path)
self.fs.add_real_file(self.original_nested_file_path)
self.fs.add_real_file(self.original_schema_file_path)

self.custom_dir = os.path.dirname(FIXTURES_PATH)
self.custom_appname = "zowe_abcd"
Expand Down Expand Up @@ -381,6 +385,37 @@ def test_profile_not_found_warning(self, get_pass_func):
prof_manager.config_dir = self.custom_dir
props: dict = prof_manager.load("non_existent_profile")

@patch("keyring.get_password", side_effect=keyring_get_password)
def test_profile_loading_with_env_variables(self, get_pass_func):
"""
Test loading of correct file given a filename and directory,
also load by profile_name correctly populating fields from custom
profile and secure credentials
"""
# Setup - copy profile to fake filesystem created by pyfakefs
os.environ["ZOWE_OPT_HOST"] = "aaditya"
custom_file_path = os.path.join(self.custom_dir, "zowe.config.json")
shutil.copy(self.original_nested_file_path, custom_file_path)
shutil.copy(self.original_schema_file_path, self.custom_dir)
os.chdir(self.custom_dir)

self.setUpCreds(custom_file_path, {
"profiles.zosmf.properties.user": "user",
"profiles.zosmf.properties.password": "password",
})

# Test
prof_manager = ProfileManager(appname="zowe")
prof_manager.config_dir = self.custom_dir
props: dict = prof_manager.load(profile_name="lpar1.zosmf", override_with_env=True)

expected_props = {
"host": "aaditya",
"rejectUnauthorized": True,
"port": 443,
}
self.assertEqual(props, expected_props)


class TestValidateConfigJsonClass(unittest.TestCase):
"""Testing the validate_config_json function"""
Expand Down

0 comments on commit 558edfb

Please sign in to comment.