Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(azuredevops): added azure devops output #7

Merged
merged 4 commits into from
Jan 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,40 @@
## [Unreleased]


<a name="0.4.0"></a>
## [0.4.0] - 2021-01-31
### Feat
- **azuredevops:** send request
- **azuredevops:** added azure devops output


<a name="0.3.4"></a>
## [0.3.4] - 2021-01-30
### Chore
- remove requirements.txt


<a name="0.3.3"></a>
## [0.3.3] - 2021-01-30
### Chore
- update build information


<a name="0.3.2"></a>
## [0.3.2] - 2021-01-30
### Docs
- update docs with usage


<a name="0.3.1"></a>
## [0.3.1] - 2021-01-30
### Docs
- update setup classifier
- update module documentation

### Pull Requests
- Merge pull request [#6](https://github.com/bcochofel/terraplanfeed/issues/6) from bcochofel/update_stdout_doc


<a name="0.3.0"></a>
## [0.3.0] - 2021-01-30
Expand Down Expand Up @@ -91,7 +119,11 @@
- Merge pull request [#1](https://github.com/bcochofel/terraplanfeed/issues/1) from bcochofel/package_skel


[Unreleased]: https://github.com/bcochofel/terraplanfeed/compare/0.3.1...HEAD
[Unreleased]: https://github.com/bcochofel/terraplanfeed/compare/0.4.0...HEAD
[0.4.0]: https://github.com/bcochofel/terraplanfeed/compare/0.3.4...0.4.0
[0.3.4]: https://github.com/bcochofel/terraplanfeed/compare/0.3.3...0.3.4
[0.3.3]: https://github.com/bcochofel/terraplanfeed/compare/0.3.2...0.3.3
[0.3.2]: https://github.com/bcochofel/terraplanfeed/compare/0.3.1...0.3.2
[0.3.1]: https://github.com/bcochofel/terraplanfeed/compare/0.3.0...0.3.1
[0.3.0]: https://github.com/bcochofel/terraplanfeed/compare/0.2.0...0.3.0
[0.2.0]: https://github.com/bcochofel/terraplanfeed/compare/0.1.3...0.2.0
Expand Down
2 changes: 1 addition & 1 deletion terraplanfeed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
Modules:
terraform
"""
__version__ = "0.3.1"
__version__ = "0.4.0"
233 changes: 233 additions & 0 deletions terraplanfeed/azuredevops.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,243 @@
"""
Azure DevOps module for Pull Request comment with changes.
This module expects the following ENV variables:
- SYSTEM_TEAMFOUNDATIONSERVERURI
- SYSTEM_TEAMPROJECT
- BUILD_REPOSITORY_ID
- SYSTEM_PULLREQUEST_PULLREQUESTID
- SYSTEM_ACCESSTOKEN
if any of these variables is empty it will use stdout driver.
Some useful links:
https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#system-variables-devops-services
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20threads/create?view=azure-devops-rest-6.0
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20threads/create?view=azure-devops-rest-6.0#commentthreadstatus
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20threads/create?view=azure-devops-rest-6.0#comment
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20threads/create?view=azure-devops-rest-6.0#commenttype
Functions:
getAction: gets the action from actions list
parseChanges: gets list of changes and creates multiline summary
validateEnvVars: validates required env variables
formatUrl: formats URL using environment variables
formatDataToSend: creates data to be sent in the request
sendRequest: send the request with the content to devops
main: entrypoint
"""
import logging
import os
import base64
import requests
import terraplanfeed.stdout as stdout

logger = logging.getLogger(__name__)

URLCOMPONENTS = {
"URL": os.getenv("SYSTEM_TEAMFOUNDATIONSERVERURI"),
"PROJ": os.getenv("SYSTEM_TEAMPROJECT"),
"REPOSITORYID": os.getenv("BUILD_REPOSITORY_ID"),
"PULLREQUESTID": os.getenv("SYSTEM_PULLREQUEST_PULLREQUESTID"),
"APIVERSION": "6.0",
}

COMMENTTYPE = {
"CODECHANGE": "codeChange",
"SYSTEM": "system",
"TEXT": "text",
"UNKNOWN": "unknown",
}

COMMENTTHREADSTATUS = {
"ACTIVE": "active",
"BYDESIGN": "byDesign",
"CLOSED": "closed",
"FIXED": "fixed",
"PENDING": "pending",
"UNKNOWN": "unknown",
"WONTFIX": "wontFix",
}

ACTION_SYMBOLS = {
"no-op": ":thumbsup:",
"create": ":sparkles:",
"read": ":open_book:",
"update": ":pencil2:",
"replace": ":warning:",
"delete": ":stop_sign:",
}

HEADER = """
**Terraform Plan changes summary:**
===================================
"""

FOOTER = """
"""


def getAction(actions):
"""
Get action
Args:
actions(list): list of actions
Returns:
action symbol
"""

logger.debug("get action")
if "create" in actions and len(actions) > 1:
return ACTION_SYMBOLS["replace"]
else:
return ACTION_SYMBOLS[actions[0]]


def parseChanges(changes):
"""
Parse changes.
Args:
changes(list): list of resources dict
Returns:
Multiline string with summary of changes
"""

content = ""
logger.debug("parsing changes...")
for c in changes:
action = getAction(c["actions"])
message = "({action}): {name} ({address})".format(
action=action, name=c["name"], address=c["address"]
)
content += message + "\n"
return content


def validateEnvVars():
"""
Validates the contents of the required env variables
Returns:
boolean true or false if they are present
"""

logger.debug("check required env variables")
if URLCOMPONENTS["URL"] is None:
return False
if URLCOMPONENTS["PROJ"] is None:
return False
if URLCOMPONENTS["REPOSITORYID"] is None:
return False
if URLCOMPONENTS["PULLREQUESTID"] is None:
return False
if os.getenv("SYSTEM_ACCESSTOKEN") is None:
return False

return True


def formatUrl():
"""
Formats URL using environment variables.
Returns: url
"""

url = ""
logger.debug("format url")
url = "{url}{proj}/_apis/git/repositories/{repoid}/pullRequests/{prid}/threads?api-version={apiversion}".format(
url=URLCOMPONENTS["URL"],
proj=URLCOMPONENTS["PROJ"],
repoid=URLCOMPONENTS["REPOSITORYID"],
prid=URLCOMPONENTS["PULLREQUESTID"],
apiversion=URLCOMPONENTS["APIVERSION"],
)

return url


def formatDataToSend(content, commentType, statusCode):
"""
Creates JSON content to send to Azure DevOps.
Args:
content(str): multiline string
commentType(str): comment type
statusCode(str): status code
Returns:
dict with the content
"""

formattedContent = HEADER + content + FOOTER
data = {
"comments": [
{
"parentCommentId": 0,
"content": formattedContent,
"commentType": commentType,
}
],
"status": statusCode,
}

return data


def sendRequest(url, data):
"""
Sends request to Azure DevOps API.
Args:
url: URL to send request
data: json content to send to API
Returns:
True or False based on the send request
"""

logger.debug("send request")
logger.debug("url: {}".format(url))
logger.debug("data: {}".format(data))

token = os.getenv("SYSTEM_ACCESSTOKEN")
access_token = ":{}".format(token)

encodedBytes = base64.b64encode(access_token.encode("utf-8"))
encodedStr = str(encodedBytes, "utf-8")

session = requests.Session()
session.headers["Authorization"] = "Basic {}".format(encodedStr)
response = session.post(url, json=data)

return response.status_code


def main(changes):
"""
Handles changes and formats content to send to Azure DevOps API.
Args:
changes(list): list of resources dict
"""

logger.debug("azure devops entrypoint")
if not changes:
content = "No changes " + ACTION_SYMBOLS["no-op"]
commentType = COMMENTTYPE["TEXT"]
statusCode = COMMENTTHREADSTATUS["CLOSED"]
else:
content = parseChanges(changes)
commentType = COMMENTTYPE["CODECHANGE"]
statusCode = COMMENTTHREADSTATUS["ACTIVE"]
data = formatDataToSend(content, commentType, statusCode)
if validateEnvVars():
url = formatUrl()
retcode = sendRequest(url, data)
logger.debug("status code: {}".format(retcode))
else:
stdout.write(content)
9 changes: 6 additions & 3 deletions terraplanfeed/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
import json

from terraplanfeed.terraform import parsePlan
from terraplanfeed.stdout import writeToStdout
import terraplanfeed.stdout as stdout
import terraplanfeed.azuredevops as azuredevops

logger = logging.getLogger(__name__)

Expand All @@ -37,5 +38,7 @@ def terraplanfeed(filename, output):

resources = parsePlan(tf)

if output.lower() == "stdout":
writeToStdout(resources)
if output.lower() == "azuredevops":
azuredevops.main(resources)
else:
stdout.main(resources)
31 changes: 23 additions & 8 deletions terraplanfeed/stdout.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
Functions:
getAction: gets the action from actions list
parseChanges: gets list of changes and creates multiline summary
writeToStdout: writes the summary content to stdout
write: writes the summary content to stdout
main: entrypoint
"""
import logging

Expand All @@ -22,9 +23,8 @@
}

HEADER = """
Summary of changes:
===================
**Terraform Plan changes summary:**
===================================
"""

FOOTER = """
Expand Down Expand Up @@ -71,16 +71,31 @@ def parseChanges(changes):
return content


def writeToStdout(changes):
def write(content):
"""
Writes summary of changes to stdout.
Args:
changes(list): list of resources dict
content(str): multiline string
"""

logger.debug("write to stdout...")
content = parseChanges(changes)
logger.debug("write to stdout")
print(HEADER)
print(content)
print(FOOTER)


def main(changes):
"""
Entrypoint for stdout output driver.
Args:
changes(list): list of resources dict
"""

logger.debug("stdout entrypoint")
if not changes:
content = "No changes"
else:
content = parseChanges(changes)
write(content)