我目前正在从事一个 QA 自动化项目,特别是实施一个在执行完成后删除测试环境的解决方案。我有一个名为
setup.py
的 Python 脚本,它可以在我的本地环境中有效地执行此任务。该脚本正确安排了测试环境的删除。
为了提供更多详细信息,该脚本创建测试环境,然后触发 delete_organization 函数,该函数会自动安排在当天晚上晚些时候删除测试环境。该脚本在 CircleCI 上自动执行。
这个文件还将数据写入到
.env
文件中,这个文件看起来像这样,它执行的所有内容都是这样的
ORGANIZATION=Pedro
SCHOOL_YEAR=2024
APP_BASE_URL=https://qaa-kristen-richards-1719497724.dev.company.com
API_BASE_URL=https://api.dev.company.com
USER_PASSWORD=Xno2R0c5X3BPNEpw
API_USER_PASSWORD=Qm1hM0tPIXBpKSYr
setup.py
文件
"""
Setup
This script helps to create a new organization and generates a .env file with
the base URLs of Enroll and Sync Service products.
"""
import time
from enroll.common import create_env_file_content
from enroll.organization import default_organization_data, CreateOrganizationService
from enroll.common.utils.password import decode_password
from enroll.common.api.base_api import BaseApi
import os
import datetime
import requests
from argparse import ArgumentParser
def create_organization(payload, env):
return CreateOrganizationService(env).perform(payload)
def four_digit_argument(value):
ivalue = int(value)
if ivalue < 1000 or ivalue > 9999:
raise Exception("%s is not a valid 4-digit integer")
return ivalue
if __name__ == '__main__':
organization_data_payload = default_organization_data()
parser = ArgumentParser()
parser.add_argument(
'-e',
'--env',
default='dev',
type=str,
required=False,
help='Environment to setup the Golden Master to trigger the Suite against it',
choices=('sandbox', 'dev', 'staging', 'test', 'performance', 'local', 'demand'),
)
parser.add_argument(
'-o',
'--org',
default='',
type=str,
required=False,
help='Organization to setup the Golden Master to trigger the Suite against it',
)
parser.add_argument(
'-y',
'--year',
default=0,
type=four_digit_argument,
required=False,
help='Organization to setup the Golden Master to trigger the Suite against it',
)
parser.add_argument(
'-p',
'--password',
default=organization_data_payload['automation_password'],
required=False,
help='Automation user password for the created organization'
)
parser.add_argument(
'-ap',
'--apipassword',
default=organization_data_payload['automationapi_password'],
required=False,
help='Automation Api user password for the created organization'
)
args = parser.parse_args()
## running the org on a pre existing org is determined only by the presence of both parameters, org and year.
## when both are given it is assumed that the organization exists in the specified environment
is_a_pre_existing_org = args.org != '' and args.year != 0
## by default use the organization data payload if is not a pre-existing-org
if not is_a_pre_existing_org:
args.year = organization_data_payload['school_years'][0]
## only set the org name for not pre existing orgs if it was not given by parameter,
## there is a valid scenario of specifying the org name for the creation (by omiting the year)
if args.org == '':
args.org = organization_data_payload['subdomain']
organization_data_payload['subdomain'] = args.org
organization_data_payload['school_years'] = [args.year, args.year - 1]
# when the organization already exists
# then we skip the creation step, the password for the automation user
# is pre-defined, later we could improve this part by allowing the script
# to automatically create the [email protected] and
# [email protected] users automatically when those doesn't exist already
if is_a_pre_existing_org:
args.password = decode_password('U1VoQ1ZVNVhaRllvY3pCQQ==')
args.apipassword = decode_password('U1VoQ1ZVNVhaRllvY3pCQQ==')
else:
response = create_organization(organization_data_payload, args.env)
if response.status_code != 200:
raise Exception(
"An error occurred while creating a new organization", response)
env_file_content = create_env_file_content(
args.env, args.org, args.year, args.password, args.apipassword)
env_file = open('.env', 'w')
env_file.write(env_file_content)
env_file.close()
# Wait for the data to be created
if args.env != 'local':
time.sleep(50)
api_client = BaseApi() # Create an instance of the BaseApi class
api_client.authenticate_account(credentials={'username': '[email protected]', 'password': decode_password(os.environ.get('USER_PASSWORD'))})
if BaseApi._current_username:
username = BaseApi._current_username
token = BaseApi._tokens.get(username)
def get_org_id(token):
url = f"{os.environ.get('API_BASE_URL')}/secret/api/because/company/policies/{os.environ.get('ORGANIZATION')}"
headers = {
"Authorization": f"Bearer {token}",
"organization": os.environ.get('ORGANIZATION'),
"yr": os.environ.get('SCHOOL_YEAR'),
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # Raise an exception for non-2xx status codes
data = response.json() # Parse the JSON response
# Extract the value of the "id" key
if "id" in data["data"]:
return data["data"]["id"]
else:
print(f'Key "id" not found in response data')
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
def delete_organization():
current_datetime = datetime.datetime.now()
formatted_date = current_datetime.strftime("%Y-%m-%d")
url = "https://123456.execute-api.us-east-2.amazonaws.com/schedule/qaa"
headers = {
"X-KEY": "slkjfdksh2378374"
}
data = {
"org_id": get_org_id(token),
"org_subdomain": os.environ.get('ORGANIZATION'),
"org_namespace": os.environ.get('ORGANIZATION'),
"date_to_remove": formatted_date,
"platform_stage": "development",
"user": "[email protected]",
"environment": "development"
}
response = requests.post(url, headers=headers, json=data)
if response.status_code == 200:
print("The organization will be deleted tonight.")
else:
error_message = response.json().get('message', "Unknown error")
print(f"Error deleting organization: {error_message}")
delete_organization()
问题依赖于 CicleCI 的执行,它抛出此错误
ENVIRONMENT: dev
ORGANIZATION: -
YEAR: -
Traceback (most recent call last):
File "setup.py", line 121, in <module>
api_client.authenticate_account(credentials={'username': '[email protected]', 'password': decode_password(os.environ.get('USER_PASSWORD'))})
File "/opt/automation/enroll/common/utils/password.py", line 14, in decode_password
byte_password = bytes(password, encoding='UTF-8')
TypeError: encoding without a string argument
Exited with code exit status 1
password.py
文件看起来是这样的
import base64
def decode_password(password: str) -> str:
"""
Decode a base64-encoded password string.
Args:
password (str): The base64-encoded password string to be decoded.
Returns:
str: The decoded password string.
"""
byte_password = bytes(password, encoding='UTF-8')
return base64.b64decode(byte_password).decode('UTF-8')
def encode_password(password: str) -> str:
"""
Encode a password string using base64 encoding.
Args:
password (str): The password string to be encoded.
Returns:
str: The base64-encoded password string.
"""
byte_password = password.encode('ascii')
return base64.b64encode(byte_password).decode('UTF-8')
CircleCI 执行的
config_continuation.yml
看起来像这样
version: 2.1
orbs:
slack: circleci/[email protected]
parameters:
parallelism:
type: integer
default: 4
jobs:
build:
parallelism: << pipeline.parameters.parallelism >>
working_directory: /opt/automation
docker:
- image: enggaccounts/pytest-base-ci:0.0.2
auth:
username: $DOCKERHUB_USER
password: $DOCKERHUB_PASSWORD
environment:
USER_PASSWORD: your_password_here # Add your password here
steps:
- run:
name: Figuring Enroll Data
# it will figure:
# environment,
# organization,
# year,
# all based on a branch naming convention
# by default if the standard isn't followed it will assume the defaults
# (defaults implies creating a new org on the development environment and running the suite for current year)
# the naming convention is as follows:
# ENROLL_123_mycustom_automation_branch_ENV_demand_ORG_mycustom-org-123_YEAR_2023
# what matters is what comes after _ENV_
command: |
if echo $CIRCLE_BRANCH | grep -q "_ENV_"; then
params_str="${CIRCLE_BRANCH#*_ENV_}"
env=${params_str/_*}
org=''
year=''
is_custom='0'
if echo $CIRCLE_BRANCH | grep -q "_ORG_"; then
params_str="${CIRCLE_BRANCH#*_ORG_}"
org="${params_str/_*}"
fi
if echo $CIRCLE_BRANCH | grep -q "_YEAR_"; then
params_str="${CIRCLE_BRANCH#*_YEAR_}"
year="${params_str/_*}"
fi
if echo $CIRCLE_BRANCH | grep -q "_CUSTOM"; then
is_custom='1'
echo "export IS_CUSTOM=${is_custom}" >> "$BASH_ENV"
fi
if [ ${#env} -gt 0 ]; then
echo "$env"
echo "export ENROLL_ENVIRONMENT=${env}" >> "$BASH_ENV"
else
echo "Custom branch pattern detected but ENV was not defined!"
fi
if [ ${#org} -gt 0 ]; then
echo "$org"
echo "export ENROLL_ORGANIZATION=${org}" >> "$BASH_ENV"
else
echo "Custom branch pattern detected but ORG was not defined!"
fi
if [ ${#year} -gt 0 ]; then
echo "$year"
echo "export ENROLL_YEAR=${year}" >> "$BASH_ENV"
else
echo "Custom branch pattern detected but YEAR was not defined!"
fi
else
echo "$env"
echo 'export ENROLL_ENVIRONMENT="dev"' >> "$BASH_ENV"
fi
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v2-dependencies-{{ checksum "Pipfile.lock" }}
# fallback to using the latest cache if no exact match is found
- v2-dependencies-
- run:
name: Install pipenv
command: sudo apt install -y pipenv
- run:
name: Install dependencies
command: pipenv install
- run:
# SETUP the suite to run as specified,
name: Setup the organization
command: |
if [ ${#ENROLL_ENVIRONMENT} -gt 0 ] && [ ${#ENROLL_ORGANIZATION} -gt 0 ] && [ ${#ENROLL_YEAR} -gt 0 ]; then
echo "ENVIRONMENT: ${ENROLL_ENVIRONMENT}"
echo "ORGANIZATION: ${ENROLL_ORGANIZATION}"
echo "YEAR: ${ENROLL_YEAR}"
pipenv run python3 setup.py -e ${ENROLL_ENVIRONMENT} -o ${ENROLL_ORGANIZATION} -y ${ENROLL_YEAR}
elif [ ${#ENROLL_ENVIRONMENT} -gt 0 ]; then
echo "ENVIRONMENT: ${ENROLL_ENVIRONMENT}"
echo "ORGANIZATION: -"
echo "YEAR: -"
pipenv run python3 setup.py -e ${ENROLL_ENVIRONMENT}
else
echo "ENVIRONMENT: -"
echo "ORGANIZATION: -"
echo "YEAR: -"
pipenv run python3 setup.py
fi
- run:
name: Organization information
command: |
ORGANIZATION=$(grep 'ORGANIZATION=' .env | cut -d '=' -f2-)
SCHOOL_YEAR=$(grep 'SCHOOL_YEAR=' .env | cut -d '=' -f2-)
APP_BASE_URL=$(grep 'APP_BASE_URL=' .env | cut -d '=' -f2-)
echo "Organization: $ORGANIZATION"
echo "School Year: $SCHOOL_YEAR"
echo "App base url: $APP_BASE_URL"
- save_cache:
key: v2-dependencies-{{ checksum "Pipfile.lock" }}
paths:
- "~/.cache/pipenv"
- run:
name: Run tests
command: |
set -e
TEST_FILES=$(circleci tests glob "enroll/**/tests/*.py" | circleci tests split --split-by=timings)
mkdir -p reports
if [[ "$IS_CUSTOM" == 1 ]]; then
pipenv run pytest $TEST_FILES --html=reports/index.html --junitxml=reports/junit.xml -m custom
else
pipenv run pytest $TEST_FILES --html=reports/index.html --junitxml=reports/junit.xml
fi
- store_test_results:
path: reports
# Save artifacts
- store_artifacts:
path: reports
# if you want to modify slack notification template
# https://api.slack.com/reference/messaging/attachments
# we are using attachments in order to help us see each message within it's own section
- slack/notify:
event: pass
custom: |
{
"attachments": [
{
"color": "#1cbf43",
"fallback": "A $CIRCLE_JOB job has succeeded!",
"text": "<https://github.com/$CIRCLE_USERNAME|$CIRCLE_USERNAME> A `$CIRCLE_JOB` job has succeeded!",
"fields": [
{
"title": "Project",
"value": "$CIRCLE_PROJECT_REPONAME",
"short": true
},
{
"title": "Branch",
"value": "$CIRCLE_BRANCH (<$CIRCLE_PULL_REQUEST|PR>)",
"short": true
}
],
"actions": [
{
"type": "button",
"text": "Job $CIRCLE_BUILD_NUM",
"url": "$CIRCLE_BUILD_URL"
}
]
}
]
}
- slack/notify:
event: fail
custom: | # including template here so it is easier to customize
{
"attachments": [
{
"color": "#ed5c5c",
"fallback": "A $CIRCLE_JOB job has failed. $CIRCLE_PR_USERNAME",
"text": "<https://github.com/$CIRCLE_USERNAME|$CIRCLE_USERNAME> A `$CIRCLE_JOB` job has failed!",
"fields": [
{
"title": "Project",
"value": "$CIRCLE_PROJECT_REPONAME",
"short": true
},
{
"title": "Branch",
"value": "$CIRCLE_BRANCH (<$CIRCLE_PULL_REQUEST|PR>)",
"short": true
}
],
"actions": [
{
"type": "button",
"text": "Job $CIRCLE_BUILD_NUM",
"url": "$CIRCLE_BUILD_URL"
}
]
}
]
}
workflows:
build-workflow:
jobs:
- build
config.yml
看起来像这样
version: 2.1
setup: true
orbs:
continuation: circleci/[email protected]
jobs:
determine-parallelism:
docker:
- image: cimg/base:edge
resource_class: medium
steps:
- checkout
- run:
name: Determine parallelism
command: |
# Put command/logic here to determine parallelism
echo "determining"
if echo $CIRCLE_BRANCH | grep -q "_ENV_" &&
echo $CIRCLE_BRANCH | grep -q "_ORG_" &&
echo $CIRCLE_BRANCH | grep -q "_YEAR_"; then
echo "lets use 1"
PARALLELISM=1
else
echo "lets use 4"
PARALLELISM=4
fi
echo "lets create file"
echo "{\"parallelism\": $PARALLELISM}" > pipeline-parameters.json
- continuation/continue:
configuration_path: .circleci/config_continuation.yml
parameters: pipeline-parameters.json
workflows:
build-setup:
jobs:
- determine-parallelism
如何修复此错误以使其在 CircleCI 上也能正常工作?请注意,无法在 YAML 文件中对密码进行硬编码,因为每次执行的密码都不同。 CircleCI 上的每次执行都会创建四个容器,每个容器都有不同的测试环境,以实现并行执行。我想我需要以某种方式传递这个值存储在
decode_password(os.environ.get('USER_PASSWORD'))
中的参数
idk但我从circleci收到错误,我认为可能是config.yml文件找不到满足要求的版本pipenv==2021.5.29(来自版本:)没有找到pipenv==2021.5的匹配发行版。 29
对于您的错误,您可能需要查看此
针对“TypeError:没有字符串参数的编码”错误