Skip to content

Commit

Permalink
Merge pull request #1951 from openkfw/1863-upgrade-button
Browse files Browse the repository at this point in the history
1863 upgrade button
  • Loading branch information
MartinJurcoGlina authored Sep 9, 2024
2 parents 461d359 + 07cb9db commit be4ca7f
Show file tree
Hide file tree
Showing 17 changed files with 660 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ scripts/development/.env
scripts/operation/.env
.idea/
docs/developer/api-docs
scripts/operation/.env.bak
scripts/operation/cronjob.err
scripts/operation/cronjob.output
api/src/trubudget-config/upgrade_version.txt
api/src/trubudget-config/upgradable.txt
182 changes: 182 additions & 0 deletions api/src/app_latest_version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import Joi = require("joi");
import { AugmentedFastifyInstance } from "./types";
import { VError } from "verror";
import { AuthenticatedRequest } from "./httpd/lib";
import { toHttpError } from "./http_errors";
import * as NotAuthenticated from "./http_errors/not_authenticated";
import { assertUnreachable } from "./lib/assertUnreachable";
import { Ctx } from "./lib/ctx";
import * as Result from "./result";
import { AuthToken } from "./service/domain/organization/auth_token";
import { ServiceUser } from "./service/domain/organization/service_user";
import * as UserCreate from "./service/domain/organization/user_create";
import axios from "axios";
import { DOCKERHUB_TRUBUDGET_TAGS_URL } from "system/constants";
import * as fs from "fs";

/**
* Represents the request body of the endpoint
*/
interface RequestBodyV1 {
apiVersion: "1.0";
}

const requestBodyV1Schema = Joi.object({
apiVersion: Joi.valid("1.0").required(),
data: Joi.object({}),
});

type RequestBody = RequestBodyV1;
const requestBodySchema = Joi.alternatives([requestBodyV1Schema]);

/**
* Validates the request body of the http request
*
* @param body the request body
* @returns the request body wrapped in a {@link Result.Type}. Contains either the object or an error
*/
function validateRequestBody(body: unknown): Result.Type<RequestBody> {
const { error, value } = requestBodySchema.validate(body);
return !error ? value : error;
}

/**
* Creates the swagger schema for the `/global.appUpgrade` endpoint
*
* @param server fastify server
* @returns the swagger schema for this endpoint
*/
function mkSwaggerSchema(server: AugmentedFastifyInstance): Object {
return {
preValidation: [server.authenticate],
schema: {
description: "Get latest app version available on dockerhub.",
tags: ["global"],
summary: "Get latest app version",
security: [
{
bearerToken: [],
},
],
body: {
type: "object",
required: ["apiVersion"],
properties: {
apiVersion: { type: "string", example: "1.0" },
data: {
type: "object",
},
},
},
response: {
200: {
description: "successful response",
type: "object",
properties: {
apiVersion: { type: "string", example: "1.0" },
data: {
type: "object",
properties: {
version: { type: "string", example: "2.15.0" },
message: { type: "string", example: "App is not upgradable" },
},
},
},
},
401: NotAuthenticated.schema,
},
},
};
}

/**
* Represents the service that stored version to upgrade to
*/
interface Service {
storeUpgradeVersion(
ctx: Ctx,
serviceUser: ServiceUser,
requestData: UserCreate.RequestData,
): Promise<Result.Type<AuthToken>>;
}

/**
* Creates an http handler that handles incoming http requests for the `/app.upgrade` route
*
* @param server the current fastify server instance
* @param urlPrefix the prefix of the http url
* @param service the service {@link Service} object used to offer an interface to the domain logic
*/
export function addHttpHandler(server: AugmentedFastifyInstance, urlPrefix: string): void {
server.register(async function () {
server.post(`${urlPrefix}/app.latestVersion`, mkSwaggerSchema(server), (request, reply) => {
const ctx: Ctx = { requestId: request.id, source: "http" };

const serviceUser: ServiceUser = {
id: (request as AuthenticatedRequest).user.userId,
groups: (request as AuthenticatedRequest).user.groups,
address: (request as AuthenticatedRequest).user.address,
};

const bodyResult = validateRequestBody(request.body);

if (Result.isErr(bodyResult)) {
const { code, body } = toHttpError(new VError(bodyResult, "failed to get new app version"));
request.log.error({ err: bodyResult }, "Invalid request body");
reply.status(code).send(body);
return;
}

// Check if user is root
if (serviceUser.id !== "root") {
const { body } = toHttpError(
new VError("User is not root", "failed to get new app version"),
);
request.log.error({ err: body }, "User is not root");
reply.status(403).send(body);
return;
}

// check if it is upgradable app
const appIsUpgradable = fs.existsSync(__dirname + "/trubudget-config/upgradable.txt");
if (!appIsUpgradable) {
reply.status(200).send({
apiVersion: "1.0",
data: {
message: "App is not upgradable",
},
});
return;
}

switch (bodyResult.apiVersion) {
case "1.0": {
axios.get(DOCKERHUB_TRUBUDGET_TAGS_URL).then((response) => {
const tags = response.data.results;
const highestVersion: string | undefined = tags
.filter(
(tag: { name: string }): boolean =>
tag.name !== "latest" && /^v\d+\.\d+\.\d+$/.test(tag.name),
)
.map((tag: { name: string }): string => tag.name)
.find((): boolean => true);
const code = 200;
const body = {
apiVersion: "1.0",
data: {
version: highestVersion,
},
};
reply.status(code).send(body);
});

break;
}
default:
// Joi validates only existing apiVersions
request.log.error({ err: bodyResult }, "Wrong api version specified");
assertUnreachable(bodyResult.apiVersion);
}
});
});
}
170 changes: 170 additions & 0 deletions api/src/app_upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import Joi = require("joi");
import { AugmentedFastifyInstance } from "./types";
import { VError } from "verror";
import { AuthenticatedRequest } from "./httpd/lib";
import { toHttpError } from "./http_errors";
import * as NotAuthenticated from "./http_errors/not_authenticated";
import { assertUnreachable } from "./lib/assertUnreachable";
import { Ctx } from "./lib/ctx";
import * as Result from "./result";
import { AuthToken } from "./service/domain/organization/auth_token";
import { ServiceUser } from "./service/domain/organization/service_user";
import * as UserCreate from "./service/domain/organization/user_create";
import * as fs from "fs";

