Add Artifact Manager service.
[ccsdk/cds.git] / ms / artifact-manager / manager / utils.py
1 """Copyright 2019 Deutsche Telekom.
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7     http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 """
15
16 import os
17 import shutil
18 from abc import ABC, abstractmethod
19 from io import BytesIO
20 from pathlib import Path
21 from zipfile import ZipFile, is_zipfile
22
23 from manager.configuration import config
24 from manager.errors import ArtifactNotFoundError, ArtifactOverwriteError, InvalidRequestError
25
26
27 class Repository(ABC):
28     """Abstract repository class.
29
30     Defines repository methods.
31     """
32
33     @abstractmethod
34     def upload_blueprint(self, file: bytes, name: str, version: str) -> None:
35         """Store blueprint file in the repository.
36
37         :param file: File to save
38         :param name: Blueprint name
39         :param version: Blueprint version
40         """
41
42     @abstractmethod
43     def download_blueprint(self, name: str, version: str) -> bytes:
44         """Download blueprint file from repository.
45
46         :param name: Blueprint name
47         :param version: Blueprint version
48         :return: Zipped Blueprint file bytes
49         """
50
51     @abstractmethod
52     def remove_blueprint(self, name: str, version: str) -> None:
53         """Remove blueprint file from repository.
54
55         :param name: Blueprint name
56         :param version: Blueprint version
57         """
58
59
60 class FileRepository(Repository):
61     """Store blueprints on local directory."""
62
63     base_path = None
64
65     def __init__(self, base_path: Path) -> None:
66         """Initialize the repository while passing the needed path.
67
68         :param base_path: Local OS path on which blueprint files reside.
69         """
70         self.base_path = base_path
71
72     def __remove_directory_tree(self, full_path: str) -> None:
73         """Remove specified path.
74
75         :param full_path: Full path to a directory.
76         :raises: FileNotFoundError
77         """
78         try:
79             shutil.rmtree(full_path, ignore_errors=False)
80         except OSError:
81             raise ArtifactNotFoundError
82
83     def __create_directory_tree(self, full_path: str, mode: int = 0o744, retry_on_error: bool = True) -> None:
84         """Create directory or overwrite existing one.
85
86         This method will handle a directory tree creation. If there is a collision
87         in directory structure - old directory tree will be removed
88         and creation will be attempted one more time. If the creation fails for the second time
89         the exception will be raised.
90
91         :param full_path: Full directory tree path (eg. one/two/tree) as string.
92         :param mode: Permission mask for the directories.
93         :param retry_on_error: Flag that indicates if there should be a attempt to retry the operation.
94         """
95         try:
96             os.makedirs(full_path, mode=mode)
97         except FileExistsError:
98             # In this case we know that cba of same name and version need to be overwritten
99             if retry_on_error:
100                 self.__remove_directory_tree(full_path)
101                 self.__create_directory_tree(full_path, mode=mode, retry_on_error=False)
102             else:
103                 # This way we won't try for ever if something goes wrong
104                 raise ArtifactOverwriteError
105
106     def upload_blueprint(self, cba_bytes: bytes, name: str, version: str) -> None:
107         """Store blueprint file in the repository.
108
109         :param cba_bytes: Bytes to save
110         :param name: Blueprint name
111         :param version: Blueprint version
112         """
113         temporary_file: BytesIO = BytesIO(cba_bytes)
114
115         if not is_zipfile(temporary_file):
116             raise InvalidRequestError
117
118         target_path: str = str(Path(self.base_path.absolute(), name, version))
119         self.__create_directory_tree(target_path)
120
121         with ZipFile(temporary_file, "r") as zip_file:  # type: ZipFile
122             zip_file.extractall(target_path)
123
124     def download_blueprint(self, name: str, version: str) -> bytes:
125         """Download blueprint file from repository.
126
127         This method does the in-memory zipping the files and returns bytes
128
129         :param name: Blueprint name
130         :param version: Blueprint version
131         :return: Zipped Blueprint file bytes
132         """
133         temporary_file: BytesIO = BytesIO()
134         files_path: str = str(Path(self.base_path.absolute(), name, version))
135         if not os.path.exists(files_path):
136             raise ArtifactNotFoundError
137
138         with ZipFile(temporary_file, "w") as zip_file:  # type: ZipFile
139             for directory_name, subdirectory_names, filenames in os.walk(files_path):  # type: str, list, list
140                 for filename in filenames:  # type: str
141                     zip_file.write(Path(directory_name, filename))
142
143         # Rewind the fake file to allow reading
144         temporary_file.seek(0)
145
146         zip_as_bytes: bytes = temporary_file.read()
147         temporary_file.close()
148         return zip_as_bytes
149
150     def remove_blueprint(self, name: str, version: str) -> None:
151         """Remove blueprint file from repository.
152
153         :param name: Blueprint name
154         :param version: Blueprint version
155         :raises: FileNotFoundError
156         """
157         files_path: str = str(Path(self.base_path.absolute(), name, version))
158         self.__remove_directory_tree(files_path)
159
160
161 class RepositoryStrategy(ABC):
162     """Strategy class.
163
164     It has only one public method `get_repository`, which returns valid repository
165     instance for the the configuration value.
166     You can create many Repository subclasses, but repository clients doesn't have
167     to know which one you use.
168     """
169
170     @classmethod
171     def get_reporitory(cls) -> Repository:
172         """Get the valid repository instance for the configuration value.
173
174         Currently it returns FileRepository because it is an only Repository implementation.
175         """
176         return FileRepository(Path(config["artifactManagerServer"]["fileRepositoryBasePath"]))