Skip to content

Commit

Permalink
Merge pull request #1 from infralovers/rework-cloud-confluence
Browse files Browse the repository at this point in the history
Rework cloud confluence
  • Loading branch information
mabunixda authored Aug 3, 2020
2 parents 22aeb80 + 1152bb6 commit 26b58ce
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bin/
lib/
test/
__pycache__/
Dockerfile
pyvenv.cfg
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ lib64/
parts/
sdist/
var/
bin/
wheels/
pip-wheel-metadata/
share/python-wheels/
Expand Down Expand Up @@ -128,4 +129,5 @@ dmypy.json
# Pyre type checker
.pyre/

.vscode/
.vscode/
pyvenv.cfg
14 changes: 9 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
FROM python:3-alpine

WORKDIR /usr/src/app

RUN apk add --no-cache git
ENV CONFLUENCE_USERNAME=""
ENV CONFLUENCE_PASSWORD=""
ENV CONFLUENCE_API_URL=""
ENV CONFLUENCE_SPACE=""
ENV CONFLUENCE_ANCESTOR_ID=""

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
WORKDIR /usr/src/app

COPY . .

RUN apk add --no-cache git \
&& pip install --no-cache-dir -r requirements.txt

CMD [ "python", "./markdown-to-confluence.py" ]
56 changes: 35 additions & 21 deletions confluence.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

API_HEADERS = {
'User-Agent': 'markdown-to-confluence',
'X-Atlassian-Token': 'no-check'
}

