Skip to content

Commit 029049b

Browse files
authored
Merge pull request #75 from QualiSystemsLab/natti-offline-support
Natti Gitlab Offline Support
2 parents 68d44cb + 2c54011 commit 029049b

21 files changed

+509
-30
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ cloudshell_config.yml
6464

6565
# env files
6666
*.env
67-
!*.template.env
67+
!*.template.env
68+
*.zip

package/__init__.py

Whitespace-only changes.

package/cloudshell/iac/terraform/constants.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,14 @@ class ATTRIBUTE_NAMES:
7070
CT_INPUTS = "Custom Tags"
7171
APPLY_TAGS = "Apply Tags"
7272
REMOTE_STATE_PROVIDER = "Remote State Provider"
73-
GITHUB_TERRAFORM_MODULE_URL = "Github Terraform Module URL"
73+
GIT_TERRAFORM_MODULE_URL = "Git Terraform Module URL"
7474
TERRAFORM_VERSION = "Terraform Version"
75-
GITHUB_TOKEN = "Github Token"
76-
GITHUB_URL = "Github Terraform Module URL"
75+
GIT_TOKEN = "Git Token"
7776
BRANCH = "Branch"
7877
CLOUD_PROVIDER = "Cloud Provider"
7978
UUID = "UUID"
79+
GIT_PROVIDER = "Git Provider"
80+
LOCAL_TERRAFORM = "Local Terraform"
8081

8182

8283
GET_BACKEND_DATA_COMMAND = "get_backend_data"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from abc import ABC, abstractmethod
2+
from logging import Logger
3+
4+
5+
class GitScriptDownloaderBase(ABC):
6+
7+
def __init__(self, logger: Logger):
8+
self.logger = logger
9+
10+
@abstractmethod
11+
def download_repo(self, url: str, token: str, branch: str = "") -> str:
12+
"""
13+
method should do the following:
14+
1.make request
15+
2. download repo
16+
3. prepare working dir, add repo contents to working dir
17+
4. return full path of working dir as string
18+
"""
19+
pass

package/cloudshell/iac/terraform/downloaders/downloader.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
1+
import logging
2+
from typing import Type
3+
14
from cloudshell.iac.terraform.constants import ATTRIBUTE_NAMES
2-
from cloudshell.iac.terraform.downloaders.github_downloader import GitHubScriptDownloader
35
from cloudshell.iac.terraform.downloaders.tf_exec_downloader import TfExecDownloader
46
from cloudshell.iac.terraform.models.shell_helper import ShellHelperObject
7+
from cloudshell.iac.terraform.downloaders.base_git_downloader import GitScriptDownloaderBase
8+
from cloudshell.iac.terraform.downloaders.github_downloader import GitHubScriptDownloader
9+
from cloudshell.iac.terraform.downloaders.gitlab_downloader import GitLabScriptDownloader
510

611

712
class Downloader(object):
813
def __init__(self, shell_helper: ShellHelperObject):
914
self._shell_helper = shell_helper
1015

1116
def download_terraform_module(self) -> str:
12-
url = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.GITHUB_TERRAFORM_MODULE_URL)
13-
token_enc = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.GITHUB_TOKEN)
17+
url = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.GIT_TERRAFORM_MODULE_URL)
18+
if not url:
19+
raise ValueError(f"Must populate attribute '{ATTRIBUTE_NAMES.GIT_TERRAFORM_MODULE_URL}'")
20+
21+
token_enc = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.GIT_TOKEN)
1422
token = self._shell_helper.api.DecryptPassword(token_enc).Value
1523
branch = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.BRANCH)
1624

17-
self._shell_helper.sandbox_messages.write_message("downloading Terraform module from repository...")
18-
self._shell_helper.logger.info("Downloading Terraform Repo from Github")
25+
# get downloader mapped to git provider
26+
provider = self._shell_helper.attr_handler.get_attribute(ATTRIBUTE_NAMES.GIT_PROVIDER)
27+
downloader = self._downloader_factory(provider, logger=self._shell_helper.logger)
1928

20-
downloader = GitHubScriptDownloader(self._shell_helper.logger)
29+
# download repo and return working dir
30+
self._shell_helper.sandbox_messages.write_message("downloading Terraform module from repository...")
31+
self._shell_helper.logger.info(f"Downloading Terraform Repo from '{provider}'")
32+
self._shell_helper.logger.info(f"Download URL: '{url}'")
2133
return downloader.download_repo(url, token, branch)
2234

