Published unlisted
Edited
Oct 27, 2021
Insert cell
Insert cell
Insert cell
deploy(
"update_cron",
async (req, res, ctx) => {
try {
const serviceAccount = JSON.parse(
ctx.secrets["endpointservices_secretadmin_service_account_key"]
);
const token = await getAccessTokenFromServiceAccount(serviceAccount);
await signinWithAccessToken(token);

const { subdomain, notebook, expected } = JSON.parse(req.body);
const fullname = `${subdomain}_${notebook}_${expected.name}`;

// We can only safely sync with what has been published
// So we read configuration is deployed rather than rely on what has been sent in the request
// Its still useful for the deployer to send what they want to deploy as we can see if they have
// forgotten to publish the Notebook.
const actualResponse = await fetchWithTimeout(
`https://webcode.run/observablehq.com/@${subdomain}/${notebook};public%20cron%20config%20${expected.name}`
);
if (actualResponse.status !== 200) {
return res.status(404).json({
message: "Cannot read cron config, did you forget to publish?"
});
}
const actual = await actualResponse.json();

// Check the published version matches what the caller is expecting
if (!_.isEqual(actual, expected)) {
return res.status(400).json({
message: `You need to publish the latest changes first`
});
}

// Create/update/delete cron in GCP
// GoogleAPI client for Cloud Scheduler
const gapi = await createGapi({
apiKey: "AIzaSyCclj9WTy8ZAPxOBQeLyt_JS8zVF93wVnI",
discoveryDocs: [
"https://cloudscheduler.googleapis.com/$discovery/rest?version=v1"
],
access_token: token
});

if (actual.enabled) {
const job = {
name: `projects/endpointservice/locations/${actual.location}/jobs/${fullname}`,
schedule: actual.schedule,
timeZone: actual.timezone,
httpTarget: {
uri: actual.url,
httpMethod: actual.method,
body: btoa(actual.body)
}
};

try {
await gapi.cloudscheduler.projects.locations.jobs.patch({
name: job.name,
resource: job
});
} catch (err) {
// If we are creating a new cron we need to do a limit check
const numCrons = (
await firebase
.firestore()
.collectionGroup("crons")
.where("subdomain", "==", subdomain)
.where("enabled", "==", true)
.get()
).size;
console.log("Crons: " + numCrons);
if (numCrons > 1) throw new Error("Max 2 crons per subdomain ATM");
await gapi.cloudscheduler.projects.locations.jobs.create({
parent: `projects/endpointservice/locations/${actual.location}`,
resource: job
});
}
} else {
await gapi.cloudscheduler.projects.locations.jobs.delete({
name: `projects/endpointservice/locations/${actual.location}/jobs/${fullname}`
});
}
const path = configPath(subdomain, notebook, expected.name);
console.log(`${path} to ${JSON.stringify(actual)}`);
await firebase
.firestore()
.doc(path)
.set({ ...actual, subdomain });

res.json({});
} catch (err) {
console.log(JSON.stringify(req.body));
console.log(err.message);
res.status(500).json({
message: err.message || err.body
});
}
},
{
modifiers: ["orchestrator"],
secrets: ["endpointservices_secretadmin_service_account_key"]
}
)
Insert cell
fetchWithTimeout = async (url, options, timeout = 10000) => {
return await Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Cannot fetch config, did you publish the notebook?')), timeout)
)
]);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more