MULTIPART_HEADERS = {
Expand All @@ -32,7 +33,7 @@ def __init__(self,
dry_run=False,
_client=None):
"""Creates a new Confluence API client.
Arguments:
api_url {str} -- The URL to the Confluence API root (e.g. https://wiki.example.com/api/rest/)
username {str} -- The Confluence service account username
Expand Down Expand Up @@ -64,7 +65,7 @@ def __init__(self,

def _require_kwargs(self, kwargs):
"""Ensures that certain kwargs have been provided
Arguments:
kwargs {dict} -- The dict of required kwargs
"""
Expand Down Expand Up @@ -115,16 +116,16 @@ def _request(self,

if not response.ok:
log.info('''{method} {url}: {status_code} {reason}
Params: {params}
Data: {data}
Files: {files}'''.format(method=method,
url=url,
status_code=response.status_code,
reason=response.reason,
params=params,
data=data,
files=files))
print(response.content)
Params: {params}
Data: {data}
Files: {files}'''.format(method=method,
url=url,
status_code=response.status_code,
reason=response.reason,
params=params,
data=data,
files=files))

return response.content

# Will probably want to be more robust here, but this should work for now
Expand All @@ -149,7 +150,7 @@ def exists(self, space=None, slug=None, ancestor_id=None):
Specifically, this leverages a Confluence Query Language (CQL) query
against the Confluence API. We assume that each slug is unique, at
least to the provided space/ancestor_id.
Arguments:
space {str} -- The Confluence space to use for filtering posts
slug {str} -- The page slug
Expand Down Expand Up @@ -179,7 +180,7 @@ def create_labels(self, page_id=None, slug=None, tags=[]):
We specifically require a slug to be provided, since this is how we
determine if a page exists. Any other tags are optional.
Keyword Arguments:
page_id {str} -- The ID of the existing page to which the label should apply
slug {str} -- The page slug to use as the label value
Expand Down Expand Up @@ -242,7 +243,7 @@ def _create_page_payload(self,

def get_attachments(self, post_id):
"""Gets the attachments for a particular Confluence post
Arguments:
post_id {str} -- The Confluence post ID
"""
Expand All @@ -251,7 +252,7 @@ def get_attachments(self, post_id):

def upload_attachment(self, post_id=None, attachment_path=None):
"""Uploads an attachment to a Confluence post
Keyword Arguments:
post_id {str} -- The Confluence post ID
attachment_path {str} -- The absolute path to the attachment
Expand All @@ -276,12 +277,21 @@ def get_author(self, username):
username {str} -- The Confluence username
"""
log.info('Looking up Confluence user key for {}'.format(username))
response = self.get(path='user', params={'username': username})
if not isinstance(response, dict) or not response.get('userKey'):
response = self.get(path='user', params={'accountId': username})
if not isinstance(response, dict) or not response.get('accountId'):
log.error('No Confluence user key for {}'.format(username))
return {}
return response

def get_current_user(self):
"""Returns the Confluence author profile for the connected username
"""
log.info('Looking up current Confluence user key')
response = self.get(path='user/current')
if not isinstance(response, dict) or not response.get('accountId'):
return {}
return response

def create(self,
content=None,
space=None,
Expand All @@ -295,7 +305,7 @@ def create(self,
If an ancestor_id is specified, then the page will be created as a
child of that ancestor page.
Keyword Arguments:
content {str} -- The HTML content to upload (required)
space {str} -- The Confluence space where the page should reside
Expand All @@ -318,9 +328,13 @@ def create(self,
ancestor_id=ancestor_id,
space=space,
type=type)
response = self.post(path='content/', data=page)

response = self.post(path='content/', data=page)
# print(response)
page_id = response['id']
# to resuem, we can update the labels on the page
self.create_labels(page_id=page_id, slug=slug, tags=tags)

page_url = urljoin(self.api_url, response['_links']['webui'])

log.info('Page "{title}" (id {page_id}) created successfully at {url}'.
Expand Down Expand Up @@ -353,7 +367,7 @@ def update(self,
This involves updating the attachments stored on Confluence, uploading
the page content, and finally updating the labels.
Keyword Arguments:
post_id {str} -- The ID of the Confluence post
content {str} -- The page represented in Confluence storage format
Expand Down
19 changes: 10 additions & 9 deletions convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ def convtoconf(markdown, front_matter={}):

author_keys = front_matter.get('author_keys', [])
renderer = ConfluenceRenderer(authors=author_keys)
content_html = mistune.markdown(markdown, renderer=renderer)
page_html = renderer.layout(content_html)
markdown_html = mistune.create_markdown(renderer=renderer)
# content_html = mistune.html(markdown, renderer=renderer)
page_html = markdown_html(markdown)

return page_html, renderer.attachments


class ConfluenceRenderer(mistune.Renderer):
class ConfluenceRenderer(mistune.HTMLRenderer):
def __init__(self, authors=[]):
self.attachments = []
if authors is None:
Expand All @@ -68,7 +69,7 @@ def layout(self, content):
| (30% width) | (800px width) |
| | |
------------------------------------------
Arguments:
content {str} -- The HTML of the content
"""
Expand All @@ -91,24 +92,24 @@ def layout(self, content):
main_content = column.format(width='800px', content=content)
return sidebar + main_content

def header(self, text, level, raw=None):
def heading(self, text, level):
"""Processes a Markdown header.
In our case, this just tells us that we need to render a TOC. We don't
actually do any special rendering for headers.
"""
self.has_toc = True
return super().header(text, level, raw)
return super().heading(text, level)

def render_authors(self):
"""Renders a header that details which author(s) published the post.
This is used since Confluence will show the post published as our
service account.
Arguments:
author_keys {str} -- The Confluence user keys for each post author
Returns:
str -- The HTML to prepend to the post specifying the authors
"""
Expand All @@ -121,7 +122,7 @@ def render_authors(self):
for user_key in self.authors)
return '<h1>Authors</h1><p>{}</p>'.format(author_content)