2335
def download_terraform_executable(self, tf_workingdir: str) -> None:
@@ -32,3 +44,17 @@ def download_terraform_executable(self, tf_workingdir: str) -> None:
3244
except Exception as e:
3345
self._shell_helper.logger.error(f"Failed downloading Terraform Repo from Github {str(e)}")
3446
raise
47+
48+
def _get_downloader_class(self, git_provider: str) -> Type[GitScriptDownloaderBase]:
49+
""" extend this dictionary with additional git provider downloaders """
50+
git_downloader_map = {
51+
"github": GitHubScriptDownloader,
52+
"gitlab": GitLabScriptDownloader
53+
}
54+
if git_provider.lower() not in git_downloader_map:
55+
raise NotImplementedError(f"Git Provider '{git_provider}' not supported")
56+
return git_downloader_map[git_provider.lower()]
57+
58+
def _downloader_factory(self, git_provider: str, logger: logging.Logger) -> GitScriptDownloaderBase:
59+
downloader_class = self._get_downloader_class(git_provider)
60+
return downloader_class(logger)

package/cloudshell/iac/terraform/downloaders/github_downloader.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import json
33
import os
44
import re
5-
from logging import Logger
65
from zipfile import ZipFile
76
import tempfile
87
import requests
@@ -11,17 +10,15 @@
1110
from retry import retry
1211

1312
from cloudshell.iac.terraform.constants import GITHUB_REPO_PATTERN
13+
from cloudshell.iac.terraform.downloaders.base_git_downloader import GitScriptDownloaderBase
1414

1515
GitHubFileData = collections.namedtuple(
1616
'GitHubFileData', 'account_id repo_id branch_id path api_zip_dl_url api_tf_dl_url'
1717
)
1818
REPO_FILE_NAME = "repo.zip"
1919

2020

21-
class GitHubScriptDownloader(object):
22-
23-
def __init__(self, logger: Logger):
24-
self.logger = logger
21+
class GitHubScriptDownloader(GitScriptDownloaderBase):
2522

