Skip to content

Commit e7a6ec4

Browse files
authored
Merge pull request #2491 from MichaelRitzert/feature/gitlabupdate
Support automatic updates from gitlab package registry
2 parents 719615f + 96557cc commit e7a6ec4

File tree

10 files changed

+511
-73
lines changed

10 files changed

+511
-73
lines changed

app/update/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,16 @@
3434
<artifactId>core-ui</artifactId>
3535
<version>4.7.2-SNAPSHOT</version>
3636
</dependency>
37+
<dependency>
38+
<groupId>javax.jms</groupId>
39+
<artifactId>javax.jms-api</artifactId>
40+
<version>2.0.1</version>
41+
</dependency>
42+
<dependency>
43+
<groupId>javax.json</groupId>
44+
<artifactId>javax.json-api</artifactId>
45+
<version>1.1.4</version>
46+
</dependency>
47+
3748
</dependencies>
3849
</project>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.phoebus.applications.update;
2+
3+
import java.io.File;
4+
import java.time.Instant;
5+
6+
import org.phoebus.framework.jobs.JobMonitor;
7+
8+
/**
9+
* Dummy class returned from the factory instead of null.
10+
*/
11+
public class DummyUpdate implements UpdateProvider
12+
{
13+
@Override
14+
public boolean isEnabled()
15+
{
16+
return false;
17+
}
18+
19+
@Override
20+
public Instant checkForUpdate(JobMonitor monitor) throws Exception
21+
{
22+
return null;
23+
}
24+
25+
@Override
26+
public void downloadAndUpdate(JobMonitor monitor, File install_location)
27+
throws Exception
28+
{
29+
}
30+
31+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package org.phoebus.applications.update;
2+
3+
import java.io.BufferedReader;
4+
import java.io.InputStream;
5+
import java.io.InputStreamReader;
6+
import java.net.URI;
7+
import java.net.http.HttpClient;
8+
import java.net.http.HttpRequest;
9+
import java.net.http.HttpResponse.BodyHandlers;
10+
import java.nio.charset.StandardCharsets;
11+
import java.time.Instant;
12+
import java.time.format.DateTimeFormatter;
13+
import java.util.logging.Logger;
14+
15+
import javax.json.Json;
16+
import javax.json.JsonObject;
17+
18+
import org.phoebus.framework.jobs.JobMonitor;
19+
import org.phoebus.framework.preferences.AnnotatedPreferences;
20+
import org.phoebus.framework.preferences.Preference;
21+
22+
/**
23+
* Pull updates from the gitlab package registry.
24+
*
25+
* <p>
26+
* The timestamp of the available update is determined from the timestamp of the
27+
* commit that triggered the pipeline. The version information in the package
28+
* repository is only used for display purposes.
29+
*
30+
* <p>
31+
* To initialize the package version, use
32+
* <code>echo org.phoebus.applications.update/current_version=$CI_COMMIT_TIMESTAMP >> phoebus-product/settings.ini</code>
33+
* in the build pipeline, then package <code>settings.ini</code>.
34+
*
35+
* @author Michael Ritzert
36+
*
37+
*/
38+
public class GitlabUpdate extends Update implements UpdateProvider
39+
{
40+
public static final Logger logger = Logger
41+
.getLogger(GitlabUpdate.class.getPackageName());
42+
43+
/**
44+
* The path to the "V4 API".
45+
*
46+
* Typically https://HOST/api/v4
47+
*/
48+
@Preference
49+
public static String gitlab_api_url;
50+
/** The numeric ID of the gitlab project. */
51+
@Preference
52+
public static int gitlab_project_id;
53+
/**
54+
* The package name used in the registry.
55+
*
56+
* Defaults to "phoebus-$(arch)".
57+
*/
58+
@Preference
59+
public static String gitlab_package_name;
60+
/**
61+
* Access token for the project's API and registry. Required if access to
62+
* the gitlab project is not public.
63+
*/
64+
@Preference
65+
public static String gitlab_token;
66+
67+
// filled step by step with the information of the latest update
68+
private String latest_version;
69+
private int latest_id;
70+
private long file_size;
71+
private String file_name;
72+
private String latest_commit;
73+
74+
static
75+
{
76+
AnnotatedPreferences.initialize(GitlabUpdate.class,
77+
"/update_preferences.properties");
78+
79+
gitlab_package_name = replace_arch(gitlab_package_name);
80+
}
81+
82+
@Override
83+
public boolean isEnabled()
84+
{
85+
return !gitlab_api_url.isEmpty() && (gitlab_project_id != 0)
86+
&& !gitlab_package_name.isEmpty();
87+
}
88+
89+
@Override
90+
protected Long getDownloadSize()
91+
{
92+
return file_size;
93+
}
94+
95+
@Override
96+
protected InputStream getDownloadStream() throws Exception
97+
{
98+
final var endpoint = String.format(
99+
"projects/%d/packages/generic/%s/%s/%s", gitlab_project_id,
100+
gitlab_package_name, latest_version, file_name);
101+
return makeApiCall(endpoint);
102+
}
103+
104+
/**
105+
* Identify the latest commit for the package we want.
106+
*
107+
* Fills in latest_commit, file_size, and file_name.
108+
*
109+
* @throws Exception
110+
*/
111+
private void getLatestCommit() throws Exception
112+
{
113+
monitor.updateTaskName("Finding latest commit.");
114+
final var endpoint = String.format(
115+
"projects/%d/packages/%d/package_files", gitlab_project_id,
116+
latest_id);
117+
try (final var body = makeApiCall(endpoint);
118+
final var reader = Json
119+
.createReader(new InputStreamReader(body)))
120+
{
121+
final var files = reader.readArray();
122+
// within the package, get the commit id for the last pipeline run.
123+
final var latest_file = files.stream().map(v -> (JsonObject) v)
124+
.sorted((a, b) -> {
125+
return -a.getString("created_at")
126+
.compareTo(b.getString("created_at"));
127+
}).findFirst();
128+
if (!latest_file.isPresent())
129+
{
130+
throw new RuntimeException("No commit found."); //$NON-NLS-1$
131+
}
132+
latest_commit = latest_file.get().getJsonArray("pipelines")
133+
.getJsonObject(0).getString("sha");
134+
logger.fine("Latest commit ID: " + latest_commit);
135+
file_size = latest_file.get().getInt("size");
136+
file_name = latest_file.get().getString("file_name");
137+
logger.finer(file_name + " / " + file_size);
138+
}
139+
}
140+
141+
/**
142+
* Find the latest package with the configured name.
143+
*
144+
* Fills in latest_version and latest_id.
145+
*
146+
* @throws Exception
147+
*/
148+
private void getLatestPackage() throws Exception
149+
{
150+
monitor.updateTaskName("Finding latest package.");
151+
final var endpoint = String.format(
152+
"projects/%d/packages?package_name=%s&order_by=version",
153+
gitlab_project_id, gitlab_package_name);
154+
try (final var body = makeApiCall(endpoint);
155+
final var reader = Json
156+
.createReader(new InputStreamReader(body)))
157+
{
158+
final var packages = reader.readArray();
159+
// we assume the first package in the list is the latest
160+
final var latest_package = packages.getJsonObject(0);
161+
latest_version = latest_package.getString("version");
162+
logger.info("Latest version: " + latest_version);
163+
latest_id = latest_package.getInt("id");
164+
}
165+
}
166+
167+
/**
168+
* Get the timestamp of the given commit.
169+
*
170+
* @param commit_id
171+
* The full SHA hash of the commit.
172+
* @return The timestamp of the commit.
173+
* @throws Exception
174+
*/
175+
private Instant getTimestampOfCommit(final String commit_id)
176+
throws Exception
177+
{
178+
monitor.updateTaskName("Getting commit details.");
179+
final var endpoint = String.format("projects/%d/repository/commits/%s",
180+
gitlab_project_id, commit_id);
181+
try (final var body = makeApiCall(endpoint);
182+
final var reader = Json
183+
.createReader(new InputStreamReader(body)))
184+
{
185+
final var info = reader.readObject();
186+
final var timestamp = Instant.parse(info.getString("created_at"));
187+
logger.fine("Latest release time: " + timestamp);
188+
return timestamp;
189+
}
190+
}
191+
192+
@Override
193+
protected Instant getVersion() throws Exception
194+
{
195+
getLatestPackage();
196+
getLatestCommit();
197+
update_version = getTimestampOfCommit(latest_commit);
198+
return update_version;
199+
}
200+
201+
/**
202+
* Execute a GET call to the gitlab API.
203+
*
204+
* @param endpoint
205+
* The URL to access. The part up to /v4/ is automatically
206+
* prepended.
207+
* @return An InputStream to receive the result.
208+
* @throws Exception
209+
*/
210+
private InputStream makeApiCall(final String endpoint) throws Exception
211+
{
212+
final var uri = URI
213+
.create(String.format("%s/%s", gitlab_api_url, endpoint));
214+
final var c = HttpClient.newHttpClient();
215+
// perform the HTTP request
216+
var builder = HttpRequest.newBuilder().uri(uri).GET();
217+
if (!gitlab_token.isEmpty())
218+
{
219+
builder = builder.header("PRIVATE-TOKEN", gitlab_token);
220+
}
221+
final var request = builder.build();
222+
final var response = c.send(request, BodyHandlers.ofInputStream());
223+
if (200 != response.statusCode())
224+
{
225+
try (final var body = response.body())
226+
{
227+
// on error, log the response body
228+
new BufferedReader(
229+
new InputStreamReader(body, StandardCharsets.UTF_8))
230+
.lines().forEach(logger::warning);
231+
}
232+
throw new RuntimeException("API call failed."); //$NON-NLS-1$
233+
}
234+
return response.body();
235+
}
236+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.phoebus.applications.update;
2+
3+
import java.io.File;
4+
import java.io.InputStream;
5+
import java.net.MalformedURLException;
6+
import java.net.URL;
7+
import java.time.Instant;
8+
9+
import org.phoebus.framework.jobs.JobMonitor;
10+
import org.phoebus.framework.preferences.AnnotatedPreferences;
11+
import org.phoebus.framework.preferences.Preference;
12+
import org.phoebus.framework.util.ResourceParser;
13+
14+
public class URLUpdate extends Update implements UpdateProvider
15+
{
16+
@Override
17+
public boolean isEnabled()
18+
{
19+
return !update_url.isEmpty();
20+
}
21+
22+
/** Update URL, or empty if not set */
23+
@Preference public static String update_url;
24+
25+
final URL distribution_url;
26+
27+
static {
28+
AnnotatedPreferences.initialize(URLUpdate.class, "/update_preferences.properties");
29+
30+
update_url = replace_arch(update_url);
31+
}
32+
33+
public URLUpdate()
34+
{
35+
URL tmp = null;
36+
try
37+
{
38+
tmp = new URL(update_url);
39+
}
40+
catch (MalformedURLException e)
41+
{
42+
// handled later when distribution_url is null
43+
}
44+
distribution_url = tmp;
45+
}
46+
47+
@Override
48+
public Instant checkForUpdate(final JobMonitor monitor) throws Exception
49+
{
50+
// complain, if it update_url defined, but could not be parsed.
51+
if (null == distribution_url)
52+
throw new RuntimeException("Invalid distribution_url.");
53+
return super.checkForUpdate(monitor);
54+
}
55+
56+
@Override
57+
public void downloadAndUpdate(final JobMonitor monitor, final File install_location) throws Exception
58+
{
59+
this.monitor = monitor;
60+
// shortcut for file: URLs: No need to download first
61+
if (update_url.startsWith("file:")) {
62+
monitor.beginTask("Update", 100);
63+
update(install_location, new File(update_url.substring(5)));
64+
adjustCurrentVersion();
65+
}
66+
else
67+
{
68+
super.downloadAndUpdate(monitor, install_location);
69+
}
70+
}
71+
72+
@Override
73+
protected Instant getVersion() throws Exception
74+
{
75+
logger.info("Checking " + update_url);
76+
monitor.updateTaskName("Querying latest version.");
77+
if (distribution_url.getProtocol().equals("https"))
78+
ResourceParser.trustAnybody();
79+
return Instant.ofEpochMilli(
80+
distribution_url.openConnection().getLastModified());
81+
}
82+
83+
@Override
84+
protected Long getDownloadSize() throws Exception
85+
{
86+
return distribution_url.openConnection().getContentLengthLong();
87+
}
88+
89+
@Override
90+
protected InputStream getDownloadStream() throws Exception
91+
{
92+
return distribution_url.openStream();
93+
}
94+
}

0 commit comments

Comments
 (0)