fabric-cicd 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fabric_cicd-0.1.0/LICENSE +21 -0
- fabric_cicd-0.1.0/PKG-INFO +71 -0
- fabric_cicd-0.1.0/README.md +35 -0
- fabric_cicd-0.1.0/VERSION +1 -0
- fabric_cicd-0.1.0/pyproject.toml +39 -0
- fabric_cicd-0.1.0/setup.cfg +4 -0
- fabric_cicd-0.1.0/src/fabric_cicd/__init__.py +54 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_common/__init__.py +3 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_common/_exceptions.py +29 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_common/_fabric_endpoint.py +264 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_common/_logging.py +88 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_common/_validate_input.py +112 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_items/__init__.py +20 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_items/_datapipeline.py +155 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_items/_environment.py +184 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_items/_notebook.py +18 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_items/_report.py +18 -0
- fabric_cicd-0.1.0/src/fabric_cicd/_items/_semanticmodel.py +18 -0
- fabric_cicd-0.1.0/src/fabric_cicd/fabric_workspace.py +442 -0
- fabric_cicd-0.1.0/src/fabric_cicd/publish.py +169 -0
- fabric_cicd-0.1.0/src/fabric_cicd.egg-info/PKG-INFO +71 -0
- fabric_cicd-0.1.0/src/fabric_cicd.egg-info/SOURCES.txt +23 -0
- fabric_cicd-0.1.0/src/fabric_cicd.egg-info/dependency_links.txt +1 -0
- fabric_cicd-0.1.0/src/fabric_cicd.egg-info/requires.txt +4 -0
- fabric_cicd-0.1.0/src/fabric_cicd.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: fabric-cicd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Microsoft Fabric CI/CD
|
|
5
|
+
Author: Microsoft Corporation
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) Microsoft Corporation.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/microsoft/fabric-cicd.git
|
|
29
|
+
Requires-Python: <3.13,>=3.9
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: azure-identity>=1.19.0
|
|
33
|
+
Requires-Dist: colorlog>=6.9.0
|
|
34
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
35
|
+
Requires-Dist: requests>=2.32.3
|
|
36
|
+
|
|
37
|
+
# Fabric CICD
|
|
38
|
+
|
|
39
|
+
[](https://www.python.org/)
|
|
40
|
+
[](https://badge.fury.io/py/fabric-cicd)
|
|
41
|
+
[](https://pypi.org/project/fabric-cicd/)
|
|
42
|
+
[](https://github.com/charliermarsh/ruff)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Project Overview
|
|
47
|
+
|
|
48
|
+
fabric-cicd is a Python library designed for use with [Microsoft Fabric](https://learn.microsoft.com/en-us/fabric/) workspaces. This library supports code-first Continuous Integration / Continuous Deployment (CI/CD) automations to seamlessly integrate Source Controlled workspaces into a deployment framework. The goal is to assist CI/CD developers who prefer not to interact directly with the Microsoft Fabric APIs.
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
All documentation is hosted on our [fabric-cicd](https://microsoft.github.io/fabric-cicd/) GitHub Pages
|
|
53
|
+
|
|
54
|
+
Section Overview:
|
|
55
|
+
- [Home](https://microsoft.github.io/fabric-cicd/latest/)
|
|
56
|
+
- [How To](https://microsoft.github.io/fabric-cicd/latest/how_to/)
|
|
57
|
+
- [Contribution](https://microsoft.github.io/fabric-cicd/latest/contribution/)
|
|
58
|
+
- [Changelog](https://microsoft.github.io/fabric-cicd/latest/changelog/)
|
|
59
|
+
- [About](https://microsoft.github.io/fabric-cicd/latest/help/) - Inclusive of Support & Security Policies
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
To install fabric-cicd, run:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install fabric-cicd
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Trademarks
|
|
70
|
+
|
|
71
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Fabric CICD
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://badge.fury.io/py/fabric-cicd)
|
|
5
|
+
[](https://pypi.org/project/fabric-cicd/)
|
|
6
|
+
[](https://github.com/charliermarsh/ruff)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Project Overview
|
|
11
|
+
|
|
12
|
+
fabric-cicd is a Python library designed for use with [Microsoft Fabric](https://learn.microsoft.com/en-us/fabric/) workspaces. This library supports code-first Continuous Integration / Continuous Deployment (CI/CD) automations to seamlessly integrate Source Controlled workspaces into a deployment framework. The goal is to assist CI/CD developers who prefer not to interact directly with the Microsoft Fabric APIs.
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
All documentation is hosted on our [fabric-cicd](https://microsoft.github.io/fabric-cicd/) GitHub Pages
|
|
17
|
+
|
|
18
|
+
Section Overview:
|
|
19
|
+
- [Home](https://microsoft.github.io/fabric-cicd/latest/)
|
|
20
|
+
- [How To](https://microsoft.github.io/fabric-cicd/latest/how_to/)
|
|
21
|
+
- [Contribution](https://microsoft.github.io/fabric-cicd/latest/contribution/)
|
|
22
|
+
- [Changelog](https://microsoft.github.io/fabric-cicd/latest/changelog/)
|
|
23
|
+
- [About](https://microsoft.github.io/fabric-cicd/latest/help/) - Inclusive of Support & Security Policies
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
To install fabric-cicd, run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install fabric-cicd
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Trademarks
|
|
34
|
+
|
|
35
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fabric-cicd"
|
|
3
|
+
authors = [{ name = "Microsoft Corporation" }]
|
|
4
|
+
description = "Microsoft Fabric CI/CD"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9,<3.13"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
urls.Repository = "https://github.com/microsoft/fabric-cicd.git"
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
"azure-identity>=1.19.0",
|
|
13
|
+
"colorlog>=6.9.0",
|
|
14
|
+
"pyyaml>=6.0.2",
|
|
15
|
+
"requests>=2.32.3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"gitpython>=3.1.44",
|
|
22
|
+
"mike>=2.1.3",
|
|
23
|
+
"mkdocs-include-markdown-plugin>=7.1.2",
|
|
24
|
+
"mkdocs-minify-plugin>=0.8.0",
|
|
25
|
+
"mkdocstrings-python>=1.13.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["setuptools>=61.0"]
|
|
31
|
+
build-backend = "setuptools.build_meta"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
dynamic.version = { file = "VERSION" }
|
|
35
|
+
packages.find.where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.uv]
|
|
38
|
+
package = true
|
|
39
|
+
python-preference = "only-managed"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Provides tools for managing and publishing items in a Fabric workspace."""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from fabric_cicd._common._logging import configure_logger, exception_handler
|
|
10
|
+
from fabric_cicd.fabric_workspace import FabricWorkspace
|
|
11
|
+
from fabric_cicd.publish import publish_all_items, unpublish_all_orphan_items
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def change_log_level(level: str = "DEBUG") -> None:
|
|
17
|
+
"""
|
|
18
|
+
Sets the log level for all loggers within the fabric_cicd package. Currently only supports DEBUG.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
level : str
|
|
23
|
+
The logging level to set (e.g., DEBUG).
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
Basic usage
|
|
28
|
+
>>> from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level
|
|
29
|
+
>>> change_log_level("DEBUG")
|
|
30
|
+
>>> workspace = FabricWorkspace(
|
|
31
|
+
... workspace_id="your-workspace-id",
|
|
32
|
+
... repository_directory="/path/to/repo",
|
|
33
|
+
... item_type_in_scope=["Environment", "Notebook", "DataPipeline"]
|
|
34
|
+
... )
|
|
35
|
+
>>> publish_all_items(workspace)
|
|
36
|
+
>>> unpublish_orphaned_items(workspace)
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
if level.upper() == "DEBUG":
|
|
40
|
+
configure_logger(logging.DEBUG)
|
|
41
|
+
logger.info("Changed log level to DEBUG")
|
|
42
|
+
else:
|
|
43
|
+
logger.warning(f"Log level '{level}' not supported. Only DEBUG is supported at this time. No changes made.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
configure_logger()
|
|
47
|
+
sys.excepthook = exception_handler
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"FabricWorkspace",
|
|
51
|
+
"change_log_level",
|
|
52
|
+
"publish_all_items",
|
|
53
|
+
"unpublish_all_orphan_items",
|
|
54
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseCustomError(Exception):
|
|
6
|
+
def __init__(self, message, logger, additional_info=None):
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
self.logger = logger
|
|
9
|
+
self.additional_info = additional_info
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ParsingError(BaseCustomError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InputError(BaseCustomError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenError(BaseCustomError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvokeError(BaseCustomError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ItemDependencyError(BaseCustomError):
|
|
29
|
+
pass
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from azure.core.exceptions import (
|
|
12
|
+
ClientAuthenticationError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from fabric_cicd._common._exceptions import InvokeError, TokenError
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FabricEndpoint:
|
|
21
|
+
"""Handles interactions with the Fabric API, including authentication and request management."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, token_credential):
|
|
24
|
+
"""Initializes the FabricEndpoint instance, sets up the authentication token."""
|
|
25
|
+
self.aad_token = None
|
|
26
|
+
self.aad_token_expiration = None
|
|
27
|
+
self.token_credential = token_credential
|
|
28
|
+
self._refresh_token()
|
|
29
|
+
|
|
30
|
+
def invoke(self, method, url, body="{}", files=None):
|
|
31
|
+
"""
|
|
32
|
+
Sends an HTTP request to the specified URL with the given method and body.
|
|
33
|
+
|
|
34
|
+
:param method: HTTP method to use for the request (e.g., 'GET', 'POST', 'PATCH', 'DELETE').
|
|
35
|
+
:param url: URL to send the request to.
|
|
36
|
+
:param body: The JSON body to include in the request. Defaults to an empty JSON object.
|
|
37
|
+
:param files: The file path to be included in the request. Defaults to None.
|
|
38
|
+
:return: A dictionary containing the response headers, body, and status code.
|
|
39
|
+
"""
|
|
40
|
+
exit_loop = False
|
|
41
|
+
iteration_count = 0
|
|
42
|
+
long_running = False
|
|
43
|
+
|
|
44
|
+
while not exit_loop:
|
|
45
|
+
try:
|
|
46
|
+
if files is None:
|
|
47
|
+
headers = {
|
|
48
|
+
"Authorization": f"Bearer {self.aad_token}",
|
|
49
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
50
|
+
}
|
|
51
|
+
response = requests.request(method=method, url=url, headers=headers, json=body)
|
|
52
|
+
|
|
53
|
+
else:
|
|
54
|
+
headers = {"Authorization": f"Bearer {self.aad_token}"}
|
|
55
|
+
response = requests.request(method=method, url=url, headers=headers, files=files)
|
|
56
|
+
|
|
57
|
+
iteration_count += 1
|
|
58
|
+
|
|
59
|
+
invoke_log_message = _format_invoke_log(response, method, url, body)
|
|
60
|
+
|
|
61
|
+
# Handle long-running operations
|
|
62
|
+
# https://learn.microsoft.com/en-us/rest/api/fabric/core/long-running-operations/get-operation-result
|
|
63
|
+
if (response.status_code == 200 and long_running) or response.status_code == 202:
|
|
64
|
+
url = response.headers.get("Location")
|
|
65
|
+
method = "GET"
|
|
66
|
+
body = "{}"
|
|
67
|
+
response_json = response.json()
|
|
68
|
+
|
|
69
|
+
if long_running:
|
|
70
|
+
status = response_json.get("status")
|
|
71
|
+
if status == "Succeeded":
|
|
72
|
+
long_running = False
|
|
73
|
+
exit_loop = True
|
|
74
|
+
elif status == "Failed":
|
|
75
|
+
response_error = response_json["error"]
|
|
76
|
+
msg = f"Operation failed. Error Code: {response_error['errorCode']}. Error Message: {response_error['message']}"
|
|
77
|
+
raise Exception(msg)
|
|
78
|
+
elif status == "Undefined":
|
|
79
|
+
msg = f"Operation is in an undefined state. Full Body: {response_json}"
|
|
80
|
+
raise Exception(msg)
|
|
81
|
+
else:
|
|
82
|
+
retry_after = float(response.headers.get("Retry-After", 0.5))
|
|
83
|
+
logger.info(f"Operation in progress. Checking again in {retry_after} seconds.")
|
|
84
|
+
time.sleep(retry_after)
|
|
85
|
+
else:
|
|
86
|
+
time.sleep(1)
|
|
87
|
+
long_running = True
|
|
88
|
+
|
|
89
|
+
# Handle successful responses
|
|
90
|
+
elif response.status_code in {200, 201}:
|
|
91
|
+
exit_loop = True
|
|
92
|
+
|
|
93
|
+
# Handle API throttling
|
|
94
|
+
elif response.status_code == 429:
|
|
95
|
+
retry_after = float(response.headers.get("Retry-After", 5)) + 5
|
|
96
|
+
logger.info(f"API Overloaded: Retrying in {retry_after} seconds")
|
|
97
|
+
time.sleep(retry_after)
|
|
98
|
+
|
|
99
|
+
# Handle expired authentication token
|
|
100
|
+
elif (
|
|
101
|
+
response.status_code == 401 and response.headers.get("x-ms-public-api-error-code") == "TokenExpired"
|
|
102
|
+
):
|
|
103
|
+
logger.info("AAD token expired. Refreshing token.")
|
|
104
|
+
self._refresh_token()
|
|
105
|
+
|
|
106
|
+
# Handle unauthorized access
|
|
107
|
+
elif (
|
|
108
|
+
response.status_code == 401 and response.headers.get("x-ms-public-api-error-code") == "Unauthorized"
|
|
109
|
+
):
|
|
110
|
+
msg = f"The executing identity is not authorized to call {method} on '{url}'."
|
|
111
|
+
raise Exception(msg)
|
|
112
|
+
|
|
113
|
+
# Handle item name conflicts
|
|
114
|
+
elif (
|
|
115
|
+
response.status_code == 400
|
|
116
|
+
and response.headers.get("x-ms-public-api-error-code") == "ItemDisplayNameAlreadyInUse"
|
|
117
|
+
):
|
|
118
|
+
if iteration_count <= 6:
|
|
119
|
+
logger.info("Item name is reserved. Retrying in 60 seconds.")
|
|
120
|
+
time.sleep(60)
|
|
121
|
+
else:
|
|
122
|
+
msg = f"Item name still in use after 6 attempts. Description: {response.reason}"
|
|
123
|
+
raise Exception(msg)
|
|
124
|
+
|
|
125
|
+
# Handle scenario where library removed from environment before being removed from repo
|
|
126
|
+
elif response.status_code == 400 and "is not present in the environment." in response.json().get(
|
|
127
|
+
"message", "No message provided"
|
|
128
|
+
):
|
|
129
|
+
msg = f"Deployment attempted to remove a library that is not present in the environment. Description: {response.json().get('message')}"
|
|
130
|
+
raise Exception(msg)
|
|
131
|
+
|
|
132
|
+
# Handle no environment libraries on GET request
|
|
133
|
+
elif (
|
|
134
|
+
response.status_code == 404
|
|
135
|
+
and response.headers.get("x-ms-public-api-error-code") == "EnvironmentLibrariesNotFound"
|
|
136
|
+
):
|
|
137
|
+
logger.info("Live environment doesnt have any libraries, continuing")
|
|
138
|
+
exit_loop = True
|
|
139
|
+
|
|
140
|
+
# Handle unsupported principal type
|
|
141
|
+
elif (
|
|
142
|
+
response.status_code == 400
|
|
143
|
+
and response.headers.get("x-ms-public-api-error-code") == "PrincipalTypeNotSupported"
|
|
144
|
+
):
|
|
145
|
+
msg = f"The executing principal type is not supported to call {method} on '{url}'"
|
|
146
|
+
raise Exception(msg)
|
|
147
|
+
|
|
148
|
+
# Handle unsupported item types
|
|
149
|
+
elif response.status_code == 403 and response.reason == "FeatureNotAvailable":
|
|
150
|
+
msg = f"Item type not supported. Description: {response.reason}"
|
|
151
|
+
raise Exception(msg)
|
|
152
|
+
|
|
153
|
+
# Handle unexpected errors
|
|
154
|
+
else:
|
|
155
|
+
err_msg = (
|
|
156
|
+
f" Message: {response.json()['message']}"
|
|
157
|
+
if "application/json" in (response.headers.get("Content-Type") or "")
|
|
158
|
+
else ""
|
|
159
|
+
)
|
|
160
|
+
msg = f"Unhandled error occurred calling {method} on '{url}'.{err_msg}"
|
|
161
|
+
raise Exception(msg)
|
|
162
|
+
|
|
163
|
+
# Log if reached to end of loop iteration
|
|
164
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
165
|
+
logger.debug(invoke_log_message)
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.debug(invoke_log_message)
|
|
169
|
+
raise InvokeError(e, logger, invoke_log_message) from e
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"header": dict(response.headers),
|
|
173
|
+
"body": (response.json() if "application/json" in response.headers.get("Content-Type") else {}),
|
|
174
|
+
"status_code": response.status_code,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def _refresh_token(self):
|
|
178
|
+
"""Refreshes the AAD token if empty or expiration has passed"""
|
|
179
|
+
if (
|
|
180
|
+
self.aad_token is None
|
|
181
|
+
or self.aad_token_expiration is None
|
|
182
|
+
or self.aad_token_expiration < datetime.datetime.utcnow()
|
|
183
|
+
):
|
|
184
|
+
resource_url = "https://api.fabric.microsoft.com/.default"
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
self.aad_token = self.token_credential.get_token(resource_url).token
|
|
188
|
+
except ClientAuthenticationError as e:
|
|
189
|
+
msg = f"Failed to aquire AAD token. {e}"
|
|
190
|
+
raise TokenError(msg, logger) from e
|
|
191
|
+
except Exception as e:
|
|
192
|
+
msg = f"An unexpected error occurred when generating the AAD token. {e}"
|
|
193
|
+
raise TokenError(msg, logger) from e
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
decoded_token = _decode_jwt(self.aad_token)
|
|
197
|
+
expiration = decoded_token.get("exp")
|
|
198
|
+
upn = decoded_token.get("upn")
|
|
199
|
+
appid = decoded_token.get("appid")
|
|
200
|
+
oid = decoded_token.get("oid")
|
|
201
|
+
|
|
202
|
+
if expiration:
|
|
203
|
+
self.aad_token_expiration = datetime.datetime.fromtimestamp(expiration)
|
|
204
|
+
else:
|
|
205
|
+
msg = "Token does not contain expiration claim."
|
|
206
|
+
raise TokenError(msg, logger)
|
|
207
|
+
|
|
208
|
+
if upn:
|
|
209
|
+
logger.info(f"Executing as User '{upn}'")
|
|
210
|
+
self.upn_auth = True
|
|
211
|
+
else:
|
|
212
|
+
self.upn_auth = False
|
|
213
|
+
if appid:
|
|
214
|
+
logger.info(f"Executing as Application Id '{appid}'")
|
|
215
|
+
elif oid:
|
|
216
|
+
logger.info(f"Executing as Object Id '{oid}'")
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
msg = f"An unexpected error occurred while decoding the credential token. {e}"
|
|
220
|
+
raise TokenError(msg, logger) from e
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _decode_jwt(token):
|
|
224
|
+
"""Decodes a JWT token and returns the payload as a dictionary."""
|
|
225
|
+
try:
|
|
226
|
+
# Split the token into its parts
|
|
227
|
+
parts = token.split(".")
|
|
228
|
+
if len(parts) != 3:
|
|
229
|
+
msg = "The token has an invalid JWT format"
|
|
230
|
+
raise TokenError(msg, logger)
|
|
231
|
+
|
|
232
|
+
# Decode the payload (second part of the token)
|
|
233
|
+
payload = parts[1]
|
|
234
|
+
padding = "=" * (4 - len(payload) % 4)
|
|
235
|
+
payload += padding
|
|
236
|
+
decoded_bytes = base64.urlsafe_b64decode(payload.encode("utf-8"))
|
|
237
|
+
decoded_str = decoded_bytes.decode("utf-8")
|
|
238
|
+
return json.loads(decoded_str)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
msg = f"An unexpected error occurred while decoding the credential token. {e}"
|
|
241
|
+
raise TokenError(msg, logger) from e
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _format_invoke_log(response, method, url, body):
|
|
245
|
+
message = [
|
|
246
|
+
f"\nURL: {url}",
|
|
247
|
+
f"Method: {method}",
|
|
248
|
+
(f"Request Body:\n{json.dumps(body, indent=4)}" if body else "Request Body: None"),
|
|
249
|
+
]
|
|
250
|
+
if response is not None:
|
|
251
|
+
message.extend([
|
|
252
|
+
f"Response Status: {response.status_code}",
|
|
253
|
+
"Response Headers:",
|
|
254
|
+
json.dumps(dict(response.headers), indent=4),
|
|
255
|
+
"Response Body:",
|
|
256
|
+
(
|
|
257
|
+
json.dumps(response.json(), indent=4)
|
|
258
|
+
if response.headers.get("Content-Type") == "application/json"
|
|
259
|
+
else response.text
|
|
260
|
+
),
|
|
261
|
+
"",
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
return "\n".join(message)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import colorlog
|
|
10
|
+
|
|
11
|
+
from fabric_cicd._common import _exceptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_logger(level: int = logging.INFO) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Configure the logger.
|
|
17
|
+
|
|
18
|
+
:param level: The log level to set. Must be one of the standard logging levels.
|
|
19
|
+
"""
|
|
20
|
+
# Configure default logging
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=(
|
|
23
|
+
# For non-fabric_cicd packages: INFO if DEBUG, else ERROR
|
|
24
|
+
logging.INFO if level == logging.DEBUG else logging.ERROR
|
|
25
|
+
),
|
|
26
|
+
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
|
27
|
+
filename="fabric_cicd.error.log",
|
|
28
|
+
filemode="w",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Configure Console Handler
|
|
32
|
+
console_handler = logging.StreamHandler()
|
|
33
|
+
console_handler.setLevel(level)
|
|
34
|
+
console_handler.setFormatter(
|
|
35
|
+
colorlog.ColoredFormatter(
|
|
36
|
+
"%(log_color)s[%(levelname)s] %(asctime)s - %(message)s",
|
|
37
|
+
datefmt="%H:%M:%S",
|
|
38
|
+
log_colors={"DEBUG": "cyan", "INFO": "green", "WARNING": "yellow", "ERROR": "red", "CRITICAL": "bold_red"},
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Create a logger that writes to the console and log file
|
|
43
|
+
package_logger = logging.getLogger("fabric_cicd")
|
|
44
|
+
package_logger.setLevel(level)
|
|
45
|
+
package_logger.handlers = []
|
|
46
|
+
package_logger.addHandler(console_handler)
|
|
47
|
+
|
|
48
|
+
# Create a logger that only writes to the console
|
|
49
|
+
console_only_logger = logging.getLogger("console_only")
|
|
50
|
+
console_only_logger.setLevel(level)
|
|
51
|
+
console_only_logger.handlers = []
|
|
52
|
+
console_only_logger.addHandler(console_handler)
|
|
53
|
+
console_only_logger.propagate = False # Prevent logs from being propagated to other loggers
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def exception_handler(exception_type, exception, traceback):
|
|
57
|
+
"""
|
|
58
|
+
Handle exceptions that are instances of any class from the _common._exceptions module.
|
|
59
|
+
|
|
60
|
+
:param exception_type: The type of the exception.
|
|
61
|
+
:param exception: The exception instance.
|
|
62
|
+
:param traceback: The traceback object.
|
|
63
|
+
"""
|
|
64
|
+
# Get all exception classes from the _common._exceptions module
|
|
65
|
+
exception_classes = [cls for _, cls in inspect.getmembers(_exceptions, inspect.isclass)]
|
|
66
|
+
|
|
67
|
+
# Check if the exception is an instance of any class from _common._exceptions
|
|
68
|
+
if any(isinstance(exception, cls) for cls in exception_classes):
|
|
69
|
+
# Log the exception using the logger associated with the exception
|
|
70
|
+
original_logger = exception.logger
|
|
71
|
+
|
|
72
|
+
# Write only the exception message to the console
|
|
73
|
+
logging.getLogger("console_only").error(
|
|
74
|
+
f"{exception!s}\n\nSee {Path('fabric_cicd.error.log').resolve()} for full details."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Write exception and full stack trace to logs but not terminal
|
|
78
|
+
package_logger = logging.getLogger("fabric_cicd")
|
|
79
|
+
|
|
80
|
+
# Clear any existing handlers to prevent writing to console
|
|
81
|
+
additional_info = getattr(exception, "additional_info", None)
|
|
82
|
+
additional_info = "\n\nAdditional Info: \n" + additional_info if additional_info is not None else ""
|
|
83
|
+
|
|
84
|
+
package_logger.handlers = []
|
|
85
|
+
original_logger.exception(f"%s{additional_info}", exception, exc_info=(exception_type, exception, traceback))
|
|
86
|
+
else:
|
|
87
|
+
# If the exception is not from _common._exceptions, use the default exception handler
|
|
88
|
+
sys.__excepthook__(exception_type, exception, traceback)
|