Search code examples
pythonazureazure-functions

script not being recognized as function as a function app


i had a function app that had a python script running in it i was deploying it from vs code and it was working fine , now i created a new resource and a new function and i redid the same configuration , now i tried to deploy the same folder to that function app and everything is in the app files but the function is not recognized as a function is in the function app i have all the necessary files in the directory so what can the issue be ?

    import os
    import logging
    import requests
    import azure.functions as func
    from azure.identity import DefaultAzureCredential
    from azure.keyvault.secrets import SecretClient
    from opencensus.ext.azure.log_exporter import AzureLogHandler

# Global configuration
    EMAIL = os.getenv("EMAIL")
    KEY_VAULT_NAME = os.getenv("KEY_VAULT_NAME")
    SECRET_NAME = os.getenv("SECRET_NAME")
    ACTIVE_INSTANCE = os.getenv("ACTIVE_INSTANCE")
    LOGIC_APP_URL = os.getenv("LOGIC_APP_URL")

# Instance-specific configuration
BASE_URL = os.getenv("BASE_URL")
WORKSPACE_ID = os.getenv("WORKSPACE_ID")
OBJECT_TYPE_ID = os.getenv("OBJECT_TYPE_ID")
PROJECT_ID_ATTR = os.getenv("PROJECT_ID_ATTR")
PROJECT_KEY_ATTR = os.getenv("PROJECT_KEY_ATTR")
PROJECT_NAME_ATTR = os.getenv("PROJECT_NAME_ATTR")
PROJECT_DESCRIPTION_ATTR = os.getenv("PROJECT_DESCRIPTION_ATTR")
PROJECT_CATEGORY_ATTR = os.getenv("PROJECT_CATEGORY_ATTR")
PROJECT_TYPEKEY_ATTR = os.getenv("PROJECT_TYPEKEY_ATTR")
PROJECT_LEAD_ATTR = os.getenv("PROJECT_LEAD_ATTR")
PROJECT_LEADID_ATTR = os.getenv("PROJECT_LEADID_ATTR")
PROJECT_SIMPLIFIED_ATTR = os.getenv("PROJECT_SIMPLIFIED_ATTR")
PROJECT_STYLED_ATTR = os.getenv("PROJECT_STYLED_ATTR")
PROJECT_ISPRIVATE_ATTR = os.getenv("PROJECT_ISPRIVATE_ATTR")
PROJECT_TOTALISSUECOUNT_ATTR = os.getenv("PROJECT_TOTALISSUECOUNT_ATTR")
PROJECT_LASTISSUEUPDATE_ATTR = os.getenv("PROJECT_LASTISSUEUPDATE_ATTR")
PROJECT_ARCHIVEDDATE_ATTR = os.getenv("PROJECT_ARCHIVEDDATE_ATTR")

# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(AzureLogHandler(connection_string='InstrumentationKey=42d4e759-407c-4774-8869-ef7343513438'))

def get_secret_from_key_vault(key_vault_name, secret_name):
    """Retrieve a secret from Azure Key Vault."""
    kv_uri = f"https://{key_vault_name}.vault.azure.net"
    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=kv_uri, credential=credential)
    secret = client.get_secret(secret_name)
    return secret.value



def send_error_report(error_message):
    """Send an error report via Logic App."""
    payload = {
        "To": "[email protected]",
        "CC": "",
        "Response": "No",
        "Subject": "Jira Authentication Error",
        "Body": f"An error occurred during Jira authentication: {error_message}"
    }
    try:
        response = requests.post(LOGIC_APP_URL, json=payload)
        response.raise_for_status()
        logger.info("Error report sent successfully.")
    except requests.exceptions.RequestException as e:
        logger.error(f"Failed to send error report: {e}")


