Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to Express [*****] #7877

Closed
wants to merge 25 commits into from
Closed

Migrate to Express [*****] #7877

wants to merge 25 commits into from

Conversation

paulmelnikow
Copy link
Member

@paulmelnikow paulmelnikow commented Apr 22, 2022

There's surely a bit of cleanup to do here, but thought it worth getting some 👀 on this.

I'd welcome help updating and improving the documentation, though that could also be done in a follow-on PR.

An area I've not yet addressed is query parameters. Scoutcamp used to turn the params into numbers, which I imagine Express does not do. It means we probably can simplify places where we've got Joi.alternatives().try(Joi.string(), Joi.number()) in the query param schemas.

There are surely bugs in this, and edge cases. I removed a few tests in legacy-request-handler.spec.js which we may want to rewrite elsewhere.

We should test this out (maybe even on Fly), in particular things like suggest and token acceptance.

I would be surprised if performance were a problem, though we may want to experiment with this under load to make sure.

Closes #3328
Closes #3368
Closes #4948

@shields-ci
Copy link

shields-ci commented Apr 22, 2022

Warnings
⚠️

📚 Remember to ensure any changes to config.private in services/github/auth/acceptor.spec.js are reflected in the server secrets documentation

⚠️

📚 Remember to ensure any changes to config.private in services/github/auth/acceptor.js are reflected in the server secrets documentation

⚠️

📚 Remember to ensure any changes to config.private in services/github/github-constellation.js are reflected in the server secrets documentation

Messages
📖

Thanks for contributing to our documentation. We ❤️ our documentarians!

📖 ✨ Thanks for your contribution to Shields, @paulmelnikow!

Generated by 🚫 dangerJS against fc68b88

@paulmelnikow paulmelnikow added the core Server, BaseService, GitHub auth label Apr 22, 2022
@lgtm-com
Copy link

lgtm-com bot commented Apr 22, 2022

This pull request introduces 1 alert when merging 78b886c into 560d267 - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