def block_code(self, code, lang):
def block_code(self, code, lang=None):
return textwrap.dedent('''\
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">{l}</ac:parameter>
Expand Down
35 changes: 23 additions & 12 deletions markdown-to-confluence.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def get_environ_headers(prefix):

def get_last_modified(repo):
"""Returns the paths to the last modified files in the provided Git repo
Arguments:
repo {git.Repo} -- The repository object
"""
Expand All @@ -53,7 +53,7 @@ def get_last_modified(repo):

def get_slug(filepath, prefix=''):
"""Returns the slug for a given filepath
Arguments:
filepath {str} -- The filepath for the post
prefix {str} -- Any prefixes to the slug
Expand Down Expand Up @@ -134,6 +134,12 @@ def parse_args():
help=
'Print requests that would be sent- don\'t actually make requests against Confluence (note: we return empty responses, so this might impact accuracy)'
)
parser.add_argument(
'--static-path',
dest='static_path',
default='static',
help='The prefix of your static files (default: {}))'.format(
'static'))
parser.add_argument(
'posts',
type=str,
Expand All @@ -153,7 +159,7 @@ def parse_args():

def deploy_file(post_path, args, confluence):
"""Creates or updates a file in Confluence
Arguments:
post_path {str} -- The absolute path of the post to deploy to Confluence
args {argparse.Arguments} -- The parsed command-line arguments
Expand Down Expand Up @@ -185,16 +191,20 @@ def deploy_file(post_path, args, confluence):
confluence_author = confluence.get_author(author)
if not confluence_author:
continue
front_matter['author_keys'].append(confluence_author['userKey'])
front_matter['author_keys'].append(confluence_author['accountId'])

if len(front_matter['author_keys']) == 0:
front_matter['author_keys'].append(confluence.get_current_user())

# Normalize the content into whatever format Confluence expects
html, attachments = convtoconf(markdown, front_matter=front_matter)

static_path = os.path.join(args.git, 'static')

static_path = os.path.join(args.git, args.static_path)
for i, attachment in enumerate(attachments):
attachments[i] = os.path.join(static_path, attachment.lstrip('/'))

slug_prefix = '_'.join(author.lower() for author in authors)
slug_prefix = '{}{}'.format('s','_'.join(author.lower() for author in authors))
post_slug = get_slug(post_path, prefix=slug_prefix)

ancestor_id = front_matter['wiki'].get('ancestor_id', args.ancestor_id)
Expand All @@ -207,6 +217,7 @@ def deploy_file(post_path, args, confluence):
page = confluence.exists(slug=post_slug,
ancestor_id=ancestor_id,
space=space)

if page:
confluence.update(page['id'],
content=html,
Expand All @@ -219,12 +230,12 @@ def deploy_file(post_path, args, confluence):
attachments=attachments)
else:
confluence.create(content=html,
title=front_matter['title'],
tags=tags,
slug=post_slug,
space=space,
ancestor_id=ancestor_id,
attachments=attachments)
title=front_matter['title'],
tags=tags,
slug=post_slug,
space=space,
ancestor_id=ancestor_id,
attachments=attachments)


def main():
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ chardet==3.0.4
gitdb2==2.0.6
GitPython==3.0.4
idna==2.8
mistune==0.8.4
mistune==2.0.0a4
PyYAML==5.1.2
requests==2.22.0
smmap2==2.0.5
Expand Down
4 changes: 2 additions & 2 deletions test/test_confluence.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def testGetAuthor(self):
expected = {
"type": "known",
"username": "foo",
"userKey": userKey,
"accountId": userKey,
"profilePicture": {
"path": "/download/attachments/123456/user-avatar",
"width": 48,
Expand All @@ -130,7 +130,7 @@ def testGetAuthor(self):
is_json=True)
self.api._session = client
got = self.api.get_author('foo')
self.assertEqual(got['userKey'], userKey)
self.assertEqual(got['accountId'], userKey)


class TestConfluenceHeaders(unittest.TestCase):
Expand Down
2 changes: 1 addition & 1 deletion test/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def testHeader(self):
have = 'test'
want = '<h1>{}</h1>'.format(have)
renderer = ConfluenceRenderer()
got = renderer.header(have, 1)
got = renderer.heading(have, 1)
got = got.strip()
self.assertEqual(got, want)
self.assertEqual(renderer.has_toc, True)
Expand Down

0 comments on commit 26b58ce

Please sign in to comment.