def authenticate_jira(base_url, email, api_token):
    """Authenticate with the Jira Cloud instance."""
    session = requests.Session()
    session.auth = (email, api_token)
    session.headers.update({'Content-Type': 'application/json'})
    try:
        response = session.get(f"{base_url}/rest/api/3/myself")
        response.raise_for_status()
        logger.info("Authentication successful.")
    except requests.exceptions.RequestException as e:
        logger.error(f"Failed to authenticate: {e}")
        #send_error_report(str(e))  # Call the Logic App to send an error report
        raise e
    return session



def get_jira_projects(session):
    """Fetch all Jira projects using the paginated API."""
    logger.info("Fetching Jira projects")
    url = f"{BASE_URL}/rest/api/3/project/search"
    params = {"expand": "description,lead,issueTypes,url,projectKeys,permissions,insight"}
    projects = []
    
    while url:
        try:
            response = session.get(url, params=params)
            if response.status_code != 200:
                logger.error(f"Error fetching Jira projects: {response.status_code} {response.text}")
                response.raise_for_status()
            data = response.json()
            projects.extend(data.get("values", []))
            url = data.get("nextPage")  # Continue pagination
        except requests.exceptions.RequestException as e:
            logger.error(f"Error fetching Jira projects: {e}")
            break
    
    logger.info(f"Fetched {len(projects)} projects")
    for project in projects:
        logger.info(f"Fetched project: {project.get('id')} - {project.get('key')} - {project.get('name')}")
    
    return projects

def search_asset_object(session, project_id):
    """Search for an asset object using AQL."""
    url = f"https://api.atlassian.com/jsm/assets/workspace/{WORKSPACE_ID}/v1/object/aql"
    query = {
        "qlQuery": f'objectType = "Jira Project" AND "Project ID" = "{project_id}"'
    }
    try:
        response = session.post(url, json=query)
        response.raise_for_status()
        return response.json().get("values", [])
    except requests.exceptions.RequestException as e:
        logger.error(f"Error searching asset object for project {project_id}: {e}")
        return []

def create_asset_object(session, project_data):
    """Create a new asset object."""
    url = f"https://api.atlassian.com/jsm/assets/workspace/{WORKSPACE_ID}/v1/object/create"
    payload = format_payload(project_data)
    
    try:
        response = session.post(url, json=payload)
        response.raise_for_status()
        logger.info(f"Asset created for project {project_data['id']} ({project_data['key']})")
    except requests.exceptions.RequestException as e:
        logger.error(f"Error creating asset object for project {project_data['id']}: {e}")

def update_asset_object(session, asset_id, project_data):
    """Update an existing asset object."""
    url = f"https://api.atlassian.com/jsm/assets/workspace/{WORKSPACE_ID}/v1/object/{asset_id}"
    payload = format_payload(project_data)
    
    try:
        response = session.put(url, json=payload)
        response.raise_for_status()
        logger.info(f"Asset updated for project {project_data['id']} ({project_data['key']})")
    except requests.exceptions.RequestException as e:
        logger.error(f"Error updating asset object {asset_id} for project {project_data['id']}: {e}")

