diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 3795a2af1de81..3afe51a5450b5 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -50,6 +50,8 @@ public: authorizedOrigins: 'NEXUS_ORIGINS' npm: authorizedOrigins: 'NPM_ORIGINS' + obs: + authorizedOrigins: 'OBS_ORIGINS' sonar: authorizedOrigins: 'SONAR_ORIGINS' teamcity: @@ -87,6 +89,8 @@ private: nexus_user: 'NEXUS_USER' nexus_pass: 'NEXUS_PASS' npm_token: 'NPM_TOKEN' + obs_user: 'OBS_USER' + obs_pass: 'OBS_PASS' redis_url: 'REDIS_URL' sentry_dsn: 'SENTRY_DSN' shields_secret: 'SHIELDS_SECRET' diff --git a/config/default.yml b/config/default.yml index 973de381324a3..41dc14e5f319c 100644 --- a/config/default.yml +++ b/config/default.yml @@ -22,6 +22,8 @@ public: debug: enabled: false intervalSeconds: 200 + obs: + authorizedOrigins: 'https://api.opensuse.org' weblate: authorizedOrigins: 'https://hosted.weblate.org' trace: false diff --git a/config/local.template.yml b/config/local.template.yml index af36abf238756..ae1c51c619ebf 100644 --- a/config/local.template.yml +++ b/config/local.template.yml @@ -6,6 +6,8 @@ private: # preferable for self hosting. gh_token: '...' gitlab_token: '...' + obs_user: '...' + obs_pass: '...' twitch_client_id: '...' twitch_client_secret: '...' weblate_api_key: '...' diff --git a/core/server/server.js b/core/server/server.js index 9a4cacf7b938d..8b5b166b35719 100644 --- a/core/server/server.js +++ b/core/server/server.js @@ -134,6 +134,7 @@ const publicConfigSchema = Joi.object({ }).default({ authorizedOrigins: [] }), nexus: defaultService, npm: defaultService, + obs: defaultService, sonar: defaultService, teamcity: defaultService, weblate: defaultService, @@ -172,6 +173,8 @@ const privateConfigSchema = Joi.object({ nexus_user: Joi.string(), nexus_pass: Joi.string(), npm_token: Joi.string(), + obs_user: Joi.string(), + obs_pass: Joi.string(), redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }), sentry_dsn: Joi.string(), shields_secret: Joi.string(), diff --git a/doc/server-secrets.md b/doc/server-secrets.md index 9f089ef0e4714..02b7ad1534c35 100644 --- a/doc/server-secrets.md +++ b/doc/server-secrets.md @@ -193,6 +193,21 @@ installation access to private npm packages [npm token]: https://docs.npmjs.com/getting-started/working_with_tokens +## Open Build Service + +- `OBS_USER` (yml: `private.obs_user`) +- `OBS_PASS` (yml: `private.obs_user`) + +Only authenticated users are allowed to access the Open Build Service API. +Authentication is done by sending a Basic HTTP Authorisation header. A user +account for the [reference instance](https://build.opensuse.org) is a SUSE +IdP account, which can be created [here](https://idp-portal.suse.com/univention/self-service/#page=createaccount). + +While OBS supports [API tokens](https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.authorization.token.html#id-1.5.10.16.4), +they can only be scoped to execute specific actions on a POST request. This +means however, that an actual account is required to read the build status +of a package. + ### SymfonyInsight (formerly Sensiolabs) - `SL_INSIGHT_USER_UUID` (yml: `private.sl_insight_userUuid`) diff --git a/services/build-status.js b/services/build-status.js index 0a143bcdde8e0..a658ff411e6ad 100644 --- a/services/build-status.js +++ b/services/build-status.js @@ -12,6 +12,7 @@ const greenStatuses = [ const orangeStatuses = ['partially succeeded', 'unstable', 'timeout'] const redStatuses = [ + 'broken', 'error', 'errored', 'failed', diff --git a/services/build-status.spec.js b/services/build-status.spec.js index 4708f43522a7c..b135504bcbe78 100644 --- a/services/build-status.spec.js +++ b/services/build-status.spec.js @@ -53,6 +53,7 @@ test(renderBuildStatusBadge, () => { test(renderBuildStatusBadge, () => { forCases([ + given({ status: 'broken' }), given({ status: 'error' }), given({ status: 'errored' }), given({ status: 'failed' }), diff --git a/services/obs/obs-build-status.js b/services/obs/obs-build-status.js new file mode 100644 index 0000000000000..0be8d3fd2553f --- /dev/null +++ b/services/obs/obs-build-status.js @@ -0,0 +1,34 @@ +import Joi from 'joi' +import { + isBuildStatus as gIsBuildStatus, + renderBuildStatusBadge as gRenderBuildStatusBadge, +} from '../build-status.js' + +const localStatuses = { + blocked: 'inactive', + disabled: 'inactive', + finished: 'orange', + 'scheduled-warning': 'orange', + signing: 'orange', + unknown: 'inactive', + unresolvable: 'red', +} + +const isBuildStatus = Joi.alternatives().try( + gIsBuildStatus, + Joi.equal(...Object.keys(localStatuses)) +) + +function renderBuildStatusBadge({ repository, status }) { + const color = localStatuses[status] + if (color) { + return { + message: status.toLowerCase(), + color, + } + } else { + return gRenderBuildStatusBadge({ status: status.toLowerCase() }) + } +} + +export { isBuildStatus, renderBuildStatusBadge } diff --git a/services/obs/obs.service.js b/services/obs/obs.service.js new file mode 100644 index 0000000000000..d6c6304eeeb17 --- /dev/null +++ b/services/obs/obs.service.js @@ -0,0 +1,81 @@ +import Joi from 'joi' +import { BaseXmlService } from '../index.js' +import { optionalUrl } from '../validators.js' +import { isBuildStatus, renderBuildStatusBadge } from './obs-build-status.js' + +const schema = Joi.object({ + status: Joi.object({ + '@_code': isBuildStatus, + }).required(), +}).required() + +export default class ObsService extends BaseXmlService { + static category = 'build' + static route = { + base: 'obs', + pattern: ':project/:packageName/:repository/:arch', + queryParamSchema: Joi.object({ + instance: optionalUrl, + }).required(), + } + + static auth = { + userKey: 'obs_user', + passKey: 'obs_pass', + serviceKey: 'obs', + isRequired: true, + } + + static examples = [ + { + title: 'OBS package build status', + namedParams: { + project: 'openSUSE:Tools', + packageName: 'osc', + repository: 'Debian_11', + arch: 'x86_64', + }, + queryParams: { instance: 'https://api.opensuse.org' }, + staticPreview: this.render({ + repository: 'Debian_11', + status: 'succeeded', + }), + keywords: ['open build service'], + }, + ] + + static defaultBadgeData = { label: 'build' } + + static render({ repository, status }) { + return renderBuildStatusBadge({ repository, status }) + } + + async fetch({ instance, project, packageName, repository, arch }) { + return this._requestXml( + this.authHelper.withBasicAuth({ + schema, + url: `${instance}/build/${project}/${repository}/${arch}/${packageName}/_status`, + parserOptions: { + ignoreAttributes: false, + }, + }) + ) + } + + async handle( + { project, packageName, repository, arch }, + { instance = 'https://api.opensuse.org' } + ) { + const resp = await this.fetch({ + instance, + project, + packageName, + repository, + arch, + }) + return this.constructor.render({ + repository, + status: resp.status['@_code'], + }) + } +} diff --git a/services/obs/obs.tester.js b/services/obs/obs.tester.js new file mode 100644 index 0000000000000..f05714f781212 --- /dev/null +++ b/services/obs/obs.tester.js @@ -0,0 +1,25 @@ +import { ServiceTester } from '../tester.js' +import { noToken } from '../test-helpers.js' +import ObsService from './obs.service.js' +import { isBuildStatus } from './obs-build-status.js' + +export const t = new ServiceTester({ + id: 'obs', + title: 'openSUSE Open Build Service', +}) + +t.create('status (valid)') + .skipWhen(noToken(ObsService)) + .get('/openSUSE:Factory/aaa_base/standard/x86_64.json?label=standard') + .expectBadge({ + label: 'standard', + message: isBuildStatus, + }) + +t.create('status (invalid)') + .skipWhen(noToken(ObsService)) + .get('/home:sp1rit/this_package_will_never_exist/repo/arch.json') + .expectBadge({ + label: 'build', + message: 'not found', + })