Skip to content

Commit

Permalink
Confluence storage and document plugin (#2940)
Browse files Browse the repository at this point in the history
* Confluence storage and document plugin initial commit

* Removed some debug code and fixed typo

* Fixed PR comments for confluence plugin

* Fix pr comments for confluence plugin

---------

Co-authored-by: Cino Jose <[email protected]>
  • Loading branch information
Cinojose and Cino Jose authored Feb 7, 2023
1 parent 48d15e0 commit ead6707
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 10 deletions.
1 change: 1 addition & 0 deletions requirements-base.in
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ h11
httpx
jinja2
jira==2.0.0
atlassian-python-api==3.32.0
joblib
numpy
oauth2client
Expand Down
19 changes: 18 additions & 1 deletion requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ anyio==3.6.2
# starlette
async-timeout==4.0.2
# via aiohttp
atlassian-python-api==3.32.0
# via -r requirements-base.in
attrs==22.1.0
# via
# aiohttp
Expand Down Expand Up @@ -91,6 +93,8 @@ decorator==5.1.1
# via validators
defusedxml==0.7.1
# via jira
deprecated==1.2.13
# via atlassian-python-api
dnspython==2.2.1
# via email-validator
ecdsa==0.18.0
Expand All @@ -99,6 +103,10 @@ email-validator==1.3.1
# via -r requirements-base.in
emails==0.6
# via -r requirements-base.in
exceptiongroup==1.1.0
# via
# hypothesis
# pytest
fastapi==0.89.1
# via -r requirements-base.in
frozenlist==1.3.3
Expand Down Expand Up @@ -207,6 +215,7 @@ oauth2client==4.1.3
# via -r requirements-base.in
oauthlib[signedtoken]==3.2.2
# via
# atlassian-python-api
# jira
# requests-oauthlib
packaging==21.3
Expand Down Expand Up @@ -295,6 +304,7 @@ pyyaml==6.0
requests==2.28.1
# via
# -r requirements-base.in
# atlassian-python-api
# curlify
# emails
# google-api-core
Expand All @@ -308,6 +318,7 @@ requests==2.28.1
# starlette-testclient
requests-oauthlib==1.3.1
# via
# atlassian-python-api
# google-auth-oauthlib
# jira
requests-toolbelt==0.10.1
Expand Down Expand Up @@ -335,6 +346,7 @@ sh==1.14.3
# via -r requirements-base.in
six==1.16.0
# via
# atlassian-python-api
# ecdsa
# google-auth
# google-auth-httplib2
Expand Down Expand Up @@ -402,7 +414,9 @@ text-unidecode==1.3
thinc==8.1.5
# via spacy
tomli==2.0.1
# via schemathesis
# via
# pytest
# schemathesis
tomli-w==1.0.0
# via schemathesis
tqdm==4.64.1
Expand All @@ -415,6 +429,7 @@ typing-extensions==4.4.0
# via
# pydantic
# schemathesis
# starlette
uritemplate==4.1.1
# via google-api-python-client
urllib3==1.26.13
Expand All @@ -435,6 +450,8 @@ wasabi==0.10.1
# thinc
werkzeug==2.2.2
# via schemathesis
wrapt==1.14.1
# via deprecated
yarl==1.8.1
# via
# aiohttp
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ def run(self):
"slack_contact = dispatch.plugins.dispatch_slack.plugin:SlackContactPlugin",
"slack_conversation = dispatch.plugins.dispatch_slack.plugin:SlackConversationPlugin",
"zoom_conference = dispatch.plugins.dispatch_zoom.plugin:ZoomConferencePlugin",
"dispatch_atlassian_confluence = dispatch.plugins.dispatch_atlassian_confluence.plugin:ConfluencePagePlugin",
"dispatch_atlassian_confluence_document = dispatch.plugins.dispatch_atlassian_confluence.docs.plugin:ConfluencePageDocPlugin",
],
},
)
19 changes: 10 additions & 9 deletions src/dispatch/incident/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,17 @@ def create_incident_documents(incident: Incident, db_session: SessionLocal):
incident.storage.resource_id, incident_sheet_name, file_type="sheet"
)