def format_payload(project):
    """Format payload for asset creation or update based on configuration."""
    last_issue_update_time = project.get("insight", {}).get("lastIssueUpdateTime")
    formatted_last_issue_update_time = None

    if last_issue_update_time:
        formatted_last_issue_update_time = last_issue_update_time.split("T")[0]

    description_long = project.get("description")
    shortened_description = None

    if description_long:
        shortened_description = description_long[:255]
    
    return {
        "objectTypeId": OBJECT_TYPE_ID,
        "attributes": [
            {"objectTypeAttributeId": PROJECT_ID_ATTR, 
             "objectAttributeValues": [{"value": project.get("id")}]},
            {"objectTypeAttributeId": PROJECT_KEY_ATTR, 
             "objectAttributeValues": [{"value": project.get("key")}]},
            {"objectTypeAttributeId": PROJECT_NAME_ATTR, 
             "objectAttributeValues": [{"value": project.get("name")}]},
            {"objectTypeAttributeId": PROJECT_DESCRIPTION_ATTR, 
             "objectAttributeValues": [{"value": shortened_description}]},
            {"objectTypeAttributeId": PROJECT_CATEGORY_ATTR, 
             "objectAttributeValues": [{"value": project.get("projectCategory", {}).get("name")}]},
            {"objectTypeAttributeId": PROJECT_TYPEKEY_ATTR, 
             "objectAttributeValues": [{"value": project.get("projectTypeKey")}]},
            {"objectTypeAttributeId": PROJECT_LEAD_ATTR, 
             "objectAttributeValues": [{"value": project.get("lead", {}).get("accountId")}]},
            {"objectTypeAttributeId": PROJECT_LEADID_ATTR, 
             "objectAttributeValues": [{"value": project.get("lead", {}).get("accountId")}]},
            {"objectTypeAttributeId": PROJECT_SIMPLIFIED_ATTR, 
             "objectAttributeValues": [{"value": project.get("simplified")}]},
            {"objectTypeAttributeId": PROJECT_STYLED_ATTR, 
             "objectAttributeValues": [{"value": project.get("style")}]},
            {"objectTypeAttributeId": PROJECT_ISPRIVATE_ATTR, 
             "objectAttributeValues": [{"value": project.get("isPrivate")}]},
            {"objectTypeAttributeId": PROJECT_TOTALISSUECOUNT_ATTR, 
             "objectAttributeValues": [{"value": project.get("insight", {}).get("totalIssueCount")}]},
            {"objectTypeAttributeId": PROJECT_LASTISSUEUPDATE_ATTR, 
             "objectAttributeValues": [{"value": formatted_last_issue_update_time}]},
            {"objectTypeAttributeId": PROJECT_ARCHIVEDDATE_ATTR, 
             "objectAttributeValues": [{"value": project.get("archivedDate")}]}
        ]
    }

def sync_jira_main():
    """Main logic to process Jira projects and sync with assets."""
    logger.info("Starting Jira Projects to Asset synchronization")
    try:
     # Retrieve the API token from Key Vault
     API_TOKEN = get_secret_from_key_vault(KEY_VAULT_NAME, SECRET_NAME)
    except Exception as e:
        message = f"Error fetching API token: {e}"
        logger.error(message)
        raise Exception(message)
    try:
     # Authenticate and create a session
     session = authenticate_jira(BASE_URL, EMAIL, API_TOKEN)
     
    except Exception as e:
        send_error_report(str(e))
        message = f"Error fetching Jira projects: {e}"
        logger.error(message)
        raise Exception(message)
    

    projects = get_jira_projects(session)
    for project in projects:
        project_id = project.get("id")
        if not project_id:
            logger.warning("Project without ID encountered, skipping.")
            continue
        
        existing_assets = search_asset_object(session, project_id)
        
        if len(existing_assets) == 0:
            create_asset_object(session, project)
        elif len(existing_assets) > 1:
            logger.error(f"Multiple asset objects found for project {project_id} ({project.get('key')}).")
        else:
            update_asset_object(session, existing_assets[0].get("id"), project)
    
    logger.info("Jira Projects to Asset synchronization completed.")

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

@app.route(route="leanjira_script", methods=["GET", "POST"])
def leansyncjira_script(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    # Call the sync_jira main function
    sync_jira_main()

    return func.HttpResponse("Synchronization completed.", status_code=200)


Solution

    • I have used the same code provided by you without any modification to it and requirement.txt file has below modules in it.
    azure-functions
    azure-identity
    azure-keyvault-secrets
    opencensus-ext-azure
    
    • Make sure, you have below folder structure when you create the project locally.

    enter image description here

    • Also, add all the environment variables in function app by navigating to Settings ->Environment Variables -> App settings .

    • I am able to deploy the code successfully to function app.

    enter image description here

    • I can see the function endpoint in function app as shown below.

    enter image description here