@@ -24,22 +23,6 @@ module.exports = function makeBadge({
label = `${label}`.trim()
message = `${message}`.trim()

// This ought to be the responsibility of the server, not `makeBadge`.
if (format === 'json') {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've argued before that this functionality doesn't belong in the badge library as the particular JSON schema we generate here (with name and value) is a legacy quirk of shields.io, and not something we particularly want users to replace elsewhere. For the moment I went ahead and moved this functionality to the server. It's now handled by makeJsonBadge() which is invoked from a conditional in the request handler in base.js.

If we keep this we'll probably want to add normalizeColor to the package's public interface, or alternatively a normalizeBadgeData() function, and then we could start dogfooding the public makeBadge().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read over the code for this bit at all yet. I agree with the objective. Has anything significant has changed since #4980 in terms of the implementation, specifically #4980 (comment) ? I will have a look through it. If a slightly different configuration of not eating our own dogfood is necessary to get off scoutcamp, this shouldn't be a blocker but until we do #4947 and #4949 I suspect all we can really do is move from one undesirable implementation to another. I do accept #4947 and #4949 have been open for 2 years and none of us are actively working on them.
As a note, we may want to consider documenting it as a non-BC change for badge-maker, even though format='json' is undocumented. The next badge-maker will be a major release anyway due to dropping node 10.

describe('The badge generator', function () {
describe('color test', function () {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly, but not completely, these duplicate tests in color.spec.js. We may want to go through them line by line and see if there are any here we should port over. With the format parameter removed, this behavior can no longer be tested here.

})

it('should replace undefined svg badge style with "flat"', function () {
const jsonBadgeWithUnknownStyle = makeBadge({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline

})

describe('JSON', function () {
it('should produce the expected JSON', function () {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove JSON tests

style: 'social',
})
)
.to.include('""')
.to.include('></text>')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SVG equivalent

@@ -112,7 +110,9 @@ describe('The server', function () {
`${baseUrl}:fruit-apple-green.json`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('application/json')
expect(headers['content-type']).to.equal(
'application/json; charset=utf-8'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Charset added by Express

expect(body)
.to.satisfy(isSvg)
.and.to.include('410')
.and.to.include('jpg no longer available')
})

it('should return cors header for the request', async function () {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates a test a bit higher up in this context

[`core/server/server.js`][core/server/server], is based on [Express][].
It creates an http server, sets up helpers for token persistence and
monitoring. Then it loads all the services, injecting dependencies as it
asks each one to register its route with the Express app.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a rough effort to update this, though it may make sense for someone to take a pass through later. This whole process is much more direct than before.

@@ -25,7 +25,6 @@
"@fontsource/lekton": "^4.5.6",
"@renovate/pep440": "^1.0.0",
"@sentry/node": "^6.19.6",
"@shields_io/camp": "^18.1.1",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💁🏼 🎩

return end('GitHub OAuth authentication failed to provide a code.')
}
app.post('/github-auth/done', multer().none(), async (req, res) => {
const code = (req.body ?? {}).code
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hasn't been tested yet

@paulmelnikow paulmelnikow marked this pull request as ready for review April 22, 2022 17:27
@paulmelnikow paulmelnikow changed the title Migrate to Express Migrate to Express [*****] Apr 22, 2022
@paulmelnikow
Copy link
Member Author

In the first service test run there are some interesting failures.

One issue is that the JSON badge no longer strips leading and trailing whitespace from label and message. That could be added to makeJsonBadge… or maybe we should just fix the services.

})
})

context('No named params are declared', function () {
const { regex, captureNames } = prepareRoute({
base: 'foo',
format: '(?:[^/]+)',
format: '(?:[^/]+?)',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To correctly detect .svg and .json as intended, this regex needs to be lazy.

function setRoutes(allowedOrigin, githubApiProvider, server) {
server.ajax.on('suggest/v1', (data, end, ask) => {
function setRoutes(allowedOrigin, githubApiProvider, app) {
app.get('/[$]suggest/v1', (req, res) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Scoutcamp AJAX routes implicitly started with $. For some reason the dollar needs to be escaped for Express.

if (origin) {
let host
try {
host = new URL(origin).hostname
} catch (e) {
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
end({ err: 'Disallowed' })
res.setHeader('Access-Control-Allow-Origin', 'null')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I read this I wonder if it works, especially since we're setting Access-Control-Allow-Origin: * in server.js.

@chris48s
Copy link
Member

Thanks for taking this on. This aspect of the codebase and the reliance on ScoutCamp is one of the biggest pain points of this codebase so it is really good to be making some progress on this :)

I haven't done a full review of this, but I have checked it out locally and skimmed the diff.

The main issue I hit is if I run npm start on a checkout of this branch, it throws

Failed to load plugin 'graphql' declared in 'BaseConfig': Cannot find module 'ws'

and won't start the frontend, so I've only been able to test the badge server. Any idea what is happening there?


There were a few bits I wanted to specifically check as possible edge cases:

  • Badges with regex routes e.g:

    • services/travis/travis-build.service.js
    • services/wercker/wercker.service.js
    • services/requires/requires.service.js
    • services/static-badge/static-badge.service.js
      Those all seem to work fine 👍
  • .png redirect to raster proxy. Also works 👍

  • /endpoint route conflict - not tested yet as I couldn't start the frontend.


I'll try and have a more detailed look at this next week.

@@ -306,87 +297,78 @@ class Server {

// See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
requireCloudflare() {
// Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
// by Express but not Scoutcamp.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll definitely need to add back the special case for fly.io before we can merge/deploy this. It definitely doesn't work with the default setup.
On the heroku case, I'm slightly in 2 minds whether to just bin that on the basis we're not using it or keep it on the basis people who are self-hosting might be relying on it. In any case, we added this in #5712 because we deployed to Heroku using the default behaviour of cloudflareMiddleware() and it didn't work.

Comment on lines +519 to +527
app.use(
express.static(this.config.public.documentRoot, {
// Since express's `maxAge` parameter sets `Cache-Control: public`, set
// the headers manually insetad.
cacheControl: false,
setHeaders: res =>
res.setHeader('Cache-Control', 'max-age=300, s-maxage=300'),
})
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this replacing staticMaxAge?
badges/sc@dc89a3a

Comment on lines -103 to -115
const serverResponsive = setTimeout(() => {
serverUnresponsive = true
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
const badgeData = coalesceBadge(
filteredQueryParams,
{ label: 'vendor', message: 'unresponsive' },
{}
)
const svg = makeBadge(badgeData)
const extension = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
setCacheHeadersOnResponse(ask.res)
makeSend(extension, ask.res, end)(svg)
}, 25000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we've just totally dropped this in favour of express this.server.setTimeout() ?

@@ -24,22 +23,6 @@ module.exports = function makeBadge({
label = `${label}`.trim()
message = `${message}`.trim()

// This ought to be the responsibility of the server, not `makeBadge`.
if (format === 'json') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read over the code for this bit at all yet. I agree with the objective. Has anything significant has changed since #4980 in terms of the implementation, specifically #4980 (comment) ? I will have a look through it. If a slightly different configuration of not eating our own dogfood is necessary to get off scoutcamp, this shouldn't be a blocker but until we do #4947 and #4949 I suspect all we can really do is move from one undesirable implementation to another. I do accept #4947 and #4949 have been open for 2 years and none of us are actively working on them.
As a note, we may want to consider documenting it as a non-BC change for badge-maker, even though format='json' is undocumented. The next badge-maker will be a major release anyway due to dropping node 10.

Copy link
Member

@chris48s chris48s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did another slightly more detailed pass over this and left some more comments

next()
}
},
this.makeExpressHandler(serviceContext, serviceConfig)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is dropping the early return in the 304 case intended here?
It looks like we are sticking with early return in core/base-service/redirector.js

)
}

it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably do want to replicate at lest some of these tests, particularly for the cache headers. What level do you think would make most sense to test this at with the revised architecture?

Comment on lines -91 to +92
let urlSuffix = ask.uri.search || ''
let urlSuffix = url.parse(req.url).search ?? '' // eslint-disable-line node/no-deprecated-api
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this with let urlSuffix = new url.URL(req.url).search instead of introducing a deprecated call here? I think that should be the same.

// TODO It would be nice if this were 404 or 410.
expect(statusCode).to.equal(200)
expect(statusCode).to.equal(410)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


await new Promise(resolve => camp.on('listening', () => resolve()))
this.server.setTimeout(this.config.public.requestTimeoutSeconds * 1000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. If we're going to move to just dropping the connection on timeout, I think we can just get rid of the requestTimeoutMaxAgeSeconds setting as it won't do anything
  2. requestTimeoutSeconds is an optional setting (for self hosting users). We should handle the case where it is not set.

@paulmelnikow
Copy link
Member Author

Thanks for the comments. Will nudge this forward when I can!

@PyvesB
Copy link
Member

PyvesB commented Dec 31, 2023

Given that there hasn't been any activity for a while, I'm going to close this pull request. Feel free to ping us or reopen it if you get a chance to look into it again. 😉

If others think that repository level projects would be an option and want to pick this up, you can simply fetch the commits by running git fetch origin pull/7877/head:pr-7877. Change the implementation to target repository level projects and open a new pull request with a co-authored trailer included in the commit message to give attribution to the original author.

@PyvesB PyvesB closed this Dec 31, 2023
@chris48s
Copy link
Member

It is still my ambition to pick this up and finish it at some point, but agree that there is limited value in keeping the PR hanging around

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Server, BaseService, GitHub auth
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move JSON output out of badge-maker and into server Modernizing the request code Modernizing the server
4 participants