def test_check_return(self, Command): Command.return_value.returncode = 1 Command.return_value.stderr = io.BytesIO(b"Egads!") with pytest.raises(Exception) as exc_info: sfdx("cmd", check_return=True) assert str( exc_info.value) == "Command exited with return code 1:\nEgads!"
def unset_default_org(self): """ unset the default orgs for tasks """ for org in self.list_orgs(): org_config = self.get_org(org) if org_config.default: del org_config.config["default"] self.set_org(org_config) sfdx("force:config:set defaultusername=")
def unset_default_org(self): """ unset the default orgs for tasks """ for org in self.list_orgs(): org_config = self.get_org(org) if org_config.default: del org_config.config["default"] org_config.save() sfdx("force:config:set defaultusername=")
def set_default_org(self, name): """ set the default org for tasks and flows by name """ org = self.get_org(name) self.unset_default_org() org.config["default"] = True org.save() if org.created: sfdx( sarge.shell_format("force:config:set defaultusername={}", org.sfdx_alias))
def _reset_sfdx_snapshot(self): # If org is from sfdx, reset sfdx source tracking if self.project_config.project__source_format == "sfdx" and isinstance( self.org_config, ScratchOrgConfig): sfdx( "force:source:tracking:reset", args=["-p"], username=self.org_config.username, capture_output=True, check_return=True, )
def set_default_org(self, name): """ set the default org for tasks by name key """ org = self.get_org(name) self.unset_default_org() org.config["default"] = True self.set_org(org) if org.created: sfdx( sarge.shell_format( "force:config:set defaultusername={}", org.sfdx_alias ) )
def delete_org(self): """ Uses sfdx force:org:delete to delete the org """ if not self.created: self.logger.info( "Skipping org deletion: the scratch org has not been created") return p = sfdx("force:org:delete -p", self.username, "Deleting scratch org") stdout = [] for line in p.stdout_text: stdout.append(line) if line.startswith("An error occurred deleting this org"): self.logger.error(line) else: self.logger.info(line) if p.returncode: message = "Failed to delete scratch org: \n{}".format( "".join(stdout)) raise ScratchOrgException(message) # Flag that this org has been deleted self.config["created"] = False self.config["username"] = None self.config["date_created"] = None
def delete_org(self): """ Uses sfdx force:org:delete to delete the org """ if not self.created: self.logger.info( "Skipping org deletion: the scratch org has not been created") return p = sfdx("force:org:delete -p", self.username, "Deleting scratch org") output = [] for line in list(p.stdout_text) + list(p.stderr_text): output.append(line) if "error" in line.lower(): self.logger.error(line) else: self.logger.info(line) if p.returncode: message = "Failed to delete scratch org" raise ScratchOrgException(message) # Flag that this org has been deleted self.config["created"] = False self.config["username"] = None self.config["date_created"] = None self.config["instance_url"] = None
def generate_password(self): """Generates an org password with the sfdx utility. """ if self.password_failed: self.logger.warning( "Skipping resetting password since last attempt failed") return # Set a random password so it's available via cci org info p = sfdx( "force:user:password:generate", self.username, log_note="Generating scratch org user password", ) stderr = p.stderr_text.readlines() stdout = p.stdout_text.readlines() if p.returncode: self.config["password_failed"] = True # Don't throw an exception because of failure creating the # password, just notify in a log message self.logger.warning("Failed to set password: \n{}\n{}".format( "\n".join(stdout), "\n".join(stderr)))
def sfdx_info(self): if hasattr(self, "_sfdx_info"): return self._sfdx_info # On-demand creation of scratch orgs if self.create_org is not None and not self.created: self.create_org() username = self.config.get("username") assert username is not None, "SfdxOrgConfig must have a username" self.logger.info(f"Getting org info from Salesforce CLI for {username}") # Call force:org:display and parse output to get instance_url and # access_token p = sfdx("force:org:display --json", self.username) org_info = None stderr_list = [line.strip() for line in p.stderr_text] stdout_list = [line.strip() for line in p.stdout_text] if p.returncode: self.logger.error(f"Return code: {p.returncode}") for line in stderr_list: self.logger.error(line) for line in stdout_list: self.logger.error(line) message = f"\nstderr:\n{nl.join(stderr_list)}" message += f"\nstdout:\n{nl.join(stdout_list)}" raise SfdxOrgException(message) else: try: org_info = json.loads("".join(stdout_list)) except Exception as e: raise SfdxOrgException( "Failed to parse json from output.\n " f"Exception: {e.__class__.__name__}\n Output: {''.join(stdout_list)}" ) org_id = org_info["result"]["accessToken"].split("!")[0] sfdx_info = { "instance_url": org_info["result"]["instanceUrl"], "access_token": org_info["result"]["accessToken"], "org_id": org_id, "username": org_info["result"]["username"], } if org_info["result"].get("password"): sfdx_info["password"] = org_info["result"]["password"] self._sfdx_info = sfdx_info self._sfdx_info_date = datetime.datetime.utcnow() self.config.update(sfdx_info) sfdx_info.update( { "created_date": org_info["result"].get("createdDate"), "expiration_date": org_info["result"].get("expirationDate"), } ) return sfdx_info
def _convert_sfdx_format(self, path, name): orig_path = path with contextlib.ExitStack() as stack: if not pathlib.Path(path, "package.xml").exists(): self.logger.info("Converting from sfdx to mdapi format") path = stack.enter_context(temporary_dir(chdir=False)) args = ["-r", str(orig_path), "-d", path] if name: args += ["-n", name] sfdx( "force:source:convert", args=args, capture_output=False, check_return=True, ) yield path
def get_access_token(self, **userfields): """Get the access token for a specific user If no keyword arguments are passed in, this will return the access token for the default user. If userfields has the key "username", the access token for that user will be returned. Otherwise, a SOQL query will be made based off of the passed-in fields to find the username, and the token for that username will be returned. Examples: | # default user access token: | token = org.get_access_token() | # access token for '*****@*****.**' | token = org.get_access_token(username='******') | # access token for user based on lookup fields | token = org.get_access_token(alias='dadvisor') """ if not userfields: # No lookup fields specified? Return the token for the default user return self.access_token # if we have a username, use it. Otherwise we need to do a # lookup using the passed-in fields. username = userfields.get("username", None) if username is None: where = [f"{key} = '{value}'" for key, value in userfields.items()] query = f"SELECT Username FROM User WHERE {' AND '.join(where)}" result = self.salesforce_client.query_all(query).get("records", []) if len(result) == 0: raise SfdxOrgException( "Couldn't find a username for the specified user.") elif len(result) > 1: raise SfdxOrgException( "More than one user matched the search critiera.") else: username = result[0]["Username"] p = sfdx(f"force:org:display --targetusername={username} --json") if p.returncode: output = p.stdout_text.read() try: info = json.loads(output) explanation = info["message"] except (JSONDecodeError, KeyError): explanation = output raise SfdxOrgException( f"Unable to find access token for {username}\n{explanation}") else: info = json.loads(p.stdout_text.read()) return info["result"]["accessToken"]
def force_refresh_oauth_token(self): # Call force:org:display and parse output to get instance_url and # access_token p = sfdx("force:org:open -r", self.username, log_note="Refreshing OAuth token") stdout_list = [line.strip() for line in p.stdout_text] if p.returncode: self.logger.error(f"Return code: {p.returncode}") for line in stdout_list: self.logger.error(line) message = f"Message: {nl.join(stdout_list)}" raise ScratchOrgException(message)
def force_refresh_oauth_token(self): # Call force:org:display and parse output to get instance_url and # access_token p = sfdx("force:org:open -r", self.username, log_note="Refreshing OAuth token") stdout_list = [line.strip() for line in io.TextIOWrapper(p.stdout)] if p.returncode: self.logger.error("Return code: {}".format(p.returncode)) for line in stdout_list: self.logger.error(line) message = "Message: {}".format("\n".join(stdout_list)) raise ScratchOrgException(message)
def scratch_info(self): if hasattr(self, "_scratch_info"): return self._scratch_info # Create the org if it hasn't already been created if not self.created: self.create_org() self.logger.info("Getting scratch org info from Salesforce DX") username = self.config.get("username") if not username: raise ScratchOrgException( "SFDX claimed to be successful but there was no username " "in the output...maybe there was a gack?") # Call force:org:display and parse output to get instance_url and # access_token p = sfdx("force:org:display --json", self.username) org_info = None stderr_list = [line.strip() for line in p.stderr_text] stdout_list = [line.strip() for line in p.stdout_text] if p.returncode: self.logger.error("Return code: {}".format(p.returncode)) for line in stderr_list: self.logger.error(line) for line in stdout_list: self.logger.error(line) message = "\nstderr:\n{}".format("\n".join(stderr_list)) message += "\nstdout:\n{}".format("\n".join(stdout_list)) raise ScratchOrgException(message) else: try: org_info = json.loads("".join(stdout_list)) except Exception as e: raise ScratchOrgException( "Failed to parse json from output. This can happen if " "your scratch org gets deleted.\n " "Exception: {}\n Output: {}".format( e.__class__.__name__, "".join(stdout_list))) org_id = org_info["result"]["accessToken"].split("!")[0] if "password" in org_info["result"] and org_info["result"]["password"]: password = org_info["result"]["password"] else: password = self.config.get("password") self._scratch_info = { "instance_url": org_info["result"]["instanceUrl"], "access_token": org_info["result"]["accessToken"], "org_id": org_id, "username": org_info["result"]["username"], "password": password, } self.config.update(self._scratch_info) self._scratch_info_date = datetime.datetime.utcnow() return self._scratch_info
def create_org(self): """ Uses sfdx force:org:create to create the org """ if not self.config_file: # FIXME: raise exception return if not self.scratch_org_type: self.config["scratch_org_type"] = "workspace" # If the scratch org definition itself contains an `adminEmail` entry, # we don't want to override it from our own configuration, which may # simply come from the user's Git config. with open(self.config_file, "r") as org_def: org_def_data = json.load(org_def) org_def_has_email = "adminEmail" in org_def_data options = { "config_file": self.config_file, "devhub": " --targetdevhubusername {}".format(self.devhub) if self.devhub else "", "namespaced": " -n" if not self.namespaced else "", "days": " --durationdays {}".format(self.days) if self.days else "", "alias": sarge.shell_format(' -a "{0!s}"', self.sfdx_alias) if self.sfdx_alias else "", "email": sarge.shell_format('adminEmail="{0!s}"', self.email_address) if self.email_address and not org_def_has_email else "", "default": " -s" if self.default else "", "extraargs": os.environ.get("SFDX_ORG_CREATE_ARGS", ""), } # This feels a little dirty, but the use cases for extra args would mostly # work best with env vars command = "force:org:create -f {config_file}{devhub}{namespaced}{days}{alias}{default} {email} {extraargs}".format( **options) p = sfdx(command, username=None, log_note="Creating scratch org") stderr = [line.strip() for line in p.stderr_text] stdout = [line.strip() for line in p.stdout_text] if p.returncode: message = "{}: \n{}\n{}".format(FAILED_TO_CREATE_SCRATCH_ORG, "\n".join(stdout), "\n".join(stderr)) raise ScratchOrgException(message) re_obj = re.compile( "Successfully created scratch org: (.+), username: (.+)") for line in stdout: match = re_obj.search(line) if match: self.config["org_id"] = match.group(1) self.config["username"] = match.group(2) self.logger.info(line) for line in stderr: self.logger.error(line) self.config["date_created"] = datetime.datetime.now() if self.config.get("set_password"): self.generate_password() # Flag that this org has been created self.config["created"] = True
def test_check_return(self, Command): Command.return_value.returncode = 1 with pytest.raises(Exception) as exc_info: sfdx("cmd", check_return=True) assert str(exc_info.value) == "Command exited with return code 1"
def test_posix_quoting(self, Command): sfdx("cmd", args=["a'b"]) cmd = Command.call_args[0][0] assert cmd == r"sfdx cmd 'a'\''b'"
def create_org(self): """ Uses sfdx force:org:create to create the org """ if not self.config_file: raise ScratchOrgException( f"Scratch org config {self.name} is missing a config_file") if not self.scratch_org_type: self.config["scratch_org_type"] = "workspace" # If the scratch org definition itself contains an `adminEmail` entry, # we don't want to override it from our own configuration, which may # simply come from the user's Git config. with open(self.config_file, "r") as org_def: org_def_data = json.load(org_def) org_def_has_email = "adminEmail" in org_def_data devhub = self._choose_devhub() instance = self.instance or os.environ.get("SFDX_SIGNUP_INSTANCE") options = { "config_file": self.config_file, "devhub": f" --targetdevhubusername {devhub}" if devhub else "", "namespaced": " -n" if not self.namespaced else "", "days": f" --durationdays {self.days}" if self.days else "", "wait": " -w 120", "alias": sarge.shell_format(' -a "{0!s}"', self.sfdx_alias) if self.sfdx_alias else "", "email": sarge.shell_format(' adminEmail="{0!s}"', self.email_address) if self.email_address and not org_def_has_email else "", "default": " -s" if self.default else "", "instance": f" instance={instance}" if instance else "", "extraargs": os.environ.get("SFDX_ORG_CREATE_ARGS", ""), } # This feels a little dirty, but the use cases for extra args would mostly # work best with env vars command = "force:org:create -f {config_file}{devhub}{namespaced}{days}{alias}{default}{wait}{email}{instance} {extraargs}".format( **options) p = sfdx(command, username=None, log_note="Creating scratch org") stderr = [line.strip() for line in p.stderr_text] stdout = [line.strip() for line in p.stdout_text] if p.returncode: message = f"{FAILED_TO_CREATE_SCRATCH_ORG}: \n{nl.join(stdout)}\n{nl.join(stderr)}" raise ScratchOrgException(message) re_obj = re.compile( "Successfully created scratch org: (.+), username: (.+)") username = None for line in stdout: match = re_obj.search(line) if match: self.config["org_id"] = match.group(1) self.config["username"] = username = match.group(2) self.logger.info(line) for line in stderr: self.logger.error(line) if username is None: raise ScratchOrgException( "SFDX claimed to be successful but there was no username " "in the output...maybe there was a gack?") self.config["date_created"] = datetime.datetime.utcnow() if self.config.get("set_password"): self.generate_password() # Flag that this org has been created self.config["created"] = True
def retrieve_components( components, org_config, target: str, md_format: bool, extra_package_xml_opts: dict, namespace_tokenize: str, api_version: str, ): """Retrieve specified components from an org into a target folder. Retrieval is done using the sfdx force:source:retrieve command. Set `md_format` to True if retrieving into a folder with a package in metadata format. In this case the folder will be temporarily converted to dx format for the retrieval and then converted back. Retrievals to metadata format can also set `namespace_tokenize` to a namespace prefix to replace it with a `%%%NAMESPACE%%%` token. """ with contextlib.ExitStack() as stack: if md_format: # Create target if it doesn't exist if not os.path.exists(target): os.mkdir(target) touch(os.path.join(target, "package.xml")) # Inject namespace if namespace_tokenize: process_text_in_directory( target, functools.partial(inject_namespace, namespace=namespace_tokenize, managed=True), ) # Temporarily convert metadata format to DX format stack.enter_context(temporary_dir()) os.mkdir("target") # We need to create sfdx-project.json # so that sfdx will recognize force-app as a package directory. with open("sfdx-project.json", "w") as f: json.dump( { "packageDirectories": [{ "path": "force-app", "default": True }] }, f) sfdx( "force:mdapi:convert", log_note="Converting to DX format", args=["-r", target, "-d", "force-app"], check_return=True, ) # Construct package.xml with components to retrieve, in its own tempdir package_xml_path = stack.enter_context(temporary_dir(chdir=False)) _write_manifest(components, package_xml_path, api_version) # Retrieve specified components in DX format sfdx( "force:source:retrieve", access_token=org_config.access_token, log_note="Retrieving components", args=[ "-a", str(api_version), "-x", os.path.join(package_xml_path, "package.xml"), "-w", "5", ], capture_output=False, check_return=True, env={"SFDX_INSTANCE_URL": org_config.instance_url}, ) if md_format: # Convert back to metadata format sfdx( "force:source:convert", log_note="Converting back to metadata format", args=["-r", "force-app", "-d", target], capture_output=False, check_return=True, ) # Reinject namespace tokens if namespace_tokenize: process_text_in_directory( target, functools.partial(tokenize_namespace, namespace=namespace_tokenize), ) # Regenerate package.xml, # to avoid reformatting or losing package name/scripts package_xml_opts = { "directory": target, "api_version": api_version, **extra_package_xml_opts, } package_xml = PackageXmlGenerator(**package_xml_opts)() with open(os.path.join(target, "package.xml"), "w") as f: f.write(package_xml)
def test_windows_quoting(self, Command): sfdx("cmd", args=['a"b'], access_token="token") cmd = Command.call_args[0][0] assert cmd == r'sfdx cmd "a\"b" -u token'