sheet.update(
{
"name": incident_sheet_name,
"description": incident_sheet_description,
"resource_type": DocumentResourceTypes.tracking,
"resource_id": sheet["id"],
}
)
if sheet:
sheet.update(
{
"name": incident_sheet_name,
"description": incident_sheet_description,
"resource_type": DocumentResourceTypes.tracking,
"resource_id": sheet["id"],
}
)

incident_documents.append(sheet)
incident_documents.append(sheet)

event_service.log_incident_event(
db_session=db_session,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._version import __version__ # noqa
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
28 changes: 28 additions & 0 deletions src/dispatch/plugins/dispatch_atlassian_confluence/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from pydantic import Field, SecretStr, HttpUrl

from enum import Enum
from dispatch.config import BaseConfigurationModel


class HostingType(str, Enum):
"""Type of Atlassian Confluence deployment."""

cloud = "cloud"
server = "server"


class ConfluenceConfigurationBase(BaseConfigurationModel):
"""Atlassian Confluence configuration description."""

api_url: HttpUrl = Field(
title="API URL", description="This URL is used for communication with API."
)
hosting_type: HostingType = Field(
"cloud", title="Hosting Type", description="Defines the type of deployment."
)
username: str = Field(
title="Username", description="Username to use to authenticate to Confluence API."
)
password: SecretStr = Field(
title="Password", description="Password to use to authenticate to Confluence API."
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._version import __version__ # noqa
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
52 changes: 52 additions & 0 deletions src/dispatch/plugins/dispatch_atlassian_confluence/docs/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from dispatch.plugins.dispatch_atlassian_confluence import docs as confluence_doc_plugin
from dispatch.plugins.bases import DocumentPlugin
from dispatch.plugins.dispatch_atlassian_confluence.config import ConfluenceConfigurationBase
from atlassian import Confluence
from typing import List


def replace_content(client: Confluence, document_id: str, replacements: List[str]) -> dict():
# read content based on document_id
current_content = client.get_page_by_id(
document_id, expand="body.storage", status=None, version=None
)
current_content_body = current_content["body"]["storage"]["value"]
for k, v in replacements.items():
if v:
current_content_body = current_content_body.replace(k, v)

updated_content = client.update_page(
page_id=document_id,
title=current_content["title"],
body=current_content_body,
representation="storage",
type="page",
parent_id=None,
minor_edit=False,
full_width=False,
)
return updated_content


class ConfluencePageDocPlugin(DocumentPlugin):
title = "Confluence pages plugin - Document Management"
slug = "confluence-docs-document"
description = "Use Confluence to update the contents."
version = confluence_doc_plugin.__version__

author = "Cino Jose"
author_url = "https://github.com/Netflix/dispatch"

def __init__(self):
self.configuration_schema = ConfluenceConfigurationBase

def update(self, document_id: str, **kwargs):
"""Replaces text in document."""
kwargs = {"{{" + k + "}}": v for k, v in kwargs.items()}
confluence_client = Confluence(
url=self.configuration.api_url,
username=self.configuration.username,
password=self.configuration.password.get_secret_value(),
cloud=self.configuration.hosting_type,
)
return replace_content(confluence_client, document_id, kwargs)
139 changes: 139 additions & 0 deletions src/dispatch/plugins/dispatch_atlassian_confluence/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from dispatch.plugins import dispatch_atlassian_confluence as confluence_plugin
from dispatch.plugins.bases import StoragePlugin
from dispatch.plugins.dispatch_atlassian_confluence.config import ConfluenceConfigurationBase

from pydantic import Field

from atlassian import Confluence
import requests
from requests.auth import HTTPBasicAuth
import logging
from typing import List

logger = logging.getLogger(__name__)


# TODO : Use the common config from the root directory.
class ConfluenceConfiguration(ConfluenceConfigurationBase):
"""Confluence configuration description."""

template_id: str = Field(
title="Incident template ID", description="This is the page id of the template."
)
root_id: str = Field(
title="Default Space ID", description="Defines the default Confluence Space to use."
)
parent_id: str = Field(
title="Parent ID for the pages",
description="Define the page id of a parent page where all the incident documents can be kept.",
)
open_on_close: bool = Field(
title="Open On Close",
default=False,
description="Controls the visibility of resources on incident close. If enabled Dispatch will make all resources visible to the entire workspace.",
)
read_only: bool = Field(
title="Readonly",
default=False,
description="The incident document will be marked as readonly on incident close. Participants will still be able to interact with the document but any other viewers will not.",
)


class ConfluencePagePlugin(StoragePlugin):
title = "Confluence Plugin - Store your incident details"
slug = "confluence"
description = "Confluence plugin to create incident documents"
version = confluence_plugin.__version__

author = "Cino Jose"
author_url = "https://github.com/Netflix/dispatch"

def __init__(self):
self.configuration_schema = ConfluenceConfiguration

def create_file(
self, drive_id: str, name: str, participants: List[str] = [], file_type: str = "folder"
):
"""Creates a new Home page for the incident documents.."""
try:
if file_type not in ["document", "folder"]:
return None
confluence_client = Confluence(
url=self.configuration.api_url,
username=self.configuration.username,
password=self.configuration.password.get_secret_value(),
cloud=self.configuration.hosting_type,
)
child_display_body = """<h3>Incident Documents:</h3><ac:structured-macro ac:name="children"
ac:schema-version="2" data-layout="default" ac:local-id="ec0e8d6d-3215-4328-b1f8-e96b03ccefb9"
ac:macro-id="10235d28b48543519d4e2b06ca230142"><ac:parameter ac:name="sort">modified</ac:parameter>
<ac:parameter ac:name="reverse">true</ac:parameter></ac:structured-macro>"""
page_details = confluence_client.create_page(
drive_id,
name,
body=child_display_body,
parent_id=self.configuration.parent_id,
type="page",
representation="storage",
editor="v2",
full_width=False,
)
return {
"weblink": f"{self.configuration.api_url}wiki/spaces/{drive_id}/pages/{page_details['id']}/{name}",
"id": page_details["id"],
"name": name,
"description": "",
}
except Exception as e:
logger.error(f"Exception happened while creating page: {e}")

def copy_file(self, folder_id: str, file_id: str, name: str):
# TODO : This is the function that is responsible for making the incident documents.
try:
confluence_client = Confluence(
url=self.configuration.api_url,
username=self.configuration.username,
password=self.configuration.password.get_secret_value(),
cloud=self.configuration.hosting_type,
)
logger.info(f"Copy_file function with args {folder_id}, {file_id}, {name}")
template_content = confluence_client.get_page_by_id(
self.configuration.template_id, expand="body.storage", status=None, version=None
)
page_details = confluence_client.create_page(
space=self.configuration.root_id,
parent_id=folder_id,
title=name,
type="page",
body=template_content["body"],
representation="storage",
editor="v2",
full_width=False,
)
if self.configuration.parent_id:
"""TODO: Find and fix why the page is not created under the parent_id, folder_id"""
self.move_file_confluence(page_id_to_move=page_details["id"], parent_id=folder_id)
return {
"weblink": f"{self.configuration.api_url}wiki/spaces/{folder_id}/pages/{page_details['id']}/{name}",
"id": page_details["id"],
"name": name,
}
except Exception as e:
logger.error(f"Exception happened while creating page: {e}")

def move_file(self, new_folder_id: str, file_id: str, **kwargs):
"""Moves a file from one place to another. Not used in the plugin,
keeping the body as the interface is needed to avoid exceptions."""
return {}

def move_file_confluence(self, page_id_to_move: str, parent_id: str):
try:
url = f"{self.configuration.api_url}wiki/rest/api/content/{page_id_to_move}/move/append/{parent_id}"
auth = HTTPBasicAuth(
self.configuration.username, self.configuration.password.get_secret_value()
)
headers = {"Accept": "application/json"}
response = requests.request("PUT", url, headers=headers, auth=auth)
return response
except Exception as e:
logger.error(f"Exception happened while moving page: {e}")

0 comments on commit ead6707

Please sign in to comment.