1
+ #
2
+ # Copyright (c) 2023 Red Hat, Inc.
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
+
1
16
import os
2
17
import sys
3
18
import yaml
4
19
import tarfile
5
20
import shutil
6
21
import subprocess
7
22
23
+ # This script is used to install dynamic plugins in the Backstage application,
24
+ # and is available in the container image to be called at container initialization,
25
+ # for example in an init container when using Kubernetes.
26
+ #
27
+ # It expects, as the only argument, the path to the root directory where
28
+ # the dynamic plugins will be installed.
29
+ #
30
+ # Additionally The MAX_ENTRY_SIZE environment variable can be defined to set
31
+ # the maximum size of a file in the archive (default: 10MB).
32
+ #
33
+ # It expects the `dynamic-plugins.yaml` file to be present in the current directory and
34
+ # to contain the list of plugins to install along with their optional configuration.
35
+ #
36
+ # The `dynamic-plugins.yaml` file must be a list of objects with the following properties:
37
+ # - `package`: the NPM package to install (either a package name or a path to a local package)
38
+ # - `pluginConfig`: an optional plugin-specific configuration fragment
39
+ #
40
+ # For each package mentioned in the `dynamic-plugins.yaml` file, the script will:
41
+ # - call `npm pack` to get the package archive and extract it in the dynamic plugins root directory
42
+ # - merge the plugin-specific configuration fragment in a global configuration file named `app-config.dynamic-plugins.yaml`
43
+ #
44
+
8
45
class InstallException (Exception ):
9
46
"""Exception class from which every exception in this library will derive."""
10
47
pass
11
48
12
- def merge (source , destination ):
49
+ def merge (source , destination , prefix = '' ):
13
50
for key , value in source .items ():
14
51
if isinstance (value , dict ):
15
52
# get node or create one
16
53
node = destination .setdefault (key , {})
17
- merge (value , node )
54
+ merge (value , node , key + '.' )
18
55
else :
19
56
# if key exists in destination trigger an error
20
- if key in destination :
21
- raise InstallException (' Config key ' + key + ' defined for 2 dynamic plugins' )
57
+ if key in destination and destination [ key ] != value :
58
+ raise InstallException (" Config key '" + prefix + key + " ' defined differently for 2 dynamic plugins" )
22
59
23
60
destination [key ] = value
24
61
@@ -28,16 +65,20 @@ def main():
28
65
dynamicPluginsRoot = sys .argv [1 ]
29
66
maxEntrySize = int (os .environ .get ('MAX_ENTRY_SIZE' , 10000000 ))
30
67
31
- dynamicPluginsFile = os .path .join (dynamicPluginsRoot , 'dynamic-plugins.yaml' )
68
+ dynamicPluginsFile = 'dynamic-plugins.yaml'
69
+ dynamicPluginsDefaultFile = 'dynamic-plugins.default.yaml'
32
70
dynamicPluginsGlobalConfigFile = os .path .join (dynamicPluginsRoot , 'app-config.dynamic-plugins.yaml' )
33
71
34
72
# test if file dynamic-plugins.yaml exists
35
73
if not os .path .isfile (dynamicPluginsFile ):
36
- print (f'No { dynamicPluginsFile } file found. Skipping dynamic plugins installation.' )
37
- with open (dynamicPluginsGlobalConfigFile , 'w' ) as file :
38
- file .write ('' )
39
- file .close ()
40
- exit (0 )
74
+ print (f'No { dynamicPluginsFile } file found, trying { dynamicPluginsDefaultFile } file.' )
75
+ dynamicPluginsFile = dynamicPluginsDefaultFile
76
+ if not os .path .isfile (dynamicPluginsFile ):
77
+ print (f'No { dynamicPluginsFile } file found. Skipping dynamic plugins installation.' )
78
+ with open (dynamicPluginsGlobalConfigFile , 'w' ) as file :
79
+ file .write ('' )
80
+ file .close ()
81
+ exit (0 )
41
82
42
83
with open (dynamicPluginsFile , 'r' ) as file :
43
84
plugins = yaml .safe_load (file )
@@ -74,6 +115,7 @@ def main():
74
115
75
116
archive = os .path .join (dynamicPluginsRoot , completed .stdout .decode ('utf-8' ).strip ())
76
117
directory = archive .replace ('.tgz' , '' )
118
+ directoryRealpath = os .path .realpath (directory )
77
119
78
120
print ('\t ==> Removing previous plugin directory' , directory , flush = True )
79
121
shutil .rmtree (directory , ignore_errors = True , onerror = None )
@@ -90,13 +132,33 @@ def main():
90
132
if member .size > maxEntrySize :
91
133
raise InstallException ('Zip bomb detected in ' + member .name )
92
134
93
- # Remove the `package/` prefix from the file name
94
- member .name = member .name [8 :]
135
+ member .name = member .name .removeprefix ('package/' )
95
136
file .extract (member , path = directory )
96
137
elif member .isdir ():
97
138
print ('\t \t Skipping directory entry' , member .name , flush = True )
139
+ elif member .islnk () or member .issym ():
140
+ if not member .linkpath .startswith ('package/' ):
141
+ raise InstallException ('NPM package archive contains a link outside of the archive: ' + member .name + ' -> ' + member .linkpath )
142
+
143
+ member .name = member .name .removeprefix ('package/' )
144
+ member .linkpath = member .linkpath .removeprefix ('package/' )
145
+
146
+ realpath = os .path .realpath (os .path .join (directory , * os .path .split (member .linkname )))
147
+ if not realpath .startswith (directoryRealpath ):
148
+ raise InstallException ('NPM package archive contains a link outside of the archive: ' + member .name + ' -> ' + member .linkpath )
149
+
150
+ file .extract (member , path = directory )
98
151
else :
99
- raise InstallException ('NPM package archive contains a non regular file: ' + member .name )
152
+ if member .type == tarfile .CHRTYPE :
153
+ type_str = "character device"
154
+ elif member .type == tarfile .BLKTYPE :
155
+ type_str = "block device"
156
+ elif member .type == tarfile .FIFOTYPE :
157
+ type_str = "FIFO"
158
+ else :
159
+ type_str = "unknown"
160
+
161
+ raise InstallException ('NPM package archive contains a non regular file: ' + member .name + ' - ' + type_str )
100
162
101
163
file .close ()
102
164
0 commit comments