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

Confluence storage and document plugin #2940

Merged
merged 5 commits into from
Feb 7, 2023
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
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:
Cinojose marked this conversation as resolved.
Show resolved Hide resolved
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}")