diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7ad740e3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# top-most EditorConfig file +root = true + +# For all files +[*] +insert_final_newline = true +tab_width = 2 + +[*.{props,targets,csproj}] +tab_width = 4 + +[*.cs] +tab_width = 4 + +# VSTHRD200: Use "Async" suffix for async methods +dotnet_diagnostic.VSTHRD200.severity = none + +# SA1001: Commas should be spaced correctly +dotnet_diagnostic.SA1001.severity = none + +# FAA0002: Replace Xunit assertion with Fluent Assertions equivalent +dotnet_diagnostic.FAA0002.severity = none + +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = none diff --git a/.github/scripts/_meta.mts b/.github/scripts/_meta.mts index e9d5b00f..eb41cec8 100644 --- a/.github/scripts/_meta.mts +++ b/.github/scripts/_meta.mts @@ -1,11 +1,43 @@ import { globby } from "globby"; import path from "node:path"; -import fs from "node:fs"; +import fs from "node:fs/promises"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import { $, within } from "zx"; + +let _queue: Promise = Promise.resolve(); +const enqueue = (fn: () => Promise): Promise => { + var task = _queue.then(() => within(fn)); + _queue = task.then( + (_) => {}, + (_) => {} + ); + return task; +}; + +const configSchema = z.object({ + name: z.string().optional(), + shortName: z.string().optional(), + image: z + .object({ + name: z.string().optional(), + type: z.enum(["dotnet", "docker"]).default("dotnet"), + source: z.string().optional(), + }) + .refine((v) => { + if (v.type === "docker" && !v.name) { + return { message: "Image name is required for docker images" }; + } + }) + .optional(), +}); export type VerticalType = "app" | "lib" | "pkg"; export type ImageInfo = { readonly name: string; + readonly type: "dotnet" | "docker"; + readonly source: string; }; export type Vertical = { @@ -15,7 +47,6 @@ export type Vertical = { readonly path: string; readonly relPath: string; readonly image?: ImageInfo; - readonly rawConfig: Readonly>; }; const vertialDirs = { @@ -26,33 +57,74 @@ const vertialDirs = { const last = (arr: string[]) => arr[arr.length - 1]; -const readVertical = (type: VerticalType, dirPath: string): Vertical => { +const readVertical = async ( + type: VerticalType, + dirPath: string +): Promise => { const verticalPath = path.resolve(dirPath); const dirName = path.basename(verticalPath); const configPath = path.resolve(verticalPath, "conf.json"); - let config: Readonly> = {}; + let parsed: any = {}; try { - const json = fs.readFileSync(configPath, { encoding: "utf-8" }); - config = JSON.parse(json); + const json = await fs.readFile(configPath, { encoding: "utf-8" }); + const parsed = JSON.parse(json); } catch (e) {} + const result = await configSchema.safeParseAsync(parsed); + if (!result.success) { + const error = fromError(result.error); + console.error(`Error parsing ${configPath}: ${error.toString()}`); + throw error; + } + + const config = result.data; + let name = dirName; - if (typeof config.name === "string" && config.name) { + if (config.name) { name = config.name; } let shortName = last(name.split(".")); - if (typeof config.shortName === "string" && config.shortName) { + if (config.shortName) { shortName = config.shortName; } let image: ImageInfo | undefined; - if ("image" in config) { - // TODO: validate? - image = { - name: (config.image as any).name, - }; + if (type === "app") { + $.cwd = verticalPath; + const confImage = config.image ?? { type: "dotnet" }; + + switch (confImage.type) { + case "dotnet": { + if (!confImage.source) { + confImage.source = `src/${name}`; + } + + if (!confImage.name) { + confImage.name = await enqueue(async () => { + $.cwd = verticalPath; + const result = + await $`dotnet msbuild ${confImage.source} -getProperty:ContainerName`; + return result.stdout.trim(); + }); + } + break; + } + + case "docker": { + if (!confImage.source) { + confImage.source = "Dockerfile"; + } + break; + } + + default: { + throw new Error(`Unsupported image type: ${confImage.type}`); + } + } + + image = confImage as ImageInfo; } return { @@ -62,17 +134,26 @@ const readVertical = (type: VerticalType, dirPath: string): Vertical => { path: verticalPath, relPath: dirPath, image, - rawConfig: config, }; }; const apps = await globby(`${vertialDirs.app}/*`, { onlyDirectories: true }); const libs = await globby(`${vertialDirs.lib}/*`, { onlyDirectories: true }); const pkgs = await globby(`${vertialDirs.pkg}/*`, { onlyDirectories: true }); -const verticals = [ +const promises = [ ...apps.map((app) => readVertical("app", app)), ...libs.map((lib) => readVertical("lib", lib)), ...pkgs.map((pkg) => readVertical("pkg", pkg)), ]; +const verticals = await Promise.all(promises); + +export const getApp = (name: string) => { + const app = verticals.find((v) => v.name === name); + if (!app) { + throw new Error(`App not found: ${name}`); + } + + return app; +}; export { verticals }; diff --git a/.github/scripts/build-push-app.mts b/.github/scripts/build-push-app.mts new file mode 100644 index 00000000..d3e87da2 --- /dev/null +++ b/.github/scripts/build-push-app.mts @@ -0,0 +1,38 @@ +import { $ } from "zx"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { getApp } from "./_meta.mts"; +import * as actions from "@actions/core"; + +const argv = yargs(hideBin(process.argv)) + .positional("name", { + type: "string", + required: true, + }) + .option("tag", { + type: "string", + }) + .parse(); + +const app = getApp(argv.name); +const tag = + argv.tag || (process.env.GITHUB_SHA ?? "").substring(0, 7) || "latest"; + +if (!app.image) { + throw new Error(`No image config found for ${app.name}`); +} + +const imgCfg = app.image; +if (imgCfg.type !== "dotnet") { + throw new Error(`Unsupported image type (not implemented): ${imgCfg.type}`); +} + +const source = imgCfg.source.replaceAll("\\", "/"); +$.cwd = app.path; +await $`dotnet publish ${source} --os linux-musl -t:PublishContainer -p:ContainerImageTag=${tag} -bl`.verbose( + true +); + +const fullImage = `ghcr.io/altinn/altinn-authorization-tmp/${imgCfg.name}:${tag}`; +actions.setOutput("image", fullImage); +actions.setOutput("tag", tag); diff --git a/.github/scripts/get-metadata.mts b/.github/scripts/get-metadata.mts new file mode 100644 index 00000000..80085d76 --- /dev/null +++ b/.github/scripts/get-metadata.mts @@ -0,0 +1,3 @@ +import { verticals } from "./_meta.mts"; + +console.log(verticals); diff --git a/.github/scripts/metadata.py b/.github/scripts/metadata.py deleted file mode 100755 index 66e38dde..00000000 --- a/.github/scripts/metadata.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -from json import load as load_json -from os import getenv -from argparse import ArgumentParser, Namespace - - -def main(args: Namespace): - """ - Reads the given JSON file and appends the key=values to GITHUB_ENV. - """ - with open(getenv("GITHUB_ENV"), "+a") as f: - f.writelines( - map(lambda item: f"{item[0]}={item[1]}\n", read_file(args.file).items()) - ) - - -def read_file(file: str) -> dict: - if file is None: - raise ValueError("argument file is not provided") - - with open(file, "+r") as f: - return load_json(f) - - -if __name__ == "__main__": - args = ArgumentParser() - args.add_argument("-f", "--file", help="file path for metadata.json") - main(args.parse_args()) diff --git a/.github/scripts/package.json b/.github/scripts/package.json index c05d0575..defda982 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -14,6 +14,8 @@ "tsx": "^4.7.1", "typescript": "^5.4.3", "yargs": "^17.7.2", + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0", "zx": "^8.0.0" } } diff --git a/.github/scripts/pnpm-lock.yaml b/.github/scripts/pnpm-lock.yaml index e34c1173..56b666b7 100644 --- a/.github/scripts/pnpm-lock.yaml +++ b/.github/scripts/pnpm-lock.yaml @@ -32,6 +32,12 @@ importers: yargs: specifier: ^17.7.2 version: 17.7.2 + zod: + specifier: ^3.23.8 + version: 3.23.8 + zod-validation-error: + specifier: ^3.4.0 + version: 3.4.0(zod@3.23.8) zx: specifier: ^8.0.0 version: 8.1.9 @@ -463,6 +469,15 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + zod-validation-error@3.4.0: + resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zx@8.1.9: resolution: {integrity: sha512-UHuLHphHmsBYKkAchkSrEN4nzDyagafqC9HUxtc1J7eopaScW6H9dsLJ1lmkAntnLtDTGoM8fa+jrJrXiIfKFA==} engines: {node: '>= 12.17.0'} @@ -842,6 +857,12 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + zod-validation-error@3.4.0(zod@3.23.8): + dependencies: + zod: 3.23.8 + + zod@3.23.8: {} + zx@8.1.9: optionalDependencies: '@types/fs-extra': 11.0.4 diff --git a/.github/workflows/_deploy-app.yml b/.github/workflows/_deploy-app.yml index 8b231dcb..cf4c8a5f 100644 --- a/.github/workflows/_deploy-app.yml +++ b/.github/workflows/_deploy-app.yml @@ -3,21 +3,16 @@ name: "Template: Deploy app" on: workflow_call: inputs: - repository: - description: Docker image repository (part of the registry) + name: + required: true + description: Name of the app type: string - default: altinn/altinn-authorization-tmp path: required: true description: Path to the app type: string - imageName: - required: true - description: Name of the image - type: string - jobs: build-push: name: Build and Push @@ -31,10 +26,16 @@ jobs: - uses: actions/checkout@v4 name: Checkout repository - - name: Set Short SHA - run: | - short_sha=$(git rev-parse --short ${{ github.sha }}) - echo "short_sha=$short_sha" >> $GITHUB_ENV + - name: Install .NET + uses: actions/setup-dotnet@v4 + + - uses: pnpm/action-setup@v3 + with: + version: 9 + run_install: | + - cwd: .github/scripts + args: [--frozen-lockfile] + - args: [--global, tsx] - uses: docker/login-action@v3 name: Login to ghcr @@ -43,11 +44,23 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@v6 - id: build - name: Build and Push - with: - file: ${{ inputs.path }}/Dockerfile - context: ./ - push: true - tags: ghrc.io/${{ inputs.repository }}/${{ inputs.imageName }}:${{ env.short_sha }} + # - uses: docker/build-push-action@v6 + # id: build + # name: Build and Push + # with: + # file: ${{ inputs.path }}/Dockerfile + # context: ./ + # push: true + # tags: ghrc.io/${{ inputs.repository }}/${{ inputs.imageName }}:${{ env.short_sha }} + + - name: Build and Push ${{ inputs.name }} + id: img + run: tsx ./.github/scripts/build-push-app.mts "${{ inputs.name }}" + + - name: Print outputs + env: + IMAGE: ${{ steps.img.outputs.image }} + TAG: ${{ steps.img.outputs.tag }} + run: | + echo "Image: ${IMAGE}" + echo "Tag: ${TAG}" diff --git a/.github/workflows/_find-verticals.yml b/.github/workflows/_find-verticals.yml index f3d47c46..222541ab 100644 --- a/.github/workflows/_find-verticals.yml +++ b/.github/workflows/_find-verticals.yml @@ -28,6 +28,10 @@ jobs: steps: - uses: actions/checkout@v4 + + - name: Install .NET + uses: actions/setup-dotnet@v4 + - uses: pnpm/action-setup@v3 with: version: 9 diff --git a/.github/workflows/cd-apps.yml b/.github/workflows/cd-apps.yml index 0263c4f8..5db5bd74 100644 --- a/.github/workflows/cd-apps.yml +++ b/.github/workflows/cd-apps.yml @@ -25,6 +25,5 @@ jobs: uses: ./.github/workflows/_deploy-app.yml with: - repository: altinn/altinn-authorization-tmp path: ${{ matrix.path }} - imageName: ${{ matrix.imageName }} + name: ${{ matrix.name }} diff --git a/.github/workflows/pr-apps.yml b/.github/workflows/pr-apps.yml index 788fd73c..1507cf8c 100644 --- a/.github/workflows/pr-apps.yml +++ b/.github/workflows/pr-apps.yml @@ -7,9 +7,6 @@ on: - .github/workflows/apps-* - .github/workflows/infra-* -env: - DOTNET_VERSION: 8.0.x - jobs: find-verticals: uses: ./.github/workflows/_find-verticals.yml @@ -25,10 +22,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install .NET ${{ env.DOTNET_VERSION }} + - name: Install .NET uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - name: Build working-directory: ${{ matrix.path }} diff --git a/.github/workflows/pr-check-slns.yml b/.github/workflows/pr-check-slns.yml index faec1f2a..fb28c4ac 100644 --- a/.github/workflows/pr-check-slns.yml +++ b/.github/workflows/pr-check-slns.yml @@ -12,9 +12,6 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/pr-libs.yml b/.github/workflows/pr-libs.yml index 1a1695a0..ca668574 100644 --- a/.github/workflows/pr-libs.yml +++ b/.github/workflows/pr-libs.yml @@ -25,11 +25,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Install .NET ${{ env.DOTNET_VERSION }} + - name: Install .NET uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - name: Build working-directory: ${{ matrix.path }} diff --git a/.gitignore b/.gitignore index 2b9e07d1..06e26b95 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,7 @@ terraform.rc .vscode .vs .idea -*.DotSettings.user \ No newline at end of file +*.DotSettings.user + +# MSBuild binlogs +*.binlog diff --git a/.justfile b/.justfile index d337812e..e75e5016 100644 --- a/.justfile +++ b/.justfile @@ -19,3 +19,8 @@ @update-sln-files: install-script-packages-frozen #!pwsh ./.github/scripts/node_modules/.bin/tsx ./.github/scripts/update-sln-files.mts + +# Print all projects metadata +@get-metadata: install-script-packages-frozen + #!pwsh + ./.github/scripts/node_modules/.bin/tsx ./.github/scripts/get-metadata.mts diff --git a/global.json b/global.json new file mode 100644 index 00000000..d8840a79 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.402" + } +} diff --git a/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages.Models/Altinn.Authorization.AccessPackages.Models.csproj b/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages.Models/Altinn.Authorization.AccessPackages.Models.csproj index 36b964f5..2b8a13c7 100644 --- a/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages.Models/Altinn.Authorization.AccessPackages.Models.csproj +++ b/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages.Models/Altinn.Authorization.AccessPackages.Models.csproj @@ -1,3 +1,6 @@  - \ No newline at end of file + + + + diff --git a/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages/Altinn.Authorization.AccessPackages.csproj b/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages/Altinn.Authorization.AccessPackages.csproj index f8687061..8266ff73 100644 --- a/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages/Altinn.Authorization.AccessPackages.csproj +++ b/src/apps/Altinn.Authorization.AccessPackages/src/Altinn.Authorization.AccessPackages/Altinn.Authorization.AccessPackages.csproj @@ -1,5 +1,10 @@ + + true + altinn-authorization-access-packages + + @@ -20,9 +25,10 @@ Include="..\..\..\..\libs\Altinn.Authorization.Configuration\src\Altinn.Authorization.Configuration.OpenTelemetry\Altinn.Authorization.Configuration.OpenTelemetry.csproj" /> - + - \ No newline at end of file + diff --git a/src/apps/Altinn.Authorization.DeployApi/src/Altinn.Authorization.DeployApi/Altinn.Authorization.DeployApi.csproj b/src/apps/Altinn.Authorization.DeployApi/src/Altinn.Authorization.DeployApi/Altinn.Authorization.DeployApi.csproj index 3cffc644..964edd00 100644 --- a/src/apps/Altinn.Authorization.DeployApi/src/Altinn.Authorization.DeployApi/Altinn.Authorization.DeployApi.csproj +++ b/src/apps/Altinn.Authorization.DeployApi/src/Altinn.Authorization.DeployApi/Altinn.Authorization.DeployApi.csproj @@ -1,25 +1,25 @@ - - net8.0 - enable - enable - + + true + altinn-authorization-deployapi + - - - - - - - - - - - + + + + + + + + + + + - - - + + + diff --git a/src/apps/Altinn.Authorization.Index/src/Altinn.Authorization.Index/Altinn.Authorization.Index.csproj b/src/apps/Altinn.Authorization.Index/src/Altinn.Authorization.Index/Altinn.Authorization.Index.csproj index 1e8177a4..eb5d39cb 100644 --- a/src/apps/Altinn.Authorization.Index/src/Altinn.Authorization.Index/Altinn.Authorization.Index.csproj +++ b/src/apps/Altinn.Authorization.Index/src/Altinn.Authorization.Index/Altinn.Authorization.Index.csproj @@ -1,8 +1,8 @@ - Altinn.Authorization.Index - Altinn.Authorization.Index + true + altinn-authorization-index @@ -29,4 +29,4 @@ Include="..\..\..\..\libs\Altinn.Authorization.Hosting\src\Altinn.Authorization.Hosting\Altinn.Authorization.Hosting.csproj" /> - \ No newline at end of file + diff --git a/src/apps/Directory.Build.props b/src/apps/Directory.Build.props index 5136b8b5..9af0ff8c 100644 --- a/src/apps/Directory.Build.props +++ b/src/apps/Directory.Build.props @@ -1,4 +1,4 @@ - \ No newline at end of file + diff --git a/src/apps/Directory.Build.targets b/src/apps/Directory.Build.targets new file mode 100644 index 00000000..4c4a7c5f --- /dev/null +++ b/src/apps/Directory.Build.targets @@ -0,0 +1,28 @@ + + + + + + + + true + true + + localhost + ghcr.io + altinn/altinn-authorization-tmp/$(ContainerName) + + + + + + + + false + false + + + + + + diff --git a/src/libs/Directory.Build.props b/src/libs/Directory.Build.props index 20e06d22..9af0ff8c 100644 --- a/src/libs/Directory.Build.props +++ b/src/libs/Directory.Build.props @@ -1,5 +1,4 @@ - - \ No newline at end of file +