/**
* Represents the request body of the endpoint
*/
interface RequestBodyV1 {
apiVersion: "1.0";
data: {
version: string;
};
}

const requestBodyV1Schema = Joi.object({
apiVersion: Joi.valid("1.0").required(),
data: Joi.object({
version: Joi.string().required(),
}),
});

type RequestBody = RequestBodyV1;
const requestBodySchema = Joi.alternatives([requestBodyV1Schema]);

/**
* Validates the request body of the http request
*
* @param body the request body
* @returns the request body wrapped in a {@link Result.Type}. Contains either the object or an error
*/
function validateRequestBody(body: unknown): Result.Type<RequestBody> {
const { error, value } = requestBodySchema.validate(body);
return !error ? value : error;
}

/**
* Creates the swagger schema for the `/global.appUpgrade` endpoint
*
* @param server fastify server
* @returns the swagger schema for this endpoint
*/
function mkSwaggerSchema(server: AugmentedFastifyInstance): Object {
return {
preValidation: [server.authenticate],
schema: {
description: "Upgrade app to new version.",
tags: ["global"],
summary: "Upgrade app",
security: [
{
bearerToken: [],
},
],
body: {
type: "object",
required: ["apiVersion", "data"],
properties: {
apiVersion: { type: "string", example: "1.0" },
data: {
type: "object",
required: ["version"],
properties: {
version: { type: "string", example: "2.15.0" },
},
},
},
},
response: {
200: {
description: "successful response",
type: "object",
properties: {
apiVersion: { type: "string", example: "1.0" },
data: {
type: "object",
properties: {
version: { type: "string", example: "2.15.0" },
},
},
},
},
401: NotAuthenticated.schema,
},
},
};
}

/**
* Represents the service that stored version to upgrade to
*/
interface Service {
storeUpgradeVersion(
ctx: Ctx,
serviceUser: ServiceUser,
requestData: UserCreate.RequestData,
): Promise<Result.Type<AuthToken>>;
}

/**
* Creates an http handler that handles incoming http requests for the `/app.upgrade` route
*
* @param server the current fastify server instance
* @param urlPrefix the prefix of the http url
* @param service the service {@link Service} object used to offer an interface to the domain logic
*/
export function addHttpHandler(server: AugmentedFastifyInstance, urlPrefix: string): void {
server.register(async function () {
server.post(`${urlPrefix}/app.upgrade`, mkSwaggerSchema(server), (request, reply) => {
const ctx: Ctx = { requestId: request.id, source: "http" };

const serviceUser: ServiceUser = {
id: (request as AuthenticatedRequest).user.userId,
groups: (request as AuthenticatedRequest).user.groups,
address: (request as AuthenticatedRequest).user.address,
};

const bodyResult = validateRequestBody(request.body);

if (Result.isErr(bodyResult)) {
const { code, body } = toHttpError(
new VError(bodyResult, "failed to store new app version"),
);
request.log.error({ err: bodyResult }, "Invalid request body");
reply.status(code).send(body);
return;
}

// Check if user is root
if (serviceUser.id !== "root") {
const { code, body } = toHttpError(
new VError("User is not root", "failed to store new app version"),
);
request.log.error({ err: body }, "User is not root");
reply.status(code).send(body);
return;
}

switch (bodyResult.apiVersion) {
case "1.0": {
const data = bodyResult.data;
fs.writeFileSync(__dirname + "/trubudget-config/upgrade_version.txt", data.version);

const code = 200;
const body = {
apiVersion: "1.0",
data: {
version: bodyResult.data.version,
},
};
reply.status(code).send(body);
break;
}
default:
// Joi validates only existing apiVersions
request.log.error({ err: bodyResult }, "Wrong api version specified");
assertUnreachable(bodyResult.apiVersion);
}
});
});
}
9 changes: 9 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import * as Project from "./service/domain/workflow/project";
import * as Subproject from "./service/domain/workflow/subproject";
import * as Workflowitem from "./service/domain/workflow/workflowitem";
import getValidConfig, { config } from "./config";
import * as AppLatestVersionAPI from "./app_latest_version";
import * as AppUpgradeVersionAPI from "./app_upgrade";
import * as GlobalPermissionGrantAPI from "./global_permission_grant";
import * as GlobalPermissionRevokeAPI from "./global_permission_revoke";
import * as GlobalPermissionsGrantAllAPI from "./global_permissions_grant_all";
Expand Down Expand Up @@ -296,6 +298,13 @@ registerRoutes(server, db, URL_PREFIX, blockchain.host, blockchain.port, storage
Cache.invalidateCache(db),
);

/*
* APIs related to App versioning
*/

AppLatestVersionAPI.addHttpHandler(server, URL_PREFIX);
AppUpgradeVersionAPI.addHttpHandler(server, URL_PREFIX);

/*
* APIs related to Global Permissions
*/
Expand Down
1 change: 1 addition & 0 deletions api/src/system/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DOCKERHUB_TRUBUDGET_TAGS_URL = "https://hub.docker.com/v2/repositories/trubudget%2Fapi/tags?page_size=10";
Loading

0 comments on commit be4ca7f

Please sign in to comment.