2623
@retry((HTTPError, URLError), delay=1, backoff=2, tries=5)
2724
def download_repo(self, url: str, token: str, branch: str = "") -> str:
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import re
2+
from dataclasses import dataclass
3+
from typing import List
4+
from urllib.error import HTTPError, URLError
5+
from retry import retry
6+
from cloudshell.iac.terraform.downloaders.base_git_downloader import GitScriptDownloaderBase
7+
from cloudshell.iac.terraform.services.gitlab_api_handler import GitlabApiHandler
8+
from urllib.parse import unquote
9+
10+
11+
@dataclass
12+
class CommonGitLabUrlData:
13+
protocol: str
14+
domain: str
15+
path: str
16+
full_url: str
17+
sha: str
18+
19+
20+
@dataclass
21+
class GitLabBrowserUrlData(CommonGitLabUrlData):
22+
gitlab_user: str
23+
project_name: str
24+
25+
26+
@dataclass
27+
class GitLabApiUrlData(CommonGitLabUrlData):
28+
api_version: str
29+
project_id: int
30+
api_endpoint: str
31+
32+
33+
def extract_data_from_browser_url(url) -> GitLabBrowserUrlData:
34+
"""
35+
Take api style url and extract data
36+
Sample Raw Browser url: "http://192.168.85.26/quali_natti/terraformstuff/-/tree/test-branch/rds/project1"
37+
'sha' can be branch or commit id
38+
"""
39+
pattern = (r'^(?P<protocol>https?)://(?P<domain>[^/]+)/(?P<user>[^/]+)/(?P<project>[^/]+)/-/tree/'
40+
r'(?P<sha>[^/]+)/(?P<path>.*)?$')
41+
42+
match = re.match(pattern, url)
43+
if not match:
44+
raise ValueError(f"No GitLab URL Data found in RAW url '{url}'")
45+
46+
groups = match.groupdict()
47+
return GitLabBrowserUrlData(protocol=groups['protocol'],
48+
domain=groups['domain'],
49+
gitlab_user=groups['user'],
50+
project_name=groups['project'],
51+
sha=groups['sha'],
52+
path=groups['path'],
53+
full_url=url)
54+
55+
56+
def get_query_param_val(param_key: str, params_list: List[List[str]]) -> str:
57+
"""
58+
look for target param in 2D list of key pair values
59+
[[k1,v1],[k2,v2]]
60+
if not found return empty string
61+
"""
62+
target_param_search = [x for x in params_list if x[0] == param_key]
63+
param_val = target_param_search[0][1] if target_param_search else ""
64+
return param_val
65+
66+
67+
def extract_data_from_api_url(url) -> GitLabApiUrlData:
68+
"""
69+
Take user style url and extract data
70+
supports url-encoded style paths as well
71+
Sample Api url: "http://192.168.85.26/api/v4/projects/2/repository/archive.zip?path=rds"
72+
"""
73+
pattern = (r'^(?P<protocol>https?)://(?P<domain>[^/]+)(?P<api_version>/api/v\d+)?'
74+
r'(?P<api_endpoint>/projects/(?P<project_id>\d+)/repository/archive\.zip)'
75+
r'(?P<params>\?([^&]+=[^&]+&)*[^&]+=[^&]+$)')
76+
77+
match = re.match(pattern, url)
78+
if not match:
79+
raise ValueError(f"No GitLab url data found in API url '{url}'")
80+
81+
groups = match.groupdict()
82+
query_params = groups['params']
83+
84+
# remove the leading '?' of the query param string
85+
query_params = query_params.split("?")[-1]
86+
87+
# split into 2D list [[k1,v1],[k2,v2]]
88+
params_list = [x.split("=") for x in query_params.split("&")]
89+
90+
# search for target params
91+
path = get_query_param_val("path", params_list)
92+
sha = get_query_param_val("sha", params_list)
93+
ref = get_query_param_val("ref", params_list)
94+
95+
# take sha param if passed, otherwise use the ref
96+
sha = sha if sha else ref
97+
98+
# url encoded path not necessary
99+
path = unquote(path)
100+
sha = unquote(sha)
101+
return GitLabApiUrlData(protocol=groups['protocol'],
102+
domain=groups['domain'],
103+
api_version=groups['api_version'],
104+
project_id=groups['project_id'],
105+
api_endpoint=groups['api_endpoint'],
106+
path=path,
107+
sha=sha,
108+
full_url=url)
109+
110+
111+
def is_gitlab_api_url(url: str) -> bool:
112+
"""
113+
check if is api endpoint
114+
Sample Api url: "http://192.168.85.26/api/v4/projects/2/repository/archive.zip?path=rds"
115+
"""
116+
pattern = r'^(?P<protocol>https?)://(?P<domain>[^/]+)(?P<api_version>/api/v\d+)?(?P<api_endpoint>/[^\s]+)*/?$'
117+
match = re.match(pattern, url)
118+
if not match:
119+
return False
120+
121+
groups = match.groupdict()
122+
api_version = groups['api_version'] # "/api/v4"
123+
124+
if not api_version:
125+
return False
126+
127+
return True
128+
129+
130+
class GitLabScriptDownloader(GitScriptDownloaderBase):
131+
132+
@retry((HTTPError, URLError), delay=1, backoff=2, tries=5)
133+
def download_repo(self, url: str, token: str, branch: str = "") -> str:
134+
135+
# extract data from browser "raw style url" or "gitlab api" style
136+
is_api_url = is_gitlab_api_url(url)
137+
if is_api_url:
138+
url_data = extract_data_from_api_url(url)
139+
else:
140+
url_data = extract_data_from_browser_url(url)
141+
142+
# allow service branch attr to override the url defined sha
143+
sha = branch if branch else url_data.sha
144+
is_https = True if url_data.protocol == "https" else False
145+
api_handler = GitlabApiHandler(host=url_data.domain, token=token, is_https=is_https)
146+
147+
# if using raw style url, do lookup for project id from project name
148+
project_id = url_data.project_id if is_api_url else api_handler.get_project_id_from_name(url_data.project_name)
149+
working_dir = api_handler.download_archive_to_temp_dir(project_id=project_id, path=url_data.path, sha=sha)
150+
self.logger.info(f"Temp Working Dir: {working_dir}")
151+
return working_dir

0 commit comments

Comments
 (0)