lennybot 1.0.26__tar.gz → 1.0.33__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lennybot-1.0.26 → lennybot-1.0.33}/PKG-INFO +65 -8
- {lennybot-1.0.26 → lennybot-1.0.33}/README.md +49 -6
- {lennybot-1.0.26 → lennybot-1.0.33}/pyproject.toml +3 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/requirements.txt +1 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/__init__.py +1 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/__init__.py +6 -3
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/download_resources.py +11 -2
- lennybot-1.0.33/src/lennybot/actions/update_json.py +48 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/check/__init__.py +1 -2
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/check/docker_image_available.py +23 -4
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/config/config.py +22 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/helper/__init__.py +1 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/lennybot.py +4 -4
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/model/plan.py +1 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/apply.py +0 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/github.py +4 -2
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/plan.py +7 -2
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/source/__init__.py +3 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/source/source_github.py +0 -2
- lennybot-1.0.33/src/lennybot/service/source/source_nodejs.py +57 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot.egg-info/PKG-INFO +65 -8
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot.egg-info/SOURCES.txt +10 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot.egg-info/requires.txt +1 -0
- lennybot-1.0.33/test/test_config.py +11 -0
- lennybot-1.0.33/test/test_config_validation.py +41 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/test/test_docker_image_available.py +7 -1
- lennybot-1.0.33/test/test_source_nodejs.py +45 -0
- lennybot-1.0.33/test/test_update_dockerfile.py +60 -0
- lennybot-1.0.33/test/test_update_image_tag.py +48 -0
- lennybot-1.0.33/test/test_update_json.py +40 -0
- lennybot-1.0.33/test/test_update_yaml.py +42 -0
- lennybot-1.0.33/version.txt +1 -0
- lennybot-1.0.26/version.txt +0 -1
- {lennybot-1.0.26 → lennybot-1.0.33}/LICENSE +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/setup.cfg +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/setup.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/__main__.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/iaction.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/remove_checksums.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/update_dockerfile.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/update_image_tag.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/actions/update_yaml.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/check/icheck.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/config/__init__.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/model/__init__.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/model/state.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/__init__.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/source/isource.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot/service/source/source_github_query.py +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot.egg-info/dependency_links.txt +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot.egg-info/entry_points.txt +0 -0
- {lennybot-1.0.26 → lennybot-1.0.33}/src/lennybot.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lennybot
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.33
|
|
4
4
|
Summary: Automatic Updates for Kustomize Resources
|
|
5
5
|
Home-page: http://github.com/raynigon/lennybot
|
|
6
6
|
Author: Simon Schneider
|
|
@@ -23,6 +23,7 @@ Requires-Dist: yamlpath
|
|
|
23
23
|
Requires-Dist: requests
|
|
24
24
|
Requires-Dist: GitPython
|
|
25
25
|
Requires-Dist: PyGithub
|
|
26
|
+
Requires-Dist: jsonpath-ng
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: setuptools; extra == "dev"
|
|
28
29
|
Requires-Dist: wheel; extra == "dev"
|
|
@@ -30,6 +31,19 @@ Requires-Dist: black; extra == "dev"
|
|
|
30
31
|
Provides-Extra: test
|
|
31
32
|
Requires-Dist: coverage; extra == "test"
|
|
32
33
|
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: author-email
|
|
36
|
+
Dynamic: classifier
|
|
37
|
+
Dynamic: description
|
|
38
|
+
Dynamic: description-content-type
|
|
39
|
+
Dynamic: home-page
|
|
40
|
+
Dynamic: keywords
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
Dynamic: project-url
|
|
43
|
+
Dynamic: provides-extra
|
|
44
|
+
Dynamic: requires-dist
|
|
45
|
+
Dynamic: requires-python
|
|
46
|
+
Dynamic: summary
|
|
33
47
|
|
|
34
48
|
[](https://pypi.org/project/lennybot/)
|
|
35
49
|
# lennybot
|
|
@@ -64,7 +78,13 @@ The lennybot allows to define multiple applications.
|
|
|
64
78
|
Each application has to have a version source, which can be queried to determine the latest version.
|
|
65
79
|
If a newer version is available, the lennybot executes multiple pre defined actions per application.
|
|
66
80
|
E.g. Update Docker Image Tags.
|
|
67
|
-
|
|
81
|
+
Sometimes there are conditions which need to be fulfilled before the action can be executed.
|
|
82
|
+
These conditions can be specified as checks.
|
|
83
|
+
E.g. Check if the docker image is available in the registry, because sometimes a new version of an applications gets released, but the docker image is not available yet.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
The applications, sources, checks and actions can be configured in the `config.yml` file.
|
|
68
88
|
For more information see below.
|
|
69
89
|
|
|
70
90
|
## Configuration
|
|
@@ -85,19 +105,16 @@ Each section represents a configuration object.
|
|
|
85
105
|
|
|
86
106
|
| Path | Description |
|
|
87
107
|
|--------------------------------------------|------------------------------------------------------------------------|
|
|
88
|
-
| state.file | The state file which is used to store the version of each application |
|
|
89
108
|
| state.pr.enabled | Toggle PR creation in CI mode. Has to be either true or false |
|
|
90
109
|
| state.pr.repository | The name of the repository in github on which the PR should be created |
|
|
91
110
|
| state.pr.branchPrefix | Prefix for the branch name which should be used to create the PRs |
|
|
92
111
|
|
|
93
112
|
### Applications
|
|
94
113
|
|
|
95
|
-
|
|
|
114
|
+
| Property | Description |
|
|
96
115
|
|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
97
116
|
| applications[*].name | The name of the application which should be updated |
|
|
98
|
-
| applications[*].source
|
|
99
|
-
| applications[*].source.repository | The GitHub Repository which should be used to determine the latest version |
|
|
100
|
-
| applications[*].source.regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
117
|
+
| applications[*].source | The configuration for the source of the latest version. This is specific to the type of source, see below for the diffrent source types. |
|
|
101
118
|
| applications[\*].actions[\*].type | The action has to be one of these types "image-tag-update", "download-resources" or "update-yaml". See below for details. |
|
|
102
119
|
| applications[\*].actions[\*].url | |
|
|
103
120
|
| applications[\*].actions[\*].target | |
|
|
@@ -108,21 +125,61 @@ Each section represents a configuration object.
|
|
|
108
125
|
| applications[\*].actions[\*].yamlPath | |
|
|
109
126
|
| applications[\*].actions[\*].valuePattern | |
|
|
110
127
|
|
|
128
|
+
### Sources
|
|
129
|
+
|
|
111
130
|
#### GitHub Source
|
|
131
|
+
|
|
112
132
|
<TODO>
|
|
113
133
|
|
|
134
|
+
| Property | Description |
|
|
135
|
+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
136
|
+
| .type | The source has to be either of the type "github" or of the type "github-query". See below for details. |
|
|
137
|
+
| .repository | The GitHub Repository which should be used to determine the latest version |
|
|
138
|
+
| .regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
139
|
+
|
|
140
|
+
|
|
114
141
|
#### GitHub Query Source
|
|
142
|
+
|
|
143
|
+
<TODO>
|
|
144
|
+
|
|
145
|
+
| Property | Description |
|
|
146
|
+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
147
|
+
| .type | The source has to be either of the type "github" or of the type "github-query". See below for details. |
|
|
148
|
+
| .repository | The GitHub Repository which should be used to determine the latest version |
|
|
149
|
+
| .regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### Checks
|
|
153
|
+
|
|
115
154
|
<TODO>
|
|
116
155
|
|
|
156
|
+
|
|
157
|
+
### Actions
|
|
158
|
+
|
|
117
159
|
#### Image Tag Update Action
|
|
118
160
|
<TODO>
|
|
119
161
|
|
|
162
|
+
| Property | Description |
|
|
163
|
+
|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
164
|
+
| .type | The action has to be one of these types "image-tag-update", "download-resources" or "update-yaml". See below for details. |
|
|
165
|
+
| .url | |
|
|
166
|
+
| .target | |
|
|
167
|
+
| .image | |
|
|
168
|
+
| .kustomizePath | |
|
|
169
|
+
| .tagPattern | |
|
|
170
|
+
| .targetFile | |
|
|
171
|
+
| .yamlPath | |
|
|
172
|
+
| .valuePattern | |
|
|
173
|
+
|
|
120
174
|
#### Download Resource Action
|
|
121
175
|
<TODO>
|
|
122
176
|
|
|
123
177
|
#### Update YAML Action
|
|
124
178
|
<TODO>
|
|
125
179
|
|
|
180
|
+
#### Update JSON Action
|
|
181
|
+
<TODO>
|
|
182
|
+
|
|
126
183
|
#### Update Dockerfile Action
|
|
127
184
|
<TODO>
|
|
128
185
|
|
|
@@ -31,7 +31,13 @@ The lennybot allows to define multiple applications.
|
|
|
31
31
|
Each application has to have a version source, which can be queried to determine the latest version.
|
|
32
32
|
If a newer version is available, the lennybot executes multiple pre defined actions per application.
|
|
33
33
|
E.g. Update Docker Image Tags.
|
|
34
|
-
|
|
34
|
+
Sometimes there are conditions which need to be fulfilled before the action can be executed.
|
|
35
|
+
These conditions can be specified as checks.
|
|
36
|
+
E.g. Check if the docker image is available in the registry, because sometimes a new version of an applications gets released, but the docker image is not available yet.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
The applications, sources, checks and actions can be configured in the `config.yml` file.
|
|
35
41
|
For more information see below.
|
|
36
42
|
|
|
37
43
|
## Configuration
|
|
@@ -52,19 +58,16 @@ Each section represents a configuration object.
|
|
|
52
58
|
|
|
53
59
|
| Path | Description |
|
|
54
60
|
|--------------------------------------------|------------------------------------------------------------------------|
|
|
55
|
-
| state.file | The state file which is used to store the version of each application |
|
|
56
61
|
| state.pr.enabled | Toggle PR creation in CI mode. Has to be either true or false |
|
|
57
62
|
| state.pr.repository | The name of the repository in github on which the PR should be created |
|
|
58
63
|
| state.pr.branchPrefix | Prefix for the branch name which should be used to create the PRs |
|
|
59
64
|
|
|
60
65
|
### Applications
|
|
61
66
|
|
|
62
|
-
|
|
|
67
|
+
| Property | Description |
|
|
63
68
|
|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
64
69
|
| applications[*].name | The name of the application which should be updated |
|
|
65
|
-
| applications[*].source
|
|
66
|
-
| applications[*].source.repository | The GitHub Repository which should be used to determine the latest version |
|
|
67
|
-
| applications[*].source.regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
70
|
+
| applications[*].source | The configuration for the source of the latest version. This is specific to the type of source, see below for the diffrent source types. |
|
|
68
71
|
| applications[\*].actions[\*].type | The action has to be one of these types "image-tag-update", "download-resources" or "update-yaml". See below for details. |
|
|
69
72
|
| applications[\*].actions[\*].url | |
|
|
70
73
|
| applications[\*].actions[\*].target | |
|
|
@@ -75,21 +78,61 @@ Each section represents a configuration object.
|
|
|
75
78
|
| applications[\*].actions[\*].yamlPath | |
|
|
76
79
|
| applications[\*].actions[\*].valuePattern | |
|
|
77
80
|
|
|
81
|
+
### Sources
|
|
82
|
+
|
|
78
83
|
#### GitHub Source
|
|
84
|
+
|
|
79
85
|
<TODO>
|
|
80
86
|
|
|
87
|
+
| Property | Description |
|
|
88
|
+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
89
|
+
| .type | The source has to be either of the type "github" or of the type "github-query". See below for details. |
|
|
90
|
+
| .repository | The GitHub Repository which should be used to determine the latest version |
|
|
91
|
+
| .regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
92
|
+
|
|
93
|
+
|
|
81
94
|
#### GitHub Query Source
|
|
95
|
+
|
|
96
|
+
<TODO>
|
|
97
|
+
|
|
98
|
+
| Property | Description |
|
|
99
|
+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
100
|
+
| .type | The source has to be either of the type "github" or of the type "github-query". See below for details. |
|
|
101
|
+
| .repository | The GitHub Repository which should be used to determine the latest version |
|
|
102
|
+
| .regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
### Checks
|
|
106
|
+
|
|
82
107
|
<TODO>
|
|
83
108
|
|
|
109
|
+
|
|
110
|
+
### Actions
|
|
111
|
+
|
|
84
112
|
#### Image Tag Update Action
|
|
85
113
|
<TODO>
|
|
86
114
|
|
|
115
|
+
| Property | Description |
|
|
116
|
+
|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
117
|
+
| .type | The action has to be one of these types "image-tag-update", "download-resources" or "update-yaml". See below for details. |
|
|
118
|
+
| .url | |
|
|
119
|
+
| .target | |
|
|
120
|
+
| .image | |
|
|
121
|
+
| .kustomizePath | |
|
|
122
|
+
| .tagPattern | |
|
|
123
|
+
| .targetFile | |
|
|
124
|
+
| .yamlPath | |
|
|
125
|
+
| .valuePattern | |
|
|
126
|
+
|
|
87
127
|
#### Download Resource Action
|
|
88
128
|
<TODO>
|
|
89
129
|
|
|
90
130
|
#### Update YAML Action
|
|
91
131
|
<TODO>
|
|
92
132
|
|
|
133
|
+
#### Update JSON Action
|
|
134
|
+
<TODO>
|
|
135
|
+
|
|
93
136
|
#### Update Dockerfile Action
|
|
94
137
|
<TODO>
|
|
95
138
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from .download_resources import
|
|
1
|
+
from .download_resources import DownloadResourceAction
|
|
2
2
|
from .iaction import IAction
|
|
3
3
|
from .remove_checksums import RemoveChecksumsAction
|
|
4
4
|
from .update_dockerfile import UpdateDockerfileAction
|
|
5
5
|
from .update_image_tag import UpdateImageTagAction
|
|
6
|
+
from .update_json import UpdateJsonAction
|
|
6
7
|
from .update_yaml import UpdateYamlAction
|
|
7
8
|
|
|
8
9
|
|
|
@@ -10,8 +11,10 @@ def create_action(name, source_version, latest_version, config) -> IAction:
|
|
|
10
11
|
action_type = config.type
|
|
11
12
|
if action_type == "image-tag-update":
|
|
12
13
|
return UpdateImageTagAction(name, source_version, latest_version, config)
|
|
13
|
-
if action_type
|
|
14
|
-
return
|
|
14
|
+
if action_type in ["download-resource", "download-resources"]:
|
|
15
|
+
return DownloadResourceAction(name, source_version, latest_version, config)
|
|
16
|
+
if action_type == "update-json":
|
|
17
|
+
return UpdateJsonAction(name, source_version, latest_version, config)
|
|
15
18
|
if action_type == "update-yaml":
|
|
16
19
|
return UpdateYamlAction(name, source_version, latest_version, config)
|
|
17
20
|
if action_type == "update-dockerfile":
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
import requests
|
|
2
4
|
|
|
3
5
|
from ..config.config import LennyBotActionConfig
|
|
4
6
|
from .iaction import IAction
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
class
|
|
9
|
+
class DownloadResourceAction(IAction):
|
|
8
10
|
def __init__(self, name, source_version, target_version, config: LennyBotActionConfig) -> None:
|
|
11
|
+
self._log = logging.getLogger(self.__class__.__name__)
|
|
9
12
|
self._name = name
|
|
10
13
|
self._source_version = source_version
|
|
11
14
|
self._target_version = target_version
|
|
@@ -30,8 +33,14 @@ class DownloadResourcesAction(IAction):
|
|
|
30
33
|
|
|
31
34
|
def run(self):
|
|
32
35
|
download_url = self._url.replace("{{version}}", self._target_version)
|
|
36
|
+
self._log.debug("Downloading resource from %s to %s", download_url, self._target_path)
|
|
33
37
|
response = requests.get(download_url)
|
|
34
38
|
if response.status_code != 200:
|
|
35
|
-
|
|
39
|
+
self._log.error(
|
|
40
|
+
"Unable to download resource, received status code: %d\n%s", response.status_code, response.text
|
|
41
|
+
)
|
|
42
|
+
raise Exception("Unable to download resource, received status code: " + str(response.status_code))
|
|
43
|
+
self._log.debug("Downloaded resources successfully")
|
|
36
44
|
with open(self._target_path, "w", encoding="utf-8") as file_ptr:
|
|
37
45
|
file_ptr.write(response.text)
|
|
46
|
+
self._log.debug("Saved resource to %s", self._target_path)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from jsonpath_ng import parse
|
|
4
|
+
|
|
5
|
+
from ..config.config import LennyBotActionConfig
|
|
6
|
+
from .iaction import IAction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UpdateJsonAction(IAction):
|
|
10
|
+
def __init__(self, name, source_version, target_version, config: LennyBotActionConfig) -> None:
|
|
11
|
+
self._name = name
|
|
12
|
+
self._source_version = source_version
|
|
13
|
+
self._target_version = target_version
|
|
14
|
+
if config.target_file is None:
|
|
15
|
+
raise Exception("Target file is not set for application " + name)
|
|
16
|
+
self._target_file = config.target_file
|
|
17
|
+
if config.json_path is None:
|
|
18
|
+
raise Exception("JSON Path is not set for application " + name)
|
|
19
|
+
self._json_path = parse(config.json_path)
|
|
20
|
+
if config.value_pattern is not None:
|
|
21
|
+
self._value_pattern = config.value_pattern
|
|
22
|
+
else:
|
|
23
|
+
self._value_pattern = "{{version}}"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def application(self) -> str:
|
|
27
|
+
return self._name
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def source_version(self) -> str:
|
|
31
|
+
return self._source_version
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def target_version(self) -> str:
|
|
35
|
+
return self._target_version
|
|
36
|
+
|
|
37
|
+
def run(self):
|
|
38
|
+
# Read the JSON data from the file
|
|
39
|
+
with open(self._target_file, "r", encoding="utf-8") as file_ptr:
|
|
40
|
+
json_data = json.load(file_ptr)
|
|
41
|
+
# Update the value in the JSON data
|
|
42
|
+
self._json_path.update(json_data, self._create_value())
|
|
43
|
+
# Write the updated JSON data back to the file
|
|
44
|
+
with open(self._target_file, "w", encoding="utf-8") as file_ptr:
|
|
45
|
+
json.dump(json_data, fp=file_ptr, indent=4)
|
|
46
|
+
|
|
47
|
+
def _create_value(self):
|
|
48
|
+
return self._value_pattern.replace("{{version}}", self._target_version)
|
|
@@ -5,10 +5,10 @@ from urllib.parse import urlencode
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
|
|
8
|
-
from ..config.config import LennyBotCheckConfig, LennyBotConfigContainerConfig
|
|
8
|
+
from ..config.config import LennyBotCheckConfig, LennyBotConfigContainerConfig
|
|
9
9
|
from .icheck import ICheck
|
|
10
10
|
|
|
11
|
-
PATTERN = r"(?:([\-\_\.\w]+)$)|(?:([\-\_\.\w]+)/([\-\_\.\w]+)$)|(?:([\-\.A-z0-9]+)/([\-\_\.\w]+)/([\-\_\.\w]+)$)|(?:([\-\.A-z0-9]+)/([\-\_\.\w]+)/([\-\_\.\w]+)/([\-\_\.\w]+)$)"
|
|
11
|
+
PATTERN = r"(?:([\-\_\.\w]+)$)|(?:([\-\_\.\w]+)/([\-\_\.\w]+)$)|(?:([\-\.A-z0-9]+)/([\-\_\.\w]+)/([\-\_\.\w]+)$)|(?:([\-\.A-z0-9]+)/([\-\_\.\w]+)/([\-\_\.\w]+)/([\-\_\.\w]+)$)|(?:([\-\.A-z0-9]+)/([\-\.A-z0-9]+)/([\-\_\.\w]+)/([\-\_\.\w]+)/([\-\_\.\w]+)$)"
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class DockerImage:
|
|
@@ -34,6 +34,7 @@ class WwwAuthenticateHeader:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class DockerImageAvailableCheck(ICheck):
|
|
37
|
+
# pylint: disable=too-many-positional-arguments
|
|
37
38
|
def __init__(
|
|
38
39
|
self,
|
|
39
40
|
application_name,
|
|
@@ -103,9 +104,27 @@ class DockerImageAvailableCheck(ICheck):
|
|
|
103
104
|
+ image_tag
|
|
104
105
|
)
|
|
105
106
|
return DockerImage(match.group(4), match.group(5) + "/" + match.group(6), image_tag)
|
|
107
|
+
if match.group(7) is not None:
|
|
108
|
+
logging.debug(
|
|
109
|
+
"regex matched following pattern: "
|
|
110
|
+
+ match.group(7)
|
|
111
|
+
+ "/"
|
|
112
|
+
+ match.group(8)
|
|
113
|
+
+ "/"
|
|
114
|
+
+ match.group(9)
|
|
115
|
+
+ "/"
|
|
116
|
+
+ match.group(10)
|
|
117
|
+
+ " "
|
|
118
|
+
+ image_tag
|
|
119
|
+
)
|
|
120
|
+
return DockerImage(
|
|
121
|
+
match.group(7),
|
|
122
|
+
match.group(8) + "/" + match.group(9) + "/" + match.group(10),
|
|
123
|
+
image_tag,
|
|
124
|
+
)
|
|
106
125
|
return DockerImage(
|
|
107
|
-
match.group(
|
|
108
|
-
match.group(
|
|
126
|
+
match.group(11),
|
|
127
|
+
match.group(12) + "/" + match.group(13) + "/" + match.group(14) + "/" + match.group(15),
|
|
109
128
|
image_tag,
|
|
110
129
|
)
|
|
111
130
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
5
|
import yaml
|
|
6
6
|
|
|
@@ -67,6 +67,7 @@ CONFIGURATION_OPTIONS = {
|
|
|
67
67
|
"required": True,
|
|
68
68
|
"attribute": "_repository",
|
|
69
69
|
},
|
|
70
|
+
"ltsOnly": {"type": "bool", "required": False, "attribute": "_lts_only"},
|
|
70
71
|
"regex": {"type": "string", "attribute": "_regex"},
|
|
71
72
|
},
|
|
72
73
|
},
|
|
@@ -92,6 +93,7 @@ CONFIGURATION_OPTIONS = {
|
|
|
92
93
|
"target": {"type": "string", "attribute": "_target"},
|
|
93
94
|
"targetFile": {"type": "string", "attribute": "_target_file"},
|
|
94
95
|
"yamlPath": {"type": "string", "attribute": "_yaml_path"},
|
|
96
|
+
"jsonPath": {"type": "string", "attribute": "_json_path"},
|
|
95
97
|
"valuePattern": {"type": "string", "attribute": "_value_pattern"},
|
|
96
98
|
},
|
|
97
99
|
},
|
|
@@ -105,6 +107,8 @@ class LennyBotSourceConfig:
|
|
|
105
107
|
self._type = None
|
|
106
108
|
self._repository = None
|
|
107
109
|
self._regex = None
|
|
110
|
+
self._source_url = None
|
|
111
|
+
self._lts_only = None
|
|
108
112
|
|
|
109
113
|
@property
|
|
110
114
|
def type(self) -> str:
|
|
@@ -118,6 +122,18 @@ class LennyBotSourceConfig:
|
|
|
118
122
|
def regex(self) -> str:
|
|
119
123
|
return str(self._regex)
|
|
120
124
|
|
|
125
|
+
@property
|
|
126
|
+
def source_url(self) -> str:
|
|
127
|
+
return str(self._source_url)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def lts_only(self) -> bool:
|
|
131
|
+
return bool(self._lts_only)
|
|
132
|
+
|
|
133
|
+
@lts_only.setter
|
|
134
|
+
def lts_only(self, value: bool) -> None:
|
|
135
|
+
self._lts_only = value
|
|
136
|
+
|
|
121
137
|
|
|
122
138
|
class LennyBotCheckConfig:
|
|
123
139
|
def __init__(self) -> None:
|
|
@@ -143,6 +159,7 @@ class LennyBotActionConfig:
|
|
|
143
159
|
self._url = None
|
|
144
160
|
self._target_file = None
|
|
145
161
|
self._yaml_path = None
|
|
162
|
+
self._json_path = None
|
|
146
163
|
self._value_pattern = None
|
|
147
164
|
|
|
148
165
|
@property
|
|
@@ -173,6 +190,10 @@ class LennyBotActionConfig:
|
|
|
173
190
|
def target_file(self) -> str | None:
|
|
174
191
|
return self._target_file
|
|
175
192
|
|
|
193
|
+
@property
|
|
194
|
+
def json_path(self) -> str | None:
|
|
195
|
+
return self._json_path
|
|
196
|
+
|
|
176
197
|
@property
|
|
177
198
|
def yaml_path(self) -> str | None:
|
|
178
199
|
return self._yaml_path
|
|
@@ -47,7 +47,7 @@ class LennyBot:
|
|
|
47
47
|
pickle.dump(plan, file_ptr)
|
|
48
48
|
|
|
49
49
|
def ci_setup(self):
|
|
50
|
-
self._log.debug(
|
|
50
|
+
self._log.debug("Setup CI")
|
|
51
51
|
now = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
52
52
|
self._branch_name = f"{self._config.github_pr.branch_prefix}"
|
|
53
53
|
if not self._branch_name.endswith("-"):
|
|
@@ -58,14 +58,14 @@ class LennyBot:
|
|
|
58
58
|
if result != 0:
|
|
59
59
|
self._log.error("Unexpected return code from git config")
|
|
60
60
|
self._repo = Repo("./", odbt=GitDB) # type: ignore
|
|
61
|
-
self._log.debug(
|
|
61
|
+
self._log.debug("Initialized repository")
|
|
62
62
|
head = self._repo.create_head(self._branch_name)
|
|
63
|
-
self._log.debug(
|
|
63
|
+
self._log.debug("Created Head")
|
|
64
64
|
head.checkout()
|
|
65
65
|
self._log.info(f"Working branch is {self._branch_name}")
|
|
66
66
|
|
|
67
67
|
def ci_finalize(self, plan: LennyBotPlan, result):
|
|
68
|
-
self._log.debug(
|
|
68
|
+
self._log.debug("Finalize CI")
|
|
69
69
|
if self._repo is None:
|
|
70
70
|
raise Exception("Repository is non, ci_setup was not called")
|
|
71
71
|
if not self._repo.index.diff(None) and not self._repo.untracked_files:
|
|
@@ -34,7 +34,9 @@ class GitHubService:
|
|
|
34
34
|
if self._github is None:
|
|
35
35
|
raise Exception("GitHub is not configured")
|
|
36
36
|
repo = self._github.get_repo(self._config.github_pr.repository)
|
|
37
|
-
new_pull = repo.create_pull(
|
|
37
|
+
new_pull = repo.create_pull(
|
|
38
|
+
repo.default_branch, branch_name, title=title, body=body
|
|
39
|
+
) # pyright: ignore [reportCallIssue]
|
|
38
40
|
labels = self._get_or_create_labels(repo)
|
|
39
41
|
new_pull.add_to_labels(*labels)
|
|
40
42
|
pulls = self._find_own_pulls()
|
|
@@ -59,7 +61,7 @@ class GitHubService:
|
|
|
59
61
|
return result
|
|
60
62
|
|
|
61
63
|
def _headers(self) -> Dict:
|
|
62
|
-
headers = {}
|
|
64
|
+
headers = {"X-GitHub-Api-Version": "2022-11-28"}
|
|
63
65
|
if self._token is not None:
|
|
64
66
|
headers["Authorization"] = f"Bearer {self._token}"
|
|
65
67
|
return headers
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import List
|
|
3
3
|
|
|
4
|
-
from lennybot.check import create_check
|
|
5
|
-
|
|
6
4
|
from ..actions import IAction, create_action
|
|
5
|
+
from ..check import create_check
|
|
7
6
|
from ..config import LennyBotAppConfig, LennyBotConfig
|
|
8
7
|
from ..helper import semver_2_vc
|
|
9
8
|
from ..model import LennyBotPlan, LennyBotState
|
|
@@ -49,6 +48,12 @@ class LennyBotApplication:
|
|
|
49
48
|
latest_vc = semver_2_vc(self._latest_version)
|
|
50
49
|
|
|
51
50
|
if current_vc >= latest_vc:
|
|
51
|
+
self._log.warning(
|
|
52
|
+
"For '%s' the current version '%s' is greater than '%s'. Downgrades are not supported",
|
|
53
|
+
self._name,
|
|
54
|
+
self._current_version,
|
|
55
|
+
self._latest_version,
|
|
56
|
+
)
|
|
52
57
|
return False
|
|
53
58
|
|
|
54
59
|
for check in self._checks:
|
|
@@ -2,6 +2,7 @@ from ...config import LennyBotSourceConfig
|
|
|
2
2
|
from .isource import ISource
|
|
3
3
|
from .source_github import GithubSource
|
|
4
4
|
from .source_github_query import GithubQuerySource
|
|
5
|
+
from .source_nodejs import NodeJSVersionSource
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def create_source(name, config: LennyBotSourceConfig, github) -> ISource:
|
|
@@ -10,4 +11,6 @@ def create_source(name, config: LennyBotSourceConfig, github) -> ISource:
|
|
|
10
11
|
return GithubSource(name, config, github)
|
|
11
12
|
if source_type == "github-query":
|
|
12
13
|
return GithubQuerySource(name, config, github)
|
|
14
|
+
if source_type == "nodejs-version":
|
|
15
|
+
return NodeJSVersionSource(name, config)
|
|
13
16
|
raise Exception(f"Unknown Source Type: {source_type}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from ...config import LennyBotSourceConfig
|
|
6
|
+
from .isource import ISource
|
|
7
|
+
|
|
8
|
+
NODEJS_ORG_VERSIONS_URL = "https://nodejs.org/dist/index.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeJSVersionNotFoundException(Exception):
|
|
12
|
+
def __init__(self, data: Any, *args: object) -> None:
|
|
13
|
+
super().__init__(*args)
|
|
14
|
+
self._data = data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NodeJSFormatException(Exception):
|
|
18
|
+
def __init__(self, *args: object) -> None:
|
|
19
|
+
super().__init__(*args)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NodeJSVersionSource(ISource):
|
|
23
|
+
def __init__(self, name, config: LennyBotSourceConfig) -> None:
|
|
24
|
+
self._name = name
|
|
25
|
+
self._lts_only = config.lts_only
|
|
26
|
+
self._source_url = config.source_url
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def application(self) -> str:
|
|
30
|
+
return self._name
|
|
31
|
+
|
|
32
|
+
def latest_version(self) -> str:
|
|
33
|
+
headers = {"user-agent": "lennybot/0.0.1"}
|
|
34
|
+
response = requests.get(NODEJS_ORG_VERSIONS_URL, headers=headers)
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
|
|
37
|
+
releases = response.json()
|
|
38
|
+
sorted(releases, key=lambda x: x["version"])
|
|
39
|
+
for release in releases:
|
|
40
|
+
if not self._lts_only:
|
|
41
|
+
return self._extract_semver_version(release)
|
|
42
|
+
if release["lts"]:
|
|
43
|
+
return self._extract_semver_version(release)
|
|
44
|
+
|
|
45
|
+
raise NodeJSVersionNotFoundException(releases)
|
|
46
|
+
|
|
47
|
+
def _extract_semver_version(self, release) -> str:
|
|
48
|
+
if "version" not in release.keys():
|
|
49
|
+
raise NodeJSFormatException("Missing version field in release")
|
|
50
|
+
version = release["version"]
|
|
51
|
+
|
|
52
|
+
if not version.startswith("v"):
|
|
53
|
+
raise NodeJSFormatException("Invalid version format")
|
|
54
|
+
|
|
55
|
+
version = version.replace("v", "")
|
|
56
|
+
|
|
57
|
+
return version
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lennybot
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.33
|
|
4
4
|
Summary: Automatic Updates for Kustomize Resources
|
|
5
5
|
Home-page: http://github.com/raynigon/lennybot
|
|
6
6
|
Author: Simon Schneider
|
|
@@ -23,6 +23,7 @@ Requires-Dist: yamlpath
|
|
|
23
23
|
Requires-Dist: requests
|
|
24
24
|
Requires-Dist: GitPython
|
|
25
25
|
Requires-Dist: PyGithub
|
|
26
|
+
Requires-Dist: jsonpath-ng
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: setuptools; extra == "dev"
|
|
28
29
|
Requires-Dist: wheel; extra == "dev"
|
|
@@ -30,6 +31,19 @@ Requires-Dist: black; extra == "dev"
|
|
|
30
31
|
Provides-Extra: test
|
|
31
32
|
Requires-Dist: coverage; extra == "test"
|
|
32
33
|
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: author-email
|
|
36
|
+
Dynamic: classifier
|
|
37
|
+
Dynamic: description
|
|
38
|
+
Dynamic: description-content-type
|
|
39
|
+
Dynamic: home-page
|
|
40
|
+
Dynamic: keywords
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
Dynamic: project-url
|
|
43
|
+
Dynamic: provides-extra
|
|
44
|
+
Dynamic: requires-dist
|
|
45
|
+
Dynamic: requires-python
|
|
46
|
+
Dynamic: summary
|
|
33
47
|
|
|
34
48
|
[](https://pypi.org/project/lennybot/)
|
|
35
49
|
# lennybot
|
|
@@ -64,7 +78,13 @@ The lennybot allows to define multiple applications.
|
|
|
64
78
|
Each application has to have a version source, which can be queried to determine the latest version.
|
|
65
79
|
If a newer version is available, the lennybot executes multiple pre defined actions per application.
|
|
66
80
|
E.g. Update Docker Image Tags.
|
|
67
|
-
|
|
81
|
+
Sometimes there are conditions which need to be fulfilled before the action can be executed.
|
|
82
|
+
These conditions can be specified as checks.
|
|
83
|
+
E.g. Check if the docker image is available in the registry, because sometimes a new version of an applications gets released, but the docker image is not available yet.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
The applications, sources, checks and actions can be configured in the `config.yml` file.
|
|
68
88
|
For more information see below.
|
|
69
89
|
|
|
70
90
|
## Configuration
|
|
@@ -85,19 +105,16 @@ Each section represents a configuration object.
|
|
|
85
105
|
|
|
86
106
|
| Path | Description |
|
|
87
107
|
|--------------------------------------------|------------------------------------------------------------------------|
|
|
88
|
-
| state.file | The state file which is used to store the version of each application |
|
|
89
108
|
| state.pr.enabled | Toggle PR creation in CI mode. Has to be either true or false |
|
|
90
109
|
| state.pr.repository | The name of the repository in github on which the PR should be created |
|
|
91
110
|
| state.pr.branchPrefix | Prefix for the branch name which should be used to create the PRs |
|
|
92
111
|
|
|
93
112
|
### Applications
|
|
94
113
|
|
|
95
|
-
|
|
|
114
|
+
| Property | Description |
|
|
96
115
|
|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
97
116
|
| applications[*].name | The name of the application which should be updated |
|
|
98
|
-
| applications[*].source
|
|
99
|
-
| applications[*].source.repository | The GitHub Repository which should be used to determine the latest version |
|
|
100
|
-
| applications[*].source.regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
117
|
+
| applications[*].source | The configuration for the source of the latest version. This is specific to the type of source, see below for the diffrent source types. |
|
|
101
118
|
| applications[\*].actions[\*].type | The action has to be one of these types "image-tag-update", "download-resources" or "update-yaml". See below for details. |
|
|
102
119
|
| applications[\*].actions[\*].url | |
|
|
103
120
|
| applications[\*].actions[\*].target | |
|
|
@@ -108,21 +125,61 @@ Each section represents a configuration object.
|
|
|
108
125
|
| applications[\*].actions[\*].yamlPath | |
|
|
109
126
|
| applications[\*].actions[\*].valuePattern | |
|
|
110
127
|
|
|
128
|
+
### Sources
|
|
129
|
+
|
|
111
130
|
#### GitHub Source
|
|
131
|
+
|
|
112
132
|
<TODO>
|
|
113
133
|
|
|
134
|
+
| Property | Description |
|
|
135
|
+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
136
|
+
| .type | The source has to be either of the type "github" or of the type "github-query". See below for details. |
|
|
137
|
+
| .repository | The GitHub Repository which should be used to determine the latest version |
|
|
138
|
+
| .regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
139
|
+
|
|
140
|
+
|
|
114
141
|
#### GitHub Query Source
|
|
142
|
+
|
|
143
|
+
<TODO>
|
|
144
|
+
|
|
145
|
+
| Property | Description |
|
|
146
|
+
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
147
|
+
| .type | The source has to be either of the type "github" or of the type "github-query". See below for details. |
|
|
148
|
+
| .repository | The GitHub Repository which should be used to determine the latest version |
|
|
149
|
+
| .regex | The regex pattern which is used to extract the semver version code from the tag value |
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### Checks
|
|
153
|
+
|
|
115
154
|
<TODO>
|
|
116
155
|
|
|
156
|
+
|
|
157
|
+
### Actions
|
|
158
|
+
|
|
117
159
|
#### Image Tag Update Action
|
|
118
160
|
<TODO>
|
|
119
161
|
|
|
162
|
+
| Property | Description |
|
|
163
|
+
|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
164
|
+
| .type | The action has to be one of these types "image-tag-update", "download-resources" or "update-yaml". See below for details. |
|
|
165
|
+
| .url | |
|
|
166
|
+
| .target | |
|
|
167
|
+
| .image | |
|
|
168
|
+
| .kustomizePath | |
|
|
169
|
+
| .tagPattern | |
|
|
170
|
+
| .targetFile | |
|
|
171
|
+
| .yamlPath | |
|
|
172
|
+
| .valuePattern | |
|
|
173
|
+
|
|
120
174
|
#### Download Resource Action
|
|
121
175
|
<TODO>
|
|
122
176
|
|
|
123
177
|
#### Update YAML Action
|
|
124
178
|
<TODO>
|
|
125
179
|
|
|
180
|
+
#### Update JSON Action
|
|
181
|
+
<TODO>
|
|
182
|
+
|
|
126
183
|
#### Update Dockerfile Action
|
|
127
184
|
<TODO>
|
|
128
185
|
|
|
@@ -19,6 +19,7 @@ src/lennybot/actions/iaction.py
|
|
|
19
19
|
src/lennybot/actions/remove_checksums.py
|
|
20
20
|
src/lennybot/actions/update_dockerfile.py
|
|
21
21
|
src/lennybot/actions/update_image_tag.py
|
|
22
|
+
src/lennybot/actions/update_json.py
|
|
22
23
|
src/lennybot/actions/update_yaml.py
|
|
23
24
|
src/lennybot/check/__init__.py
|
|
24
25
|
src/lennybot/check/docker_image_available.py
|
|
@@ -37,4 +38,12 @@ src/lennybot/service/source/__init__.py
|
|
|
37
38
|
src/lennybot/service/source/isource.py
|
|
38
39
|
src/lennybot/service/source/source_github.py
|
|
39
40
|
src/lennybot/service/source/source_github_query.py
|
|
40
|
-
|
|
41
|
+
src/lennybot/service/source/source_nodejs.py
|
|
42
|
+
test/test_config.py
|
|
43
|
+
test/test_config_validation.py
|
|
44
|
+
test/test_docker_image_available.py
|
|
45
|
+
test/test_source_nodejs.py
|
|
46
|
+
test/test_update_dockerfile.py
|
|
47
|
+
test/test_update_image_tag.py
|
|
48
|
+
test/test_update_json.py
|
|
49
|
+
test/test_update_yaml.py
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from lennybot.config import LennyBotConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestLennyBotConfig(unittest.TestCase):
|
|
8
|
+
def test_XXX(self):
|
|
9
|
+
os.environ["LB_CONTAINER_REGISTRY_ghcr.io_USERNAME"] = "USERNAME"
|
|
10
|
+
config = LennyBotConfig("test/lennybot.yaml")
|
|
11
|
+
self.assertEqual("USERNAME", config._container.registries["ghcr.io"].username)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestConfigValidation(unittest.TestCase):
|
|
7
|
+
def setUp(self) -> None:
|
|
8
|
+
with open("test/config.yaml", encoding="utf-8") as f:
|
|
9
|
+
self.config = yaml.safe_load(f)
|
|
10
|
+
|
|
11
|
+
def test_update_dockerfile_target_contains_image_name(self):
|
|
12
|
+
for app in self.config.get("applications", []):
|
|
13
|
+
for action in app.get("actions", []):
|
|
14
|
+
if action.get("type") != "update-dockerfile":
|
|
15
|
+
continue
|
|
16
|
+
image = action.get("image")
|
|
17
|
+
target = action.get("targetFile")
|
|
18
|
+
# If image is present and a target file is set, the target file should contain the image base name
|
|
19
|
+
if image is not None and target is not None:
|
|
20
|
+
image_base = image.split("/")[-1]
|
|
21
|
+
self.assertIn(
|
|
22
|
+
image_base,
|
|
23
|
+
target,
|
|
24
|
+
f"targetFile '{target}' does not contain image base '{image_base}' for app {app.get('name')}",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def test_actions_have_required_keys(self):
|
|
28
|
+
requirements = {
|
|
29
|
+
"update-dockerfile": ["image", "targetFile"],
|
|
30
|
+
"update-image-tag": ["image", "kustomizePath"],
|
|
31
|
+
"update-yaml": ["targetFile", "yamlPath"],
|
|
32
|
+
"update-json": ["targetFile", "jsonPath"],
|
|
33
|
+
}
|
|
34
|
+
for app in self.config.get("applications", []):
|
|
35
|
+
for action in app.get("actions", []):
|
|
36
|
+
a_type = action.get("type")
|
|
37
|
+
if a_type in requirements:
|
|
38
|
+
for req in requirements[a_type]:
|
|
39
|
+
self.assertIn(
|
|
40
|
+
req, action, f"Action {a_type} in app {app.get('name')} missing required key '{req}'"
|
|
41
|
+
)
|
|
@@ -39,8 +39,14 @@ class TestParseImage(unittest.TestCase):
|
|
|
39
39
|
check = DockerImageAvailableCheck("test-app", "2.7.6", "2.7.7", self.config, self.container_config)
|
|
40
40
|
self.assertRaises(Exception, check.check)
|
|
41
41
|
|
|
42
|
+
def test_given_image_path_with_5_segments(self):
|
|
43
|
+
# private.azurecr.io/public-ecr-aws/docker/library/redis:{{version}}-alpine
|
|
44
|
+
self.config._image_pattern = "private.azurecr.io/public-ecr-aws/docker/library/redis:{{version}}-alpine"
|
|
45
|
+
check = DockerImageAvailableCheck("test-app", "2.7.6", "2.7.7", self.config, self.container_config)
|
|
46
|
+
self.assertRaises(Exception, check.check)
|
|
47
|
+
|
|
42
48
|
def test_given_toolong_image_path(self):
|
|
43
|
-
self.config._image_pattern = "quay.io/argo/proj/argo/argocd:v{{version}}"
|
|
49
|
+
self.config._image_pattern = "quay.io/argo/proj/argo/argo/argo/argocd:v{{version}}"
|
|
44
50
|
check = DockerImageAvailableCheck("test-app", "2.7.6", "2.7.7", self.config, self.container_config)
|
|
45
51
|
self.assertRaises(Exception, check.check)
|
|
46
52
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from lennybot.config.config import LennyBotSourceConfig
|
|
5
|
+
from lennybot.service.source.source_nodejs import NodeJSVersionSource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestParseImage(unittest.TestCase):
|
|
9
|
+
|
|
10
|
+
def setUp(self) -> None:
|
|
11
|
+
self.config = LennyBotSourceConfig()
|
|
12
|
+
|
|
13
|
+
@patch("lennybot.service.source.source_nodejs.requests.get")
|
|
14
|
+
def test_lts_only_false(self, mock_get):
|
|
15
|
+
# returns first release when lts_only is False
|
|
16
|
+
mock_resp = MagicMock()
|
|
17
|
+
mock_resp.raise_for_status.return_value = None
|
|
18
|
+
mock_resp.json.return_value = [
|
|
19
|
+
{"version": "v25.0.0", "lts": False},
|
|
20
|
+
{"version": "v24.13.0", "lts": "Gallium"},
|
|
21
|
+
]
|
|
22
|
+
mock_get.return_value = mock_resp
|
|
23
|
+
|
|
24
|
+
self.config.lts_only = False
|
|
25
|
+
release = NodeJSVersionSource("test-node-version", self.config)
|
|
26
|
+
version = release.latest_version()
|
|
27
|
+
|
|
28
|
+
self.assertEqual(version, "25.0.0")
|
|
29
|
+
|
|
30
|
+
@patch("lennybot.service.source.source_nodejs.requests.get")
|
|
31
|
+
def test_lts_only_true(self, mock_get):
|
|
32
|
+
# returns first LTS release when lts_only is True
|
|
33
|
+
mock_resp = MagicMock()
|
|
34
|
+
mock_resp.raise_for_status.return_value = None
|
|
35
|
+
mock_resp.json.return_value = [
|
|
36
|
+
{"version": "v25.0.0", "lts": False},
|
|
37
|
+
{"version": "v24.13.0", "lts": "Gallium"},
|
|
38
|
+
]
|
|
39
|
+
mock_get.return_value = mock_resp
|
|
40
|
+
|
|
41
|
+
self.config.lts_only = True
|
|
42
|
+
lts_release = NodeJSVersionSource("test-node-version", self.config)
|
|
43
|
+
version = lts_release.latest_version()
|
|
44
|
+
|
|
45
|
+
self.assertEqual(version, "24.13.0")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from test.utils import create_sample_dockerfile, read_file
|
|
5
|
+
|
|
6
|
+
from lennybot.actions.update_dockerfile import UpdateDockerfileAction
|
|
7
|
+
from lennybot.config.config import LennyBotActionConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestUpdateDockerfileAction(unittest.TestCase):
|
|
11
|
+
def test_constructor_raises_when_target_file_missing(self):
|
|
12
|
+
config = LennyBotActionConfig()
|
|
13
|
+
setattr(config, "_target_file", None)
|
|
14
|
+
setattr(config, "_image", "nginx")
|
|
15
|
+
with self.assertRaises(Exception):
|
|
16
|
+
UpdateDockerfileAction("app", "1.0", "2.0", config)
|
|
17
|
+
|
|
18
|
+
def test_run_replaces_from_line_with_default_pattern(self):
|
|
19
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
20
|
+
dockerfile = Path(tmpdir) / "Dockerfile"
|
|
21
|
+
create_sample_dockerfile(dockerfile, ["nginx:1.2", "redis:3.0"])
|
|
22
|
+
config = LennyBotActionConfig()
|
|
23
|
+
setattr(config, "_target_file", str(dockerfile))
|
|
24
|
+
setattr(config, "_image", "nginx")
|
|
25
|
+
|
|
26
|
+
action = UpdateDockerfileAction("app", "1.2", "2.0", config)
|
|
27
|
+
action.run()
|
|
28
|
+
|
|
29
|
+
content = read_file(dockerfile)
|
|
30
|
+
self.assertIn("FROM nginx:2.0\n", content)
|
|
31
|
+
self.assertIn("FROM redis:3.0\n", content)
|
|
32
|
+
|
|
33
|
+
def test_run_respects_value_pattern(self):
|
|
34
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
35
|
+
dockerfile = Path(tmpdir) / "Dockerfile"
|
|
36
|
+
create_sample_dockerfile(dockerfile, ["redis:9.9-alpine"])
|
|
37
|
+
config = LennyBotActionConfig()
|
|
38
|
+
setattr(config, "_target_file", str(dockerfile))
|
|
39
|
+
setattr(config, "_image", "redis")
|
|
40
|
+
setattr(config, "_value_pattern", "v{{version}}-alpine")
|
|
41
|
+
|
|
42
|
+
action = UpdateDockerfileAction("app", "9.9", "3.4", config)
|
|
43
|
+
action.run()
|
|
44
|
+
|
|
45
|
+
content = read_file(dockerfile)
|
|
46
|
+
self.assertIn("FROM redis:v3.4-alpine\n", content)
|
|
47
|
+
|
|
48
|
+
def test_run_leaves_file_unchanged_when_image_not_found(self):
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
50
|
+
dockerfile = Path(tmpdir) / "Dockerfile"
|
|
51
|
+
create_sample_dockerfile(dockerfile, ["busybox:1.0"])
|
|
52
|
+
config = LennyBotActionConfig()
|
|
53
|
+
setattr(config, "_target_file", str(dockerfile))
|
|
54
|
+
setattr(config, "_image", "nginx")
|
|
55
|
+
|
|
56
|
+
action = UpdateDockerfileAction("app", "1.0", "2.0", config)
|
|
57
|
+
action.run()
|
|
58
|
+
|
|
59
|
+
content = read_file(dockerfile)
|
|
60
|
+
self.assertIn("FROM busybox:1.0\n", content)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from test.utils import create_kustomization, read_file
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from lennybot.actions.update_image_tag import UpdateImageTagAction
|
|
9
|
+
from lennybot.config.config import LennyBotActionConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestUpdateImageTagAction(unittest.TestCase):
|
|
13
|
+
def test_constructor_raises_without_kustomize_path(self):
|
|
14
|
+
config = LennyBotActionConfig()
|
|
15
|
+
setattr(config, "_image", "node")
|
|
16
|
+
setattr(config, "_kustomize_path", None)
|
|
17
|
+
with self.assertRaises(Exception):
|
|
18
|
+
UpdateImageTagAction("app", "1.0", "2.0", config)
|
|
19
|
+
|
|
20
|
+
def test_run_updates_new_tag(self):
|
|
21
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
|
+
kustomize_file = Path(tmpdir) / "kustomization.yaml"
|
|
23
|
+
create_kustomization(kustomize_file, [{"name": "node", "newTag": "old"}, {"name": "other", "newTag": "x"}])
|
|
24
|
+
|
|
25
|
+
config = LennyBotActionConfig()
|
|
26
|
+
setattr(config, "_image", "node")
|
|
27
|
+
setattr(config, "_kustomize_path", str(kustomize_file))
|
|
28
|
+
setattr(config, "_tag_pattern", "v{{version}}")
|
|
29
|
+
|
|
30
|
+
action = UpdateImageTagAction("app", "1.0", "3.2.1", config)
|
|
31
|
+
action.run()
|
|
32
|
+
|
|
33
|
+
content = yaml.safe_load(read_file(kustomize_file))
|
|
34
|
+
found = [img for img in content["images"] if img["name"] == "node"][0]
|
|
35
|
+
self.assertEqual(found["newTag"], "v3.2.1")
|
|
36
|
+
|
|
37
|
+
def test_run_raises_when_image_not_found(self):
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
39
|
+
kustomize_file = Path(tmpdir) / "kustomization.yaml"
|
|
40
|
+
create_kustomization(kustomize_file, [{"name": "other", "newTag": "x"}])
|
|
41
|
+
|
|
42
|
+
config = LennyBotActionConfig()
|
|
43
|
+
setattr(config, "_image", "node")
|
|
44
|
+
setattr(config, "_kustomize_path", str(kustomize_file))
|
|
45
|
+
|
|
46
|
+
action = UpdateImageTagAction("app", "1.0", "3.2.1", config)
|
|
47
|
+
with self.assertRaises(Exception):
|
|
48
|
+
action.run()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from test.utils import create_json_file, read_file
|
|
6
|
+
|
|
7
|
+
from lennybot.actions.update_json import UpdateJsonAction
|
|
8
|
+
from lennybot.config.config import LennyBotActionConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestUpdateJsonAction(unittest.TestCase):
|
|
12
|
+
def test_constructor_raises_when_target_file_missing(self):
|
|
13
|
+
config = LennyBotActionConfig()
|
|
14
|
+
setattr(config, "_target_file", None)
|
|
15
|
+
setattr(config, "_json_path", "$.a.b")
|
|
16
|
+
with self.assertRaises(Exception):
|
|
17
|
+
UpdateJsonAction("app", "1.0", "2.0", config)
|
|
18
|
+
|
|
19
|
+
def test_constructor_raises_when_json_path_missing(self):
|
|
20
|
+
config = LennyBotActionConfig()
|
|
21
|
+
setattr(config, "_target_file", "somefile")
|
|
22
|
+
setattr(config, "_json_path", None)
|
|
23
|
+
with self.assertRaises(Exception):
|
|
24
|
+
UpdateJsonAction("app", "1.0", "2.0", config)
|
|
25
|
+
|
|
26
|
+
def test_run_updates_json_value(self):
|
|
27
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
28
|
+
json_file = Path(tmpdir) / "file.json"
|
|
29
|
+
create_json_file(json_file, {"a": {"b": "old"}})
|
|
30
|
+
|
|
31
|
+
config = LennyBotActionConfig()
|
|
32
|
+
setattr(config, "_target_file", str(json_file))
|
|
33
|
+
setattr(config, "_json_path", "$.a.b")
|
|
34
|
+
setattr(config, "_value_pattern", "v{{version}}")
|
|
35
|
+
|
|
36
|
+
action = UpdateJsonAction("app", "1.0", "3.3.3", config)
|
|
37
|
+
action.run()
|
|
38
|
+
|
|
39
|
+
content = json.loads(read_file(json_file))
|
|
40
|
+
self.assertEqual(content["a"]["b"], "v3.3.3")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from test.utils import create_yaml_file, read_file
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from lennybot.actions.update_yaml import UpdateYamlAction
|
|
9
|
+
from lennybot.config.config import LennyBotActionConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestUpdateYamlAction(unittest.TestCase):
|
|
13
|
+
def test_constructor_raises_when_target_file_missing(self):
|
|
14
|
+
config = LennyBotActionConfig()
|
|
15
|
+
setattr(config, "_target_file", None)
|
|
16
|
+
setattr(config, "_yaml_path", "spec.imageTag")
|
|
17
|
+
with self.assertRaises(Exception):
|
|
18
|
+
UpdateYamlAction("app", "1.0", "2.0", config)
|
|
19
|
+
|
|
20
|
+
def test_constructor_raises_when_yaml_path_missing(self):
|
|
21
|
+
config = LennyBotActionConfig()
|
|
22
|
+
setattr(config, "_target_file", "somefile")
|
|
23
|
+
setattr(config, "_yaml_path", None)
|
|
24
|
+
with self.assertRaises(Exception):
|
|
25
|
+
UpdateYamlAction("app", "1.0", "2.0", config)
|
|
26
|
+
|
|
27
|
+
def test_run_sets_value_in_yaml(self):
|
|
28
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
29
|
+
yaml_file = Path(tmpdir) / "file.yaml"
|
|
30
|
+
create_yaml_file(yaml_file, {"spec": {"imageTag": "old"}})
|
|
31
|
+
|
|
32
|
+
config = LennyBotActionConfig()
|
|
33
|
+
setattr(config, "_target_file", str(yaml_file))
|
|
34
|
+
setattr(config, "_yaml_path", "spec.imageTag")
|
|
35
|
+
setattr(config, "_value_pattern", "v{{version}}")
|
|
36
|
+
|
|
37
|
+
action = UpdateYamlAction("app", "1.0", "3.3.3", config)
|
|
38
|
+
action.run()
|
|
39
|
+
|
|
40
|
+
content = yaml.safe_load(read_file(yaml_file))
|
|
41
|
+
# Expect value to be replaced; YAML path syntax may vary depending on yamlpath library
|
|
42
|
+
self.assertEqual(content["spec"]["imageTag"], "v3.3.3")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.33
|
lennybot-1.0.26/version.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.26
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|