From 452085c5aaa8b4254014c67518752b8e3e65d8bf Mon Sep 17 00:00:00 2001 From: Matthew Bystedt Date: Tue, 30 Apr 2024 15:58:01 -0700 Subject: [PATCH] feat: build package logging and checks (#199) * feat: build package logging and checks * fix: remove commented code * feat: add installation history to builds * feat: display basic build history inspector * feat: add setting for accounts to bypass semver check --- package-lock.json | 222 +++++++++--------- package.json | 14 +- scripts/db/mongo-setup.js | 7 + scripts/provision-app-quick-build.json | 3 +- scripts/provision-app-quick-build.sh | 20 +- scripts/provision-app-quick-install.json | 10 +- scripts/provision-app-quick-install.sh | 4 +- src/collection/collection.service.ts | 8 +- src/constants.ts | 8 +- src/graph/intention-sync.service.ts | 93 +++++++- src/intention/action.service.ts | 176 ++++++++++++-- src/intention/dto/action-patch-rest.dto.ts | 16 ++ src/intention/dto/artifact.dto.ts | 8 +- src/intention/dto/cloud-object.dto.ts | 14 +- src/intention/dto/cloud.dto.ts | 2 +- src/intention/dto/intention.dto.ts | 3 + src/intention/dto/package-rest.dto.ts | 16 ++ src/intention/dto/service.dto.ts | 5 +- src/intention/intention.controller.ts | 78 +++--- src/intention/intention.service.ts | 167 +++++++------ .../dto/broker-account-rest.dto.ts | 21 +- src/persistence/dto/broker-account.dto.ts | 5 + src/persistence/dto/package-build-rest.dto.ts | 19 ++ src/persistence/dto/package-build.dto.ts | 79 +++++++ src/persistence/dto/service-rest.dto.ts | 3 + src/persistence/dto/timestamp-rest.dto.ts | 4 + src/persistence/dto/timestamp.dto.ts | 11 + src/persistence/dto/user.dto.ts | 4 + .../interfaces/build.repository.ts | 33 +++ .../mongo/build-mongo.repository.ts | 132 +++++++++++ .../mongo/collection-mongo.repository.ts | 37 ++- .../mongo/graph-mongo.repository.ts | 24 +- .../mongo/intention-mongo.repository.ts | 5 + src/persistence/mongo/mongo.util.ts | 7 + src/persistence/persistence.module.ts | 21 +- src/util/action.util.spec.ts | 10 + src/util/action.util.ts | 46 ++++ ui/package-lock.json | 190 +++++++-------- ui/package.json | 22 +- ui/src/app/app.component.html | 26 +- .../inspector-instances.component.html | 7 + .../inspector-instances.component.ts | 4 + .../inspector-intentions.component.spec.ts | 2 +- .../inspector-releases.component.html | 50 ++++ .../inspector-releases.component.scss | 21 ++ .../inspector-releases.component.spec.ts | 22 ++ .../inspector-releases.component.ts | 27 +++ .../history-table.component.html | 8 +- .../history-table/history-table.component.ts | 2 +- ui/src/app/service/build-api.service.spec.ts | 16 ++ ui/src/app/service/build-api.service.ts | 19 ++ .../service/dto/broker-account-rest.dto.ts | 22 +- 52 files changed, 1325 insertions(+), 448 deletions(-) create mode 100644 src/intention/dto/action-patch-rest.dto.ts create mode 100644 src/intention/dto/package-rest.dto.ts create mode 100644 src/persistence/dto/package-build-rest.dto.ts create mode 100644 src/persistence/dto/package-build.dto.ts create mode 100644 src/persistence/dto/timestamp-rest.dto.ts create mode 100644 src/persistence/dto/timestamp.dto.ts create mode 100644 src/persistence/interfaces/build.repository.ts create mode 100644 src/persistence/mongo/build-mongo.repository.ts create mode 100644 ui/src/app/graph/inspector-releases/inspector-releases.component.html create mode 100644 ui/src/app/graph/inspector-releases/inspector-releases.component.scss create mode 100644 ui/src/app/graph/inspector-releases/inspector-releases.component.spec.ts create mode 100644 ui/src/app/graph/inspector-releases/inspector-releases.component.ts create mode 100644 ui/src/app/service/build-api.service.spec.ts create mode 100644 ui/src/app/service/build-api.service.ts diff --git a/package-lock.json b/package-lock.json index b66e7ff9..7f2c569c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "8.1.1", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-kinesis": "^3.554.0", + "@aws-sdk/client-kinesis": "^3.556.0", "@nestjs/axios": "^3.0.2", - "@nestjs/common": "^10.3.7", + "@nestjs/common": "^10.3.8", "@nestjs/config": "^3.2.2", - "@nestjs/core": "^10.3.7", + "@nestjs/core": "^10.3.8", "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.3.7", + "@nestjs/platform-express": "^10.3.8", "@nestjs/schedule": "^4.0.2", "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.3.1", @@ -50,7 +50,7 @@ "@golevelup/ts-jest": "^0.5.0", "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.1.1", - "@nestjs/testing": "^10.3.7", + "@nestjs/testing": "^10.3.8", "@types/cron": "^2.4.0", "@types/deep-equal": "^1.0.4", "@types/ejs": "^3.1.5", @@ -64,8 +64,8 @@ "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.8", - "@typescript-eslint/eslint-plugin": "^7.7.0", - "@typescript-eslint/parser": "^7.7.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -390,15 +390,15 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@aws-sdk/client-kinesis": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-kinesis/-/client-kinesis-3.554.0.tgz", - "integrity": "sha512-xRlqaZVsTWYPUNwRlhpXczjbSb+Qj6LDpK4SNCynRUJzx1G7b5hZS1L3yb9QvFNTyGj9F91LjacjaEzjBw9Fpw==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kinesis/-/client-kinesis-3.556.0.tgz", + "integrity": "sha512-S9Q6KGMKbSOQzEKpvS1PKNMW5CbyPlDQkBYhhGZpnXoEdEGN1+a4xIHLtqqgNqu4XTsREFuS0wOxAR+2VGOfjA==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.554.0", - "@aws-sdk/core": "3.554.0", - "@aws-sdk/credential-provider-node": "3.554.0", + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/credential-provider-node": "3.556.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -444,13 +444,13 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.554.0.tgz", - "integrity": "sha512-yj6CgIxCT3UwMumEO481KH4QvwArkAPzD7Xvwe1QKgJATc9bKNEo/FxV8LfnWIJ7nOtMDxbNxYLMXH/Fs1qGaQ==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.556.0.tgz", + "integrity": "sha512-unXdWS7uvHqCcOyC1de+Fr8m3F2vMg2m24GPea0bg7rVGTYmiyn9mhUX11VCt+ozydrw+F50FQwL6OqoqPocmw==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.554.0", + "@aws-sdk/core": "3.556.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -492,14 +492,14 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.554.0.tgz", - "integrity": "sha512-M86rkiRqbZBF5VyfTQ/vttry9VSoQkZ1oCqYF+SAGlXmD0Of8587yRSj2M4rYe0Uj7nRQIfSnhDYp1UzsZeRfQ==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.556.0.tgz", + "integrity": "sha512-AXKd2TB6nNrksu+OfmHl8uI07PdgzOo4o8AxoRO8SHlwoMAGvcT9optDGVSYoVfgOKTymCoE7h8/UoUfPc11wQ==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.554.0", - "@aws-sdk/core": "3.554.0", + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/core": "3.556.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -540,17 +540,17 @@ "node": ">=14.0.0" }, "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.554.0" + "@aws-sdk/credential-provider-node": "^3.556.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.554.0.tgz", - "integrity": "sha512-EhaA6T0M0DNg5M8TCF1a7XJI5D/ZxAF3dgVIchyF98iNzjYgl/7U8K6hJay2A11aFvVu70g46xYMpz3Meky4wQ==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.556.0.tgz", + "integrity": "sha512-TsK3js7Suh9xEmC886aY+bv0KdLLYtzrcmVt6sJ/W6EnDXYQhBuKYFhp03NrN2+vSvMGpqJwR62DyfKe1G0QzQ==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.554.0", + "@aws-sdk/core": "3.556.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -591,17 +591,17 @@ "node": ">=14.0.0" }, "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.554.0" + "@aws-sdk/credential-provider-node": "^3.556.0" } }, "node_modules/@aws-sdk/core": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.554.0.tgz", - "integrity": "sha512-JrG7ToTLeNf+/S3IiCUPVw9jEDB0DXl5ho8n/HwOa946mv+QyCepCuV2U/8f/1KAX0mD8Ufm/E4/cbCbFHgbSg==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.556.0.tgz", + "integrity": "sha512-vJaSaHw2kPQlo11j/Rzuz0gk1tEaKdz+2ser0f0qZ5vwFlANjt08m/frU17ctnVKC1s58bxpctO/1P894fHLrA==", "dependencies": { "@smithy/core": "^1.4.2", "@smithy/protocol-http": "^3.3.0", - "@smithy/signature-v4": "^2.2.1", + "@smithy/signature-v4": "^2.3.0", "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "fast-xml-parser": "4.2.5", @@ -645,15 +645,15 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.554.0.tgz", - "integrity": "sha512-BQenhg43S6TMJHxrdjDVdVF+HH5tA1op9ZYLyJrvV5nn7CCO4kyAkkOuSAv1NkL+RZsIkW0/vHTXwQOQw3cUsg==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.556.0.tgz", + "integrity": "sha512-0Nz4ErOlXhe3muxWYMbPwRMgfKmVbBp36BAE2uv/z5wTbfdBkcgUwaflEvlKCLUTdHzuZsQk+BFS/gVyaUeOuA==", "dependencies": { - "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/client-sts": "3.556.0", "@aws-sdk/credential-provider-env": "3.535.0", "@aws-sdk/credential-provider-process": "3.535.0", - "@aws-sdk/credential-provider-sso": "3.554.0", - "@aws-sdk/credential-provider-web-identity": "3.554.0", + "@aws-sdk/credential-provider-sso": "3.556.0", + "@aws-sdk/credential-provider-web-identity": "3.556.0", "@aws-sdk/types": "3.535.0", "@smithy/credential-provider-imds": "^2.3.0", "@smithy/property-provider": "^2.2.0", @@ -666,16 +666,16 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.554.0.tgz", - "integrity": "sha512-poX/+2OE3oxqp4f5MiaJh251p8l+bzcFwgcDBwz0e2rcpvMSYl9jw4AvGnCiG2bmf9yhNJdftBiS1A+KjxV0qA==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.556.0.tgz", + "integrity": "sha512-s1xVtKjyGc60O8qcNIzS1X3H+pWEwEfZ7TgNznVDNyuXvLrlNWiAcigPWGl2aAkc8tGcsSG0Qpyw2KYC939LFg==", "dependencies": { "@aws-sdk/credential-provider-env": "3.535.0", "@aws-sdk/credential-provider-http": "3.552.0", - "@aws-sdk/credential-provider-ini": "3.554.0", + "@aws-sdk/credential-provider-ini": "3.556.0", "@aws-sdk/credential-provider-process": "3.535.0", - "@aws-sdk/credential-provider-sso": "3.554.0", - "@aws-sdk/credential-provider-web-identity": "3.554.0", + "@aws-sdk/credential-provider-sso": "3.556.0", + "@aws-sdk/credential-provider-web-identity": "3.556.0", "@aws-sdk/types": "3.535.0", "@smithy/credential-provider-imds": "^2.3.0", "@smithy/property-provider": "^2.2.0", @@ -703,12 +703,12 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.554.0.tgz", - "integrity": "sha512-8QPpwBA31i/fZ7lDZJC4FA9EdxLg5SJ8sPB2qLSjp5UTGTYL2HRl0Eznkb7DXyp/wImsR/HFR1NxuFCCVotLCg==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.556.0.tgz", + "integrity": "sha512-ETuBgcnpfxqadEAqhQFWpKoV1C/NAgvs5CbBc5EJbelJ8f4prTdErIHjrRtVT8c02MXj92QwczsiNYd5IoOqyw==", "dependencies": { - "@aws-sdk/client-sso": "3.554.0", - "@aws-sdk/token-providers": "3.554.0", + "@aws-sdk/client-sso": "3.556.0", + "@aws-sdk/token-providers": "3.556.0", "@aws-sdk/types": "3.535.0", "@smithy/property-provider": "^2.2.0", "@smithy/shared-ini-file-loader": "^2.4.0", @@ -720,11 +720,11 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.554.0.tgz", - "integrity": "sha512-HN54DzLjepw5ZWSF9ycGevhFTyg6pjLuLKy5Y8t/f1jFDComzYdGEDe0cdV9YO653W3+PQwZZGz09YVygGYBLg==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.556.0.tgz", + "integrity": "sha512-R/YAL8Uh8i+dzVjzMnbcWLIGeeRi2mioHVGnVF+minmaIkCiQMZg2HPrdlKm49El+RljT28Nl5YHRuiqzEIwMA==", "dependencies": { - "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/client-sts": "3.556.0", "@aws-sdk/types": "3.535.0", "@smithy/property-provider": "^2.2.0", "@smithy/types": "^2.12.0", @@ -807,11 +807,11 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.554.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.554.0.tgz", - "integrity": "sha512-KMMQ5Cw0FUPL9H8g69Lp08xtzRo7r/MK+lBV6LznWBbCP/NwtZ8awVHaPy2P31z00cWtu9MYkUTviWPqJTaBvg==", + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.556.0.tgz", + "integrity": "sha512-tvIiugNF0/+2wfuImMrpKjXMx4nCnFWQjQvouObny+wrif/PGqqQYrybwxPJDvzbd965bu1I+QuSv85/ug7xsg==", "dependencies": { - "@aws-sdk/client-sso-oidc": "3.554.0", + "@aws-sdk/client-sso-oidc": "3.556.0", "@aws-sdk/types": "3.535.0", "@smithy/property-provider": "^2.2.0", "@smithy/shared-ini-file-loader": "^2.4.0", @@ -2275,9 +2275,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", - "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.8.tgz", + "integrity": "sha512-P+vPEIvqx2e+fonsYVlFXKvoChyJ8Tq+lfpqdVFqblovHbFr3kZ/nYX0cPs+XuW6bnRT8tz0SSR9XBGU43kJhw==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -2318,9 +2318,9 @@ } }, "node_modules/@nestjs/core": { - "version": "10.3.7", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", - "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.8.tgz", + "integrity": "sha512-AxF4tpYLDNn5Wfb3C4bNaaHJ4pREH5FJrSisR2A5zkYpQFORFs0Tc36lOFPMwBTy8Iv2wUwWLUVc5ftBnxEv4w==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -2393,9 +2393,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.7", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", - "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.8.tgz", + "integrity": "sha512-sifLoxgEJvAgbim1UuW6wyScMfkS9SVQRH+lN33N/9ZvZSjO6NSDLOe+wxqsnZkia+QrjFC0qy0ITRAsggfqbg==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -2579,9 +2579,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.7", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", - "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.8.tgz", + "integrity": "sha512-hpX9das2TdFTKQ4/2ojhjI6YgXtCfXRKui3A4Qaj54VVzc5+mtK502Jj18Vzji98o9MVS6skmYu+S/UvW3U6Fw==", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -3714,16 +3714,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.0.tgz", - "integrity": "sha512-GJWR0YnfrKnsRoluVO3PRb9r5aMZriiMMM/RHj5nnTrBy1/wIgk76XCtCKcnXGjpZQJQRFtGV9/0JJ6n30uwpQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/type-utils": "7.7.0", - "@typescript-eslint/utils": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -3749,15 +3749,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.0.tgz", - "integrity": "sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" }, "engines": { @@ -3777,13 +3777,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz", - "integrity": "sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3794,13 +3794,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.0.tgz", - "integrity": "sha512-bOp3ejoRYrhAlnT/bozNQi3nio9tIgv3U5C0mVDdZC7cpcQEDZXvq8inrHYghLVwuNABRqrMW5tzAv88Vy77Sg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/utils": "7.7.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3821,9 +3821,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.0.tgz", - "integrity": "sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3834,13 +3834,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz", - "integrity": "sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3886,17 +3886,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.0.tgz", - "integrity": "sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", "semver": "^7.6.0" }, "engines": { @@ -3911,12 +3911,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz", - "integrity": "sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/types": "7.7.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { diff --git a/package.json b/package.json index 41719a0d..b629f8ae 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,14 @@ "typeorm": "typeorm-ts-node-commonjs" }, "dependencies": { - "@aws-sdk/client-kinesis": "^3.554.0", + "@aws-sdk/client-kinesis": "^3.556.0", "@nestjs/axios": "^3.0.2", - "@nestjs/common": "^10.3.7", + "@nestjs/common": "^10.3.8", "@nestjs/config": "^3.2.2", - "@nestjs/core": "^10.3.7", + "@nestjs/core": "^10.3.8", "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.3.7", + "@nestjs/platform-express": "^10.3.8", "@nestjs/schedule": "^4.0.2", "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.3.1", @@ -64,7 +64,7 @@ "@golevelup/ts-jest": "^0.5.0", "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.1.1", - "@nestjs/testing": "^10.3.7", + "@nestjs/testing": "^10.3.8", "@types/cron": "^2.4.0", "@types/deep-equal": "^1.0.4", "@types/ejs": "^3.1.5", @@ -78,8 +78,8 @@ "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.8", - "@typescript-eslint/eslint-plugin": "^7.7.0", - "@typescript-eslint/parser": "^7.7.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", diff --git a/scripts/db/mongo-setup.js b/scripts/db/mongo-setup.js index 82fce63b..e0926ee1 100644 --- a/scripts/db/mongo-setup.js +++ b/scripts/db/mongo-setup.js @@ -456,6 +456,13 @@ result = db.collectionConfig.insertOne({ hint: 'Defaults unknown users to developer access', value: false, }, + maskSemverFailures: { + name: 'Mask Semver Failures', + required: true, + type: 'boolean', + hint: 'Replace invalid version with 0.0.0', + value: false, + }, }, browseFields: ['name', 'email', 'clientId', 'website', 'requireRoleId'], name: 'Broker Account', diff --git a/scripts/provision-app-quick-build.json b/scripts/provision-app-quick-build.json index e14d9041..5fab5956 100644 --- a/scripts/provision-app-quick-build.json +++ b/scripts/provision-app-quick-build.json @@ -15,7 +15,8 @@ "environment": "tools" }, "package": { - "version": "2.73.2-snapshot" + "version": "2.73.2-snapshot", + "name": "superapp-backend" } } ], diff --git a/scripts/provision-app-quick-build.sh b/scripts/provision-app-quick-build.sh index 994aa7bd..bb58da5f 100755 --- a/scripts/provision-app-quick-build.sh +++ b/scripts/provision-app-quick-build.sh @@ -6,8 +6,6 @@ cd "$this_dir" PACKAGE_BUILD_VERSION=$(git rev-parse --verify HEAD) PACKAGE_VERSION="12.0.3" sha256=($(echo $RANDOM $RANDOM $RANDOM | shasum -a 256)) -echo -n $sha256 > provision-app-quick-build.artifact.sha256 -echo "sha256: $sha256" echo "===> Intention open" # Open intention @@ -36,17 +34,27 @@ echo -n $INTENTION_ID > provision-app-quick-build.intention.id echo "===> Build" -# Not shown: Build superapp and create artifact (build.zip) +# Not shown: Build superapp and create artifact echo "===> ..." echo "===> Build - Success!" -# Add artifact to action +# Patch action with build info ACTIONS_BUILD_TOKEN=$(echo $RESPONSE | jq -r '.actions.build.token') -curl -s -X POST $BROKER_URL/v1/intention/action/artifact \ +RESPONSE=$(curl -s -X POST $BROKER_URL/v1/intention/action/patch \ -H 'Content-Type: application/json' \ -H 'X-Broker-Token: '"$ACTIONS_BUILD_TOKEN"'' \ - -d '{"checksum": "sha256:'$sha256'", "name": "build.zip", "size": '$RANDOM', "type": "zip" }' + -d '{"package":{"checksum": "sha256:'$sha256'", "size": '$RANDOM', "type": "zip" }}' \ + ) +echo $RESPONSE | jq '.' +if [ "$(echo $RESPONSE | jq '.error')" != "null" ]; then + # Use saved intention token to close intention + echo "Exit: Error detected" + exit 0 +fi echo "===> Intention close" # Use saved intention token to close intention curl -s -X POST $BROKER_URL/v1/intention/close -H 'X-Broker-Token: '"$INTENTION_TOKEN"'' + +echo -n $sha256 > provision-app-quick-build.artifact.sha256 +echo "sha256: $sha256" diff --git a/scripts/provision-app-quick-install.json b/scripts/provision-app-quick-install.json index 2785490c..08dc3c1f 100644 --- a/scripts/provision-app-quick-install.json +++ b/scripts/provision-app-quick-install.json @@ -9,6 +9,13 @@ "action": "package-installation", "id": "install", "provision": [], + "cloud": { + "target": { + "instance": { + "name": "lollipop" + } + } + }, "service": { "project": "superapp", "name": "superapp-backend", @@ -17,9 +24,6 @@ "source": { "action": "package-build#build", "intention": "feef-ffwe-343432-42fefds-ffew" - }, - "package": { - "type": "zip" } } ], diff --git a/scripts/provision-app-quick-install.sh b/scripts/provision-app-quick-install.sh index 1e30e311..4e553c1c 100755 --- a/scripts/provision-app-quick-install.sh +++ b/scripts/provision-app-quick-install.sh @@ -36,10 +36,10 @@ echo "===> Install - Success!" # Add install to action ACTIONS_INSTALL_TOKEN=$(echo $RESPONSE | jq -r '.actions.install.token') -curl -s -X POST $BROKER_URL/v1/intention/action/install \ +curl -s -X POST $BROKER_URL/v1/intention/action/patch \ -H 'Content-Type: application/json' \ -H 'X-Broker-Token: '"$ACTIONS_INSTALL_TOKEN"'' \ - -d '{"cloudTarget":{"instance": {"name": "lollipop"}},"prop": {"port": "3243"}}' + -d '{"cloud":{"target":{"propStrategy":"replace","prop":{"port": "5000", "something": "else"}}}}' echo "===> Intention close" diff --git a/src/collection/collection.service.ts b/src/collection/collection.service.ts index cb74b6f3..9fb42212 100644 --- a/src/collection/collection.service.ts +++ b/src/collection/collection.service.ts @@ -14,10 +14,12 @@ import { IntentionService } from '../intention/intention.service'; import { PERSISTENCE_TYPEAHEAD_SUBQUERY_LIMIT } from '../persistence/persistence.constants'; import { RedisService } from '../redis/redis.service'; import { IntentionActionPointerDto } from '../persistence/dto/intention-action-pointer.dto'; +import { BuildRepository } from '../persistence/interfaces/build.repository'; @Injectable() export class CollectionService { constructor( + private readonly buildRepository: BuildRepository, private readonly collectionRepository: CollectionRepository, private readonly graphRepository: GraphRepository, private readonly intentionService: IntentionService, @@ -244,6 +246,7 @@ export class CollectionService { async getServiceDetails(serviceId: string) { const service = await this.graphRepository.getServiceDetails(serviceId); + const builds = await this.buildRepository.searchBuild(serviceId, 0, 5); if (!service) { throw new NotFoundException({ statusCode: 404, @@ -259,7 +262,10 @@ export class CollectionService { ); } } - return service; + return { + ...service, + builds, + }; } async getServiceSecureInfo(serviceId: string) { diff --git a/src/constants.ts b/src/constants.ts index 84b4e513..9c8e4091 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,9 +18,15 @@ export const INTENTION_MIN_TTL_SECONDS = 30; export const INTENTION_MAX_TTL_SECONDS = 1800; export const INTENTION_TRANSIENT_TTL_MS = 7 * 24 * 60 * 60 * 1000; -export const INTENTION_SERVICE_INSTANCE_SEARCH_PATHS = [ +// Search paths use last existing path as the value +export const INTENTION_SERVICE_ENVIRONMENT_SEARCH_PATHS = [ 'action.service.environment', + 'action.service.target.environment', +] as const; +export const INTENTION_SERVICE_INSTANCE_SEARCH_PATHS = [ + ...INTENTION_SERVICE_ENVIRONMENT_SEARCH_PATHS, 'action.service.instanceName', + 'action.service.target.instanceName', ] as const; export const SHORT_ENV_CONVERSION = { diff --git a/src/graph/intention-sync.service.ts b/src/graph/intention-sync.service.ts index d52714b5..696e2d32 100644 --- a/src/graph/intention-sync.service.ts +++ b/src/graph/intention-sync.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; import { get, set } from 'radash'; import deepEqual from 'deep-equal'; +import { plainToClass } from 'class-transformer'; + +import { INTENTION_SERVICE_INSTANCE_SEARCH_PATHS } from '../constants'; import { IntentionDto } from '../intention/dto/intention.dto'; import { ActionDto } from '../intention/dto/action.dto'; import { CollectionNames } from '../persistence/dto/collection-dto-union.type'; @@ -9,8 +12,10 @@ import { PersistenceUtilService } from '../persistence/persistence-util.service' import { GraphService } from './graph.service'; import { GraphRepository } from '../persistence/interfaces/graph.repository'; import { EdgePropDto } from '../persistence/dto/edge-prop.dto'; -import { INTENTION_SERVICE_INSTANCE_SEARCH_PATHS } from '../constants'; -import { InstallDto } from 'src/intention/dto/install.dto'; +import { ActionUtil } from '../util/action.util'; +import { CollectionRepository } from '../persistence/interfaces/collection.repository'; +import { IntentionActionPointerDto } from '../persistence/dto/intention-action-pointer.dto'; +import { BuildRepository } from '../persistence/interfaces/build.repository'; interface OverlayMapBase { key: string; @@ -33,9 +38,12 @@ type OverlayMap = OverlayMapWithPath | OverlayMapWithValue; @Injectable() export class IntentionSyncService { constructor( + private readonly buildRepository: BuildRepository, + private readonly collectionRepository: CollectionRepository, private readonly graphService: GraphService, private readonly graphRepository: GraphRepository, private readonly persistenceUtilService: PersistenceUtilService, + private readonly actionUtil: ActionUtil, ) {} public async sync(intention: IntentionDto) { @@ -63,10 +71,69 @@ export class IntentionSyncService { action.action === 'package-installation' ) { await this.syncPackageInstall(intention, action, serviceVertex); + await this.syncPackageBuild(intention, action, serviceVertex); + } + + if (action.action === 'package-build') { + await this.syncPackageBuild(intention, action, serviceVertex); } } } + private async syncPackageBuild( + intention: IntentionDto, + action: ActionDto, + serviceVertex: VertexDto, + ) { + if (!action.package || !action.package.name || !action.package.version) { + // Not enough package information to save + return; + } + + const parsedVersion = this.actionUtil.parseVersion(action.package.version); + if (!parsedVersion || parsedVersion.prerelease) { + // Not a valid version. Should not occur. + return; + } + if (parsedVersion.prerelease) { + // Pre-release versions are not release candidates -- not recorded + return; + } + + const service = await this.collectionRepository.getCollectionByVertexId( + 'service', + serviceVertex.id.toString(), + ); + if (!service) { + // Awkward. There should be a service here... + return; + } + + let packageBuild = await this.buildRepository.getBuildByPackageDetail( + service.id.toString(), + action.package.name, + parsedVersion, + ); + if (!packageBuild) { + packageBuild = await this.buildRepository.addBuild( + service.id.toString(), + action.package.name, + parsedVersion, + action.package, + ); + } + + if (action.action === 'package-installation') { + await this.buildRepository.addInstallActionToBuild( + packageBuild.id.toString(), + plainToClass(IntentionActionPointerDto, { + action: this.actionUtil.actionToIdString(action), + intention: intention.id, + }), + ); + } + } + public async syncPackageInstall( intention: IntentionDto, action: ActionDto, @@ -91,7 +158,7 @@ export class IntentionSyncService { }, { key: 'action.action', - value: `${action.action}#${action.id}`, + value: this.actionUtil.actionToIdString(action), }, { key: 'actionHistory[0].intention', @@ -99,7 +166,7 @@ export class IntentionSyncService { }, { key: 'actionHistory[0].action', - value: `${action.action}#${action.id}`, + value: this.actionUtil.actionToIdString(action), }, ], 'parentId', @@ -113,12 +180,22 @@ export class IntentionSyncService { envMap[action.service.environment].vertex.toString(), ); } + + const serverVertex = await this.syncServer(action); + if (serverVertex) { + const instanceName = this.actionUtil.instanceName(action); + this.syncInstallationProperties( + serviceVertex, + instanceName, + serverVertex, + action.cloud.target.propStrategy, + action.cloud.target.prop, + ); + } } - public async syncServer(action: ActionDto, install: InstallDto) { - const serverName = - install.cloudTarget?.instance?.name ?? - action.cloud?.target?.instance?.name; + public async syncServer(action: ActionDto) { + const serverName = action.cloud?.target?.instance?.name; if (!serverName) { return null; } diff --git a/src/intention/action.service.ts b/src/intention/action.service.ts index 74b0b569..824aa4c8 100644 --- a/src/intention/action.service.ts +++ b/src/intention/action.service.ts @@ -8,14 +8,17 @@ import { BrokerAccountProjectMapDto } from '../persistence/dto/graph-data.dto'; import { BrokerAccountDto } from '../persistence/dto/broker-account.dto'; import { GraphRepository } from '../persistence/interfaces/graph.repository'; import { CollectionRepository } from '../persistence/interfaces/collection.repository'; +import { BuildRepository } from '../persistence/interfaces/build.repository'; import { ACTION_VALIDATE_TEAM_ADMIN, ACTION_VALIDATE_TEAM_DBA, + INTENTION_SERVICE_INSTANCE_SEARCH_PATHS, } from '../constants'; import { UserDto } from '../persistence/dto/user.dto'; import { UserCollectionService } from '../collection/user-collection.service'; import { PersistenceUtilService } from '../persistence/persistence-util.service'; import { PackageInstallationActionDto } from './dto/package-installation-action.dto'; +import { PackageBuildActionDto } from './dto/package-build-action.dto'; /** * Assists with the validation of intention actions @@ -24,6 +27,7 @@ import { PackageInstallationActionDto } from './dto/package-installation-action. export class ActionService { constructor( private readonly actionUtil: ActionUtil, + private readonly buildRepository: BuildRepository, private readonly collectionRepository: CollectionRepository, private readonly userCollectionService: UserCollectionService, private readonly graphRepository: GraphRepository, @@ -89,8 +93,14 @@ export class ActionService { requireServiceExists, ) ?? this.validateTargetService(action, targetServices) ?? - (await this.validateDbAction(user, intention, action)) ?? - (await this.validatePackageAction(account, user, intention, action)) ?? + (await this.validateDatabaseAccessAction(user, intention, action)) ?? + (await this.validatePackageBuildAction(account, intention, action)) ?? + (await this.validatePackageInstallAction( + account, + user, + intention, + action, + )) ?? null ); } @@ -203,7 +213,7 @@ export class ActionService { } } - private async validateDbAction( + private async validateDatabaseAccessAction( user: any, intention: IntentionDto, action: ActionDto, @@ -235,39 +245,131 @@ export class ActionService { return null; } - private async validatePackageAction( + private async validatePackageBuildAction( account: BrokerAccountDto | null, - user: any, intention: IntentionDto, action: ActionDto, ): Promise { - if (action instanceof PackageInstallationActionDto) { + if (action instanceof PackageBuildActionDto) { + // TODO: check for existing build + const validateSemverError = this.validateSemver(action); + if (validateSemverError) { + return validateSemverError; + } + + if (!action.package?.name) { + return { + message: 'Package actions must specify a name.', + data: { + action: action.action, + action_id: action.id, + key: 'action.package.name', + value: action.package?.name, + }, + }; + } + + const parsedVersion = this.parseActionVersion(action); + if (parsedVersion.prerelease) { + return null; + } + + const service = action.service.id + ? await this.collectionRepository.getCollectionById( + 'service', + action.service.id.toString(), + ) + : null; + + if (!service) { + return { + message: 'Package service not found.', + data: { + action: action.action, + action_id: action.id, + key: 'action.package.name', + value: action.package?.name, + }, + }; + } + + const existingBuild = await this.buildRepository.getBuildByPackageDetail( + service.id.toString(), + action.package.name, + parsedVersion, + ); + if ( - (account && account.skipUserValidation) || - (await this.persistenceUtil.testAccess( - ['developer', 'lead-developer'], - user.vertex.toString(), - ACTION_VALIDATE_TEAM_ADMIN, - false, - )) + existingBuild && + (action.package?.buildVersion !== existingBuild.package?.buildVersion || + (action.package?.checksum && + action.package?.checksum !== existingBuild.package?.checksum)) ) { - return null; + // console.log(action.package); + // console.log(existingBuild.package); + return { + message: 'Release version may not be altered.', + data: { + action: action.action, + action_id: action.id, + key: 'action.package.version', + value: action.package?.version, + }, + }; } + } + return null; + } + private async validatePackageInstallAction( + account: BrokerAccountDto | null, + user: any, + intention: IntentionDto, + action: ActionDto, + ): Promise { + if (action instanceof PackageInstallationActionDto) { const env = (await this.persistenceUtil.getEnvMap())[ action.service.environment ]; + if (!env) { + return { + message: 'Package installation must specify a valid environment.', + data: { + action: action.action, + action_id: action.id, + key: 'action.service.environment', + value: action.service.environment, + }, + }; + } + + const instanceName = this.actionUtil.instanceName(action); + if (!instanceName) { + return { + message: 'Service instance name could not be extracted from action.', + data: { + action: action.action, + action_id: action.id, + key: INTENTION_SERVICE_INSTANCE_SEARCH_PATHS.join(), + value: 'undefined', + }, + }; + } + const parsedVersion = this.parseActionVersion(action); + const validateSemverError = this.validateSemver(action); + const maskSemverFailures = !!account?.maskSemverFailures; + if (validateSemverError && !maskSemverFailures) { + return validateSemverError; + } if ( - env && - action.package && - action.package.version && - action.package.version.toLowerCase().endsWith('-snapshot') && - env.name === 'production' + env.name === 'production' && + parsedVersion.prerelease && + !maskSemverFailures ) { return { message: - 'Only release versions (no snapshot builds) may be installed in production.', + 'Only release versions may be installed in production. See: https://semver.org/#spec-item-9', data: { action: action.action, action_id: action.id, @@ -277,11 +379,45 @@ export class ActionService { }; } + // Test if user is admin and skip delivery validation if they are + if ( + (account && account.skipUserValidation) || + (await this.persistenceUtil.testAccess( + ['developer', 'lead-developer'], + user.vertex.toString(), + ACTION_VALIDATE_TEAM_ADMIN, + false, + )) + ) { + return null; + } + return this.validateAssistedDelivery(user, intention, action); } return null; } + private parseActionVersion(action: ActionDto) { + return this.actionUtil.parseVersion(action.package?.version ?? ''); + } + + private validateSemver(action: ActionDto): ActionError | null { + const parsedVersion = this.parseActionVersion(action); + if (!this.actionUtil.isStrictSemver(parsedVersion)) { + return { + message: + 'Package actions must specify a valid semver version. See: https://semver.org', + data: { + action: action.action, + action_id: action.id, + key: 'action.package.version', + value: action.package?.version, + }, + }; + } + return null; + } + private async validateAssistedDelivery( user: any, intention: IntentionDto, diff --git a/src/intention/dto/action-patch-rest.dto.ts b/src/intention/dto/action-patch-rest.dto.ts new file mode 100644 index 00000000..37c889b8 --- /dev/null +++ b/src/intention/dto/action-patch-rest.dto.ts @@ -0,0 +1,16 @@ +import { Type } from 'class-transformer'; +import { IsOptional, ValidateNested } from 'class-validator'; +import { CloudDto } from './cloud.dto'; +import { PackageDto } from './package.dto'; + +export class ActionPatchRestDto { + @ValidateNested() + @IsOptional() + @Type(() => CloudDto) + cloud?: CloudDto; + + @ValidateNested() + @IsOptional() + @Type(() => PackageDto) + package?: PackageDto; +} diff --git a/src/intention/dto/artifact.dto.ts b/src/intention/dto/artifact.dto.ts index 3b7acd7c..8dbf2035 100644 --- a/src/intention/dto/artifact.dto.ts +++ b/src/intention/dto/artifact.dto.ts @@ -31,10 +31,10 @@ export function IsValidHash(validationOptions?: ValidationOptions) { @Entity() export class ArtifactDto { @Column() - @IsDefined() + @IsOptional() @IsString() @IsValidHash() - checksum: string; + checksum?: string; @Column() @IsDefined() @@ -47,7 +47,7 @@ export class ArtifactDto { size?: number; @Column() - @IsDefined() + @IsOptional() @IsString() - type: string; + type?: string; } diff --git a/src/intention/dto/cloud-object.dto.ts b/src/intention/dto/cloud-object.dto.ts index ff544f2a..30df2a3f 100644 --- a/src/intention/dto/cloud-object.dto.ts +++ b/src/intention/dto/cloud-object.dto.ts @@ -1,6 +1,7 @@ import { Type } from 'class-transformer'; -import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsIn, IsOptional, IsString, ValidateNested } from 'class-validator'; import { Entity, Column } from 'typeorm'; +import { EdgePropDto } from '../../persistence/dto/edge-prop.dto'; @Entity() class CloudObjectAccountDto { @@ -86,6 +87,17 @@ export class CloudObjectDto { @Type(() => CloudObjectProjectDto) project?: CloudObjectProjectDto; + @IsOptional() + @Column() + @Type(() => EdgePropDto) + prop?: EdgePropDto; + + @IsString() + @IsIn(['merge', 'replace']) + @IsOptional() + @Column() + propStrategy?: 'merge' | 'replace'; + @IsString() @IsOptional() @Column() diff --git a/src/intention/dto/cloud.dto.ts b/src/intention/dto/cloud.dto.ts index 7077d449..f90ea342 100644 --- a/src/intention/dto/cloud.dto.ts +++ b/src/intention/dto/cloud.dto.ts @@ -10,7 +10,7 @@ export class CloudDto { @IsOptional() @Column(() => CloudObjectDto) @Type(() => CloudObjectDto) - source: CloudObjectDto; + source?: CloudObjectDto; @IsDefined() @ValidateNested() diff --git a/src/intention/dto/intention.dto.ts b/src/intention/dto/intention.dto.ts index e9d5607b..7805e6b3 100644 --- a/src/intention/dto/intention.dto.ts +++ b/src/intention/dto/intention.dto.ts @@ -42,6 +42,9 @@ export class IntentionDto { @ObjectIdColumn() @ApiProperty({ type: () => String }) + @Transform((value) => + value.obj.id ? new ObjectId(value.obj.id.toString()) : null, + ) id: ObjectId; @Column() diff --git a/src/intention/dto/package-rest.dto.ts b/src/intention/dto/package-rest.dto.ts new file mode 100644 index 00000000..a91c2e24 --- /dev/null +++ b/src/intention/dto/package-rest.dto.ts @@ -0,0 +1,16 @@ +export class PackageRestDto { + architecture?: string; + buildGuid?: string; + buildNumber?: number; + buildVersion?: string; + checksum?: string; + description?: string; + installScope?: string; + license?: string; + name?: string; + path?: string; + reference?: string; + size?: number; + type?: string; + version?: string; +} diff --git a/src/intention/dto/service.dto.ts b/src/intention/dto/service.dto.ts index f278392e..29658e13 100644 --- a/src/intention/dto/service.dto.ts +++ b/src/intention/dto/service.dto.ts @@ -1,4 +1,4 @@ -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsDefined, IsOptional, @@ -20,6 +20,9 @@ export class ServiceDto { @Column() @IsOptional() @ApiProperty({ type: () => String }) + @Transform((value) => + value.obj.vertex ? new ObjectId(value.obj.vertex.toString()) : null, + ) id?: ObjectId; // Defaults to environment diff --git a/src/intention/intention.controller.ts b/src/intention/intention.controller.ts index 70746b44..5514f564 100644 --- a/src/intention/intention.controller.ts +++ b/src/intention/intention.controller.ts @@ -11,22 +11,22 @@ import { UsePipes, ValidationPipe, } from '@nestjs/common'; -import { ApiBearerAuth, ApiHeader, ApiQuery } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiHeader } from '@nestjs/swagger'; import { Request } from 'express'; import { HEADER_BROKER_TOKEN } from '../constants'; +import { ActionUtil } from '../util/action.util'; import { IntentionDtoValidationPipe } from './intention-dto-validation.pipe'; -import { IntentionDto } from './dto/intention.dto'; import { IntentionService } from './intention.service'; import { BrokerJwtAuthGuard } from '../auth/broker-jwt-auth.guard'; +import { BrokerCombinedAuthGuard } from '../auth/broker-combined-auth.guard'; import { ActionGuardRequest } from './action-guard-request.interface'; import { ActionGuard } from './action.guard'; -import { BrokerCombinedAuthGuard } from '../auth/broker-combined-auth.guard'; import { IntentionSearchQuery } from './dto/intention-search-query.dto'; import { IntentionCloseDto } from './dto/intention-close.dto'; import { ArtifactDto } from './dto/artifact.dto'; import { ArtifactSearchQuery } from './dto/artifact-search-query.dto'; -import { InstallDto } from './dto/install.dto'; -import { ActionUtil } from '../util/action.util'; +import { IntentionDto } from './dto/intention.dto'; +import { ActionPatchRestDto } from './dto/action-patch-rest.dto'; @Controller({ path: 'intention', @@ -128,69 +128,67 @@ export class IntentionController { ); } - @Post('action/install') + @Post('action/artifact') @ApiHeader({ name: HEADER_BROKER_TOKEN, required: true }) - @ApiQuery({ - name: 'strategy', - required: false, - }) @UseGuards(ActionGuard) @UsePipes(new ValidationPipe({ transform: true })) - async actionInstallRegister( + async actionArtifactRegister( @Req() request: ActionGuardRequest, - @Body() install: InstallDto, - @Query('strategy') strategy: string | undefined, + @Body() artifact: ArtifactDto, ) { - if (!['package-installation'].includes(request.brokerActionDto.action)) { + if (!['backup', 'package-build'].includes(request.brokerActionDto.action)) { throw new BadRequestException({ statusCode: 400, message: 'Illegal action', error: - 'Install can only be associated with a package-installation action', - }); - } - if (strategy === undefined) { - strategy = 'merge'; - } - if (strategy !== 'merge' && strategy !== 'replace') { - throw new BadRequestException({ - statusCode: 400, - message: 'Illegal strategy', - error: - 'The strategy parameter must be undefined or be one of merge or replace.', + 'Artifacts can only be attached to backup or package-build actions', }); } - return await this.intentionService.actionInstallRegister( + return await this.intentionService.actionArtifactRegister( request, request.brokerIntentionDto, request.brokerActionDto, - install, - strategy, + artifact, ); } - @Post('action/artifact') + @Post('action/patch') @ApiHeader({ name: HEADER_BROKER_TOKEN, required: true }) @UseGuards(ActionGuard) @UsePipes(new ValidationPipe({ transform: true })) - async actionArtifactRegister( + async actionPackageAnnotate( @Req() request: ActionGuardRequest, - @Body() artifact: ArtifactDto, + @Body() actionPatch: ActionPatchRestDto, ) { - if (!['backup', 'package-build'].includes(request.brokerActionDto.action)) { + if ( + !['package-build', 'package-installation'].includes( + request.brokerActionDto.action, + ) + ) { throw new BadRequestException({ statusCode: 400, message: 'Illegal action', error: - 'Artifacts can only be attached to backup or package-build actions', + 'Only package-build or package-installation actions can be patched', }); } - return await this.intentionService.actionArtifactRegister( - request, - request.brokerIntentionDto, - request.brokerActionDto, - artifact, - ); + + try { + return await this.intentionService.patchAction( + request, + request.brokerIntentionDto, + request.brokerActionDto, + actionPatch, + ); + } catch (e) { + await this.intentionService.close( + request, + request.brokerIntentionDto.transaction.token, + 'failure', + 'Patch failure', + ); + throw e; + } } @Post('artifact-search') diff --git a/src/intention/intention.service.ts b/src/intention/intention.service.ts index c140d065..007df5f4 100644 --- a/src/intention/intention.service.ts +++ b/src/intention/intention.service.ts @@ -1,7 +1,9 @@ import { BadRequestException, + Inject, Injectable, NotFoundException, + forwardRef, } from '@nestjs/common'; import { Request } from 'express'; import { v4 as uuidv4 } from 'uuid'; @@ -10,6 +12,8 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { plainToInstance } from 'class-transformer'; import { ObjectId } from 'mongodb'; import { FindOptionsWhere } from 'typeorm'; +import merge from 'lodash.merge'; + import { IntentionDto } from './dto/intention.dto'; import { INTENTION_DEFAULT_TTL_SECONDS, @@ -42,7 +46,7 @@ import { import { ArtifactSearchQuery } from './dto/artifact-search-query.dto'; import { ActionUtil, FindArtifactActionOptions } from '../util/action.util'; import { CollectionNameEnum } from '../persistence/dto/collection-dto-union.type'; -import { InstallDto } from './dto/install.dto'; +import { ActionPatchRestDto } from './dto/action-patch-rest.dto'; export interface IntentionOpenResponse { actions: { @@ -66,6 +70,7 @@ export class IntentionService { private readonly auditService: AuditService, private readonly actionService: ActionService, private readonly actionUtil: ActionUtil, + @Inject(forwardRef(() => IntentionSyncService)) private readonly intentionSync: IntentionSyncService, private readonly graphRepository: GraphRepository, private readonly collectionRepository: CollectionRepository, @@ -104,12 +109,14 @@ export class IntentionService { ...this.createTokenAndHash(), start: startDate.toISOString(), }; - intentionDto.jwt = plainToInstance(BrokerJwtDto, req.user); intentionDto.expiry = startDate.valueOf() + ttl * 1000; - const registryJwt = await this.systemRepository.getRegisteryJwtByClaimJti( - intentionDto.jwt.jti, - ); - let accountBoundProjects: BrokerAccountProjectMapDto | null = null; + intentionDto.jwt = plainToInstance(BrokerJwtDto, req.user); + const registryJwt = + intentionDto.jwt && intentionDto.jwt.jti + ? await this.systemRepository.getRegisteryJwtByClaimJti( + intentionDto.jwt.jti, + ) + : null; if (registryJwt && registryJwt.blocked) { // JWT should by in block list anyway throw new BadRequestException({ @@ -119,6 +126,7 @@ export class IntentionService { }); } const account = await this.getAccount(registryJwt); + let accountBoundProjects: BrokerAccountProjectMapDto | null = null; intentionDto.requireRoleId = true; if (account) { intentionDto.accountId = registryJwt.accountId; @@ -141,9 +149,7 @@ export class IntentionService { } for (const action of intentionDto.actions) { - const env = action.service.target - ? action.service.target.environment - : action.service.environment; + const env = this.actionUtil.environmentName(action); const envDto = envMap[env]; if ( action.vaultEnvironment === undefined && @@ -420,14 +426,18 @@ export class IntentionService { } private async annotateAction(action: ActionDto) { - if (action.action === 'package-installation' && action.source) { + if ( + action.action === 'package-installation' && + action.source && + action.source.intention + ) { const foundArtifact = await this.artifactSearch( action.source.intention.toString(), action.source.action, null, - action.package.checksum, - action.package.name, - action.package.type, + action.package?.checksum, + action.package?.name, + action.package?.type, action.service.id?.toString(), action.service.name, 0, @@ -443,7 +453,9 @@ export class IntentionService { ...foundArtifact.data[0].artifact, ...action.package, }; - action.source.action = `${foundArtifact.data[0].action.action}#${foundArtifact.data[0].action.id}`; + action.source.action = this.actionUtil.actionToIdString( + foundArtifact.data[0].action, + ); } } @@ -456,6 +468,29 @@ export class IntentionService { const startDate = new Date(intention.transaction.start); for (const action of intention.actions) { if (action.lifecycle === 'started') { + if ( + outcome === 'success' && + action.action === 'package-build' && + action.package && + action.package.name + ) { + // Register build as an artifact + this.actionArtifactRegister( + req, + intention, + action, + { + name: action.package.name, + ...(action.package.checksum + ? { checksum: action.package.checksum } + : {}), + ...(action.package.size ? { size: action.package.size } : {}), + ...(action.package.type ? { type: action.package.type } : {}), + }, + true, + ); + intention = await this.intentionRepository.getIntention(intention.id); + } await this.actionLifecycle(req, intention, action, outcome, 'end'); intention = await this.intentionRepository.getIntention(intention.id); } @@ -545,8 +580,9 @@ export class IntentionService { intention: IntentionDto, action: ActionDto, artifact: ArtifactDto, + ignoreLifecycle = false, ) { - if (action.lifecycle !== 'started') { + if (!ignoreLifecycle && action.lifecycle !== 'started') { throw new BadRequestException({ statusCode: 400, message: 'Illegal artifact register', @@ -575,71 +611,69 @@ export class IntentionService { } /** - * Registers an install with an action + * Patches valid action with additional information * @param req The associated request object * @param intention The intention containing the action - * @param action The action to register the artifact with - * @param install The install to register + * @param action The action to patch + * @param patchAction The patch */ - public async actionInstallRegister( + public async patchAction( req: ActionGuardRequest, intention: IntentionDto, action: ActionDto, - install: InstallDto, - propStrategy: 'merge' | 'replace' = 'merge', + patchAction: ActionPatchRestDto, ) { - const instanceName = this.actionUtil.instanceName(action); - // console.log(instanceName); - // console.log(install); if (action.lifecycle !== 'started') { throw new BadRequestException({ statusCode: 400, - message: 'Illegal install register', - error: `Action's current lifecycle state (${action.lifecycle}) can not register artifacts`, - }); - } - if (!instanceName) { - throw new BadRequestException({ - statusCode: 400, - message: 'No service instance name', - error: 'Service instance name could not be extracted from action.', + message: 'Illegal artifact register', + error: `Action's current lifecycle state (${action.lifecycle}) does not allow patching actions`, }); } - const serviceVertex = await this.graphRepository.getVertexByName( - 'service', - action.service.name, - ); - if (!serviceVertex) { - throw new BadRequestException({ - statusCode: 400, - message: 'Illegal install register', - error: `Service (${action.service.name}) not found`, - }); + // Patch according to action + if (action.action === 'package-build') { + if (patchAction.package) { + action.package = { + ...(action.package ?? {}), + ...patchAction.package, + }; + } else { + throw new BadRequestException({ + statusCode: 400, + message: 'Nothing to patch', + error: 'Must include json body to patch', + }); + } + } else if (action.action === 'package-installation') { + if (patchAction?.cloud?.target) { + if (!action?.cloud) { + action.cloud = { target: {} }; + } + if (!action?.cloud.target) { + action.cloud.target = {}; + } + merge(action.cloud.target, patchAction.cloud.target); + } else { + throw new BadRequestException({ + statusCode: 400, + message: 'Nothing to patch', + error: 'Must include json body to patch', + }); + } } - // Sync package install - await this.intentionSync.syncPackageInstall( - intention, - action, - serviceVertex, + // replace with patched action + intention.actions = intention.actions.map((intentionAction) => + intentionAction.action !== action.action ? intentionAction : action, ); - const serverVertex = await this.intentionSync.syncServer(action, install); - if (!serverVertex) { - throw new BadRequestException({ - statusCode: 400, - message: 'Illegal install register', - error: `Server could not be synced`, - }); - } - - await this.intentionSync.syncInstallationProperties( - serviceVertex, - instanceName, - serverVertex, - propStrategy, - install.prop, + // Ensure this is still a valid intention -- copied as open modifies intention + await this.open( + req, + plainToInstance(IntentionDto, JSON.parse(JSON.stringify(intention))), + INTENTION_DEFAULT_TTL_SECONDS, + true, ); this.auditService.recordIntentionActionUsage( @@ -648,14 +682,15 @@ export class IntentionService { action, { event: { - action: 'install-register', + action: 'package-annotate', category: 'configuration', - type: 'creation', + type: 'change', }, }, null, - undefined, + null, ); + return await this.intentionRepository.addIntention(intention); } /** diff --git a/src/persistence/dto/broker-account-rest.dto.ts b/src/persistence/dto/broker-account-rest.dto.ts index fc1cc926..55d27a2d 100644 --- a/src/persistence/dto/broker-account-rest.dto.ts +++ b/src/persistence/dto/broker-account-rest.dto.ts @@ -1,12 +1,15 @@ // Shared DTO: Copy in back-end and front-end should be identical -export class BrokerAccountRestDto { - id: string; - email: string; - clientId: string; - name: string; - vertex: string; - requireRoleId: boolean; - requireProjectExists: boolean; - requireServiceExists: boolean; +import { VertexPointerRestDto } from './vertex-pointer-rest.dto'; + +export class BrokerAccountRestDto extends VertexPointerRestDto { + id!: string; + email!: string; + clientId!: string; + name!: string; + requireRoleId!: boolean; + requireProjectExists!: boolean; + requireServiceExists!: boolean; + skipUserValidation!: boolean; + maskSemverFailures!: boolean; } diff --git a/src/persistence/dto/broker-account.dto.ts b/src/persistence/dto/broker-account.dto.ts index d4d92d85..78f90893 100644 --- a/src/persistence/dto/broker-account.dto.ts +++ b/src/persistence/dto/broker-account.dto.ts @@ -53,4 +53,9 @@ export class BrokerAccountDto extends VertexPointerDto { @IsBoolean() @Column() skipUserValidation: boolean; + + @IsDefined() + @IsBoolean() + @Column() + maskSemverFailures: boolean; } diff --git a/src/persistence/dto/package-build-rest.dto.ts b/src/persistence/dto/package-build-rest.dto.ts new file mode 100644 index 00000000..240a7fcf --- /dev/null +++ b/src/persistence/dto/package-build-rest.dto.ts @@ -0,0 +1,19 @@ +import { PackageRestDto } from '../../intention/dto/package-rest.dto'; +import { IntentionActionPointerRestDto } from './intention-action-pointer-rest.dto'; +import { TimestampRestDto } from './timestamp-rest.dto'; + +class PackageBuildApprovalRestDto { + environment!: string; + user!: string; + at!: string; +} + +export class PackageBuildRestDto { + id!: string; + approval!: PackageBuildApprovalRestDto[]; + installed!: IntentionActionPointerRestDto[]; + service!: string; + semvar!: string; + package!: PackageRestDto; + timestamps!: TimestampRestDto; +} diff --git a/src/persistence/dto/package-build.dto.ts b/src/persistence/dto/package-build.dto.ts new file mode 100644 index 00000000..5389592b --- /dev/null +++ b/src/persistence/dto/package-build.dto.ts @@ -0,0 +1,79 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Entity, ObjectIdColumn, Column, Index } from 'typeorm'; +import { IsDefined, ValidateNested } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { ObjectId } from 'mongodb'; + +import { PackageDto } from '../../intention/dto/package.dto'; +import { TimestampDto } from './timestamp.dto'; +import { IntentionActionPointerDto } from './intention-action-pointer.dto'; + +@Entity() +class PackageBuildApprovalDto { + @IsDefined() + @Column() + @ApiProperty({ type: () => String }) + @Transform((value) => + value.obj.environment + ? new ObjectId(value.obj.environment.toString()) + : null, + ) + environment: ObjectId; + + @IsDefined() + @Column() + @ApiProperty({ type: () => String }) + @Transform((value) => + value.obj.userId ? new ObjectId(value.obj.userId.toString()) : null, + ) + user: ObjectId; + + @IsDefined() + @Column() + at: Date; +} + +@Entity({ name: 'packageBuild' }) +export class PackageBuildDto { + @ObjectIdColumn() + @ApiHideProperty() + @Transform((value) => + value.obj.id ? new ObjectId(value.obj.id.toString()) : null, + ) + id: ObjectId; + + @IsDefined() + @Column(() => PackageBuildApprovalDto) + @Type(() => PackageBuildApprovalDto) + approval: PackageBuildApprovalDto[]; + + @IsDefined() + @Column(() => IntentionActionPointerDto, { array: true }) + @Type(() => IntentionActionPointerDto) + installed: IntentionActionPointerDto[]; + + @IsDefined() + @Column() + @ApiProperty({ type: () => String }) + @Transform((value) => + value.obj.vertex ? new ObjectId(value.obj.vertex.toString()) : null, + ) + @Index() + service: ObjectId; + + @IsDefined() + @Column() + @Index() + semvar: string; + + @IsDefined() + @ValidateNested() + @Column(() => PackageDto) + @Type(() => PackageDto) + package: PackageDto; + + @IsDefined() + @Column(() => TimestampDto) + @Type(() => TimestampDto) + timestamps: TimestampDto; +} diff --git a/src/persistence/dto/service-rest.dto.ts b/src/persistence/dto/service-rest.dto.ts index 47b2eb8b..c2c0c595 100644 --- a/src/persistence/dto/service-rest.dto.ts +++ b/src/persistence/dto/service-rest.dto.ts @@ -1,3 +1,5 @@ +import { CollectionSearchResult } from '../../collection/dto/collection-search-result.dto'; +import { PackageBuildRestDto } from './package-build-rest.dto'; import { ServiceInstanceDetailsResponseDto } from './service-instance-rest.dto'; import { VaultConfigRestDto } from './vault-config-rest.dto'; import { VertexPointerRestDto } from './vertex-pointer-rest.dto'; @@ -14,4 +16,5 @@ export class ServiceRestDto extends VertexPointerRestDto { export class ServiceDetailsResponseDto extends ServiceRestDto { serviceInstance: ServiceInstanceDetailsResponseDto[]; + builds: CollectionSearchResult; } diff --git a/src/persistence/dto/timestamp-rest.dto.ts b/src/persistence/dto/timestamp-rest.dto.ts new file mode 100644 index 00000000..6a084f6f --- /dev/null +++ b/src/persistence/dto/timestamp-rest.dto.ts @@ -0,0 +1,4 @@ +export class TimestampRestDto { + createdAt!: string; + updatedAt!: string; +} diff --git a/src/persistence/dto/timestamp.dto.ts b/src/persistence/dto/timestamp.dto.ts new file mode 100644 index 00000000..bd39987b --- /dev/null +++ b/src/persistence/dto/timestamp.dto.ts @@ -0,0 +1,11 @@ +import { Column, Index } from 'typeorm'; + +export class TimestampDto { + @Index() + @Column() + createdAt: Date; + + @Index() + @Column() + updatedAt: Date; +} diff --git a/src/persistence/dto/user.dto.ts b/src/persistence/dto/user.dto.ts index 2096f619..0120e013 100644 --- a/src/persistence/dto/user.dto.ts +++ b/src/persistence/dto/user.dto.ts @@ -2,6 +2,7 @@ import { ApiHideProperty } from '@nestjs/swagger'; import { Entity, ObjectIdColumn, ObjectId, Column } from 'typeorm'; import { IsDefined, IsOptional, IsString } from 'class-validator'; import { VertexPointerDto } from './vertex-pointer.dto'; +import { Transform } from 'class-transformer'; export class UserGroupDto { @Column() @@ -18,6 +19,9 @@ export class UserGroupDto { export class UserDto extends VertexPointerDto { @ObjectIdColumn() @ApiHideProperty() + @Transform((value) => + value.obj.id ? new ObjectId(value.obj.id.toString()) : null, + ) id: ObjectId; @IsDefined() diff --git a/src/persistence/interfaces/build.repository.ts b/src/persistence/interfaces/build.repository.ts new file mode 100644 index 00000000..931fe9d9 --- /dev/null +++ b/src/persistence/interfaces/build.repository.ts @@ -0,0 +1,33 @@ +import { CollectionSearchResult } from '../../collection/dto/collection-search-result.dto'; +import { PackageDto } from '../../intention/dto/package.dto'; +import { SemverVersion } from '../../util/action.util'; +import { PackageBuildDto } from '../dto/package-build.dto'; +import { IntentionActionPointerDto } from '../dto/intention-action-pointer.dto'; + +export abstract class BuildRepository { + public abstract addBuild( + serviceId: string, + name: string, + semvar: SemverVersion, + buildPackage: PackageDto, + ): Promise; + + public abstract getBuild(id: string): Promise; + + public abstract getBuildByPackageDetail( + serviceId: string, + name: string, + semvar: SemverVersion, + ): Promise; + + public abstract addInstallActionToBuild( + buildId: string, + pointer: IntentionActionPointerDto, + ): Promise; + + public abstract searchBuild( + serviceId: string, + offset: number, + limit: number, + ): Promise>; +} diff --git a/src/persistence/mongo/build-mongo.repository.ts b/src/persistence/mongo/build-mongo.repository.ts new file mode 100644 index 00000000..a29a0ae8 --- /dev/null +++ b/src/persistence/mongo/build-mongo.repository.ts @@ -0,0 +1,132 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { MongoRepository } from 'typeorm'; +import { ObjectId } from 'mongodb'; +import { PackageDto } from '../../intention/dto/package.dto'; +import { PackageBuildDto } from '../dto/package-build.dto'; +import { SemverVersion } from '../../util/action.util'; +import { COLLECTION_MAX_EMBEDDED } from '../../constants'; +import { IntentionActionPointerDto } from '../dto/intention-action-pointer.dto'; +import { BuildRepository } from '../interfaces/build.repository'; +import { arrayIdFixer } from './mongo.util'; + +@Injectable() +export class BuildMongoRepository implements BuildRepository { + constructor( + @InjectRepository(PackageBuildDto) + private readonly packageBuildRepository: MongoRepository, + ) {} + + public async addBuild( + serviceId: string, + name: string, + semvar: SemverVersion, + buildPackage: PackageDto, + ) { + const result = await this.packageBuildRepository.insertOne({ + approval: [], + installed: [], + service: new ObjectId(serviceId), + name, + semvar: `${semvar.major}.${semvar.minor}.${semvar.patch}`, + package: buildPackage, + timestamps: { + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + if (!result.acknowledged) { + throw new Error(); + } + const rval = await this.getBuild(result.insertedId.toString()); + if (rval === null) { + throw new Error(); + } + + return rval; + } + + public async getBuild(id: string) { + return this.packageBuildRepository.findOne({ + where: { _id: new ObjectId(id) }, + }); + } + + public async getBuildByPackageDetail( + serviceId: string, + name: string, + semvar: SemverVersion, + ) { + return this.packageBuildRepository + .find({ + service: new ObjectId(serviceId), + name, + semvar: `${semvar.major}.${semvar.minor}.${semvar.patch}`, + }) + .then((value) => { + return value.length === 1 ? value[0] : null; + }); + } + + public async addInstallActionToBuild( + buildId: string, + pointer: IntentionActionPointerDto, + ) { + const collResult = await this.packageBuildRepository.updateOne( + { _id: new ObjectId(buildId) }, + { + $set: { + 'timestamps.updatedAt': new Date(), + }, + $push: { + installed: { + $each: [pointer], + $slice: -COLLECTION_MAX_EMBEDDED, + }, + } as any, + }, + ); + if (collResult.matchedCount !== 1) { + throw new Error(); + } + + return this.getBuild(buildId); + } + + public async searchBuild(serviceId: string, offset: number, limit: number) { + return this.packageBuildRepository + .aggregate([ + { + $match: { + service: new ObjectId(serviceId), + }, + }, + { + $sort: { _id: -1 }, + }, + { + $facet: { + data: [ + { $sort: { name: 1 } }, + { $skip: offset }, + { $limit: limit }, + ], + meta: [{ $count: 'total' }], + }, + }, + { $unwind: '$meta' }, + ]) + .toArray() + .then((array) => { + if (array[0]) { + arrayIdFixer((array[0] as any).data); + return array[0] as any; + } else { + return { + data: [], + meta: { total: 0 }, + }; + } + }); + } +} diff --git a/src/persistence/mongo/collection-mongo.repository.ts b/src/persistence/mongo/collection-mongo.repository.ts index 32fcb8e5..ee3f541e 100644 --- a/src/persistence/mongo/collection-mongo.repository.ts +++ b/src/persistence/mongo/collection-mongo.repository.ts @@ -10,8 +10,9 @@ import { ObjectId } from 'mongodb'; import { CollectionRepository } from '../interfaces/collection.repository'; import { CollectionDtoUnion } from '../dto/collection-dto-union.type'; import { CollectionConfigDto } from '../dto/collection-config.dto'; -import { getRepositoryFromCollectionName } from './mongo.util'; +import { arrayIdFixer, getRepositoryFromCollectionName } from './mongo.util'; import { CollectionSearchResult } from '../../collection/dto/collection-search-result.dto'; +import { PackageBuildDto } from '../dto/package-build.dto'; @Injectable() export class CollectionMongoRepository implements CollectionRepository { @@ -19,6 +20,8 @@ export class CollectionMongoRepository implements CollectionRepository { private readonly dataSource: DataSource, @InjectRepository(CollectionConfigDto) private readonly collectionConfigRepository: MongoRepository, + @InjectRepository(PackageBuildDto) + private readonly packageBuildRepository: MongoRepository, ) {} public getCollectionConfigs(): Promise { @@ -194,10 +197,10 @@ export class CollectionMongoRepository implements CollectionRepository { >; for (const datum of rval.data) { - this.arrayIdFixer(datum.downstream); - this.arrayIdFixer(datum.downstream_edge); - this.arrayIdFixer(datum.upstream); - this.arrayIdFixer(datum.upstream_edge); + arrayIdFixer(datum.downstream); + arrayIdFixer(datum.downstream_edge); + arrayIdFixer(datum.upstream); + arrayIdFixer(datum.upstream_edge); } return rval; } else { @@ -214,27 +217,17 @@ export class CollectionMongoRepository implements CollectionRepository { return repo.find(); } - public doUniqueKeyCheck( + public async doUniqueKeyCheck( type: keyof CollectionDtoUnion, key: string, value: string, ): Promise { const repo = getRepositoryFromCollectionName(this.dataSource, type); - return repo - .find({ - where: { - [key]: value, - }, - }) - .then((array) => { - return array.map((val) => val.id.toString()); - }); - } - - private arrayIdFixer(array: any[]) { - for (const item of array) { - item.id = item._id; - delete item._id; - } + const array = await repo.find({ + where: { + [key]: value, + }, + }); + return array.map((val) => val.id.toString()); } } diff --git a/src/persistence/mongo/graph-mongo.repository.ts b/src/persistence/mongo/graph-mongo.repository.ts index c16dbdfa..b54ab542 100644 --- a/src/persistence/mongo/graph-mongo.repository.ts +++ b/src/persistence/mongo/graph-mongo.repository.ts @@ -9,7 +9,7 @@ import { CollectionConfigDto, CollectionConfigInstanceDto, } from '../dto/collection-config.dto'; -import { getRepositoryFromCollectionName } from './mongo.util'; +import { arrayIdFixer, getRepositoryFromCollectionName } from './mongo.util'; import { BrokerAccountProjectMapDto, GraphDataResponseDto, @@ -165,7 +165,7 @@ export class GraphMongoRepository implements GraphRepository { ]) .toArray() .then((array: any) => { - this.arrayIdFixer(array); + arrayIdFixer(array); for (const datum of array) { if (datum.services) { @@ -234,7 +234,7 @@ export class GraphMongoRepository implements GraphRepository { ]) .toArray() .then((array: any) => { - this.arrayIdFixer(array); + arrayIdFixer(array); for (const datum of array) { if (datum.instances) { @@ -297,10 +297,10 @@ export class GraphMongoRepository implements GraphRepository { ]) .toArray() .then((array: any) => { - this.arrayIdFixer(array); + arrayIdFixer(array); const datum = array.length > 0 ? array[0] : null; if (datum) { - this.arrayIdFixer(datum.serviceInstance); + arrayIdFixer(datum.serviceInstance); for (const instance of datum.serviceInstance) { if (instance.environment) { @@ -593,7 +593,7 @@ export class GraphMongoRepository implements GraphRepository { ]) .toArray() .then((array: any) => { - this.arrayIdFixer(array); + arrayIdFixer(array); for (const datum of array) { if (datum.instance) { @@ -721,7 +721,6 @@ export class GraphMongoRepository implements GraphRepository { collection: CollectionDtoUnion[typeof vertex.collection], ignoreBlankFields = false, ): Promise { - console.log(collection); const curVertex = await this.getVertex(id); if (curVertex === null) { throw new Error(); @@ -769,10 +768,6 @@ export class GraphMongoRepository implements GraphRepository { } } - console.log(collection); - console.log(pushFields); - console.log(unsetFields); - // Update collection const collResult = await repository.updateOne( { vertex: new ObjectId(id) }, @@ -1089,11 +1084,4 @@ export class GraphMongoRepository implements GraphRepository { public reindexCache(): Promise { throw new Error('Method not implemented.'); } - - private arrayIdFixer(array: any[]) { - for (const item of array) { - item.id = item._id; - delete item._id; - } - } } diff --git a/src/persistence/mongo/intention-mongo.repository.ts b/src/persistence/mongo/intention-mongo.repository.ts index fbb580b2..c0f065b8 100644 --- a/src/persistence/mongo/intention-mongo.repository.ts +++ b/src/persistence/mongo/intention-mongo.repository.ts @@ -17,6 +17,11 @@ export class IntentionMongoRepository implements IntentionRepository { ) {} public async addIntention(intention: IntentionDto): Promise { + if (intention.id) { + const id = extractId(intention); + await this.intentionRepository.replaceOne({ _id: id }, intention); + return intention; + } return await this.intentionRepository.save(intention); } diff --git a/src/persistence/mongo/mongo.util.ts b/src/persistence/mongo/mongo.util.ts index fc1e50e2..334b766e 100644 --- a/src/persistence/mongo/mongo.util.ts +++ b/src/persistence/mongo/mongo.util.ts @@ -70,3 +70,10 @@ export function extractId(obj: any): ObjectId { delete obj.id; return id; } + +export function arrayIdFixer(array: any[]) { + for (const item of array) { + item.id = item._id; + delete item._id; + } +} diff --git a/src/persistence/persistence.module.ts b/src/persistence/persistence.module.ts index 486e55a0..b5ba7ee6 100644 --- a/src/persistence/persistence.module.ts +++ b/src/persistence/persistence.module.ts @@ -10,6 +10,7 @@ import { IntentionDto } from '../intention/dto/intention.dto'; import { JwtAllowDto } from './dto/jwt-allow.dto'; import { JwtBlockDto } from './dto/jwt-block.dto'; import { JwtRegistryDto } from './dto/jwt-registry.dto'; +import { PackageBuildDto } from './dto/package-build.dto'; import { PreferenceDto } from './dto/preference.dto'; import { ProjectDto } from './dto/project.dto'; import { ServerDto } from './dto/server.dto'; @@ -19,16 +20,21 @@ import { TeamDto } from './dto/team.dto'; import { UserDto } from './dto/user.dto'; import { VertexDto } from './dto/vertex.dto'; +import { BuildRepository } from './interfaces/build.repository'; +import { CollectionRepository } from './interfaces/collection.repository'; import { GraphRepository } from './interfaces/graph.repository'; import { IntentionRepository } from './interfaces/intention.repository'; +import { SystemRepository } from './interfaces/system.repository'; + +import { BuildMongoRepository } from './mongo/build-mongo.repository'; +import { CollectionMongoRepository } from './mongo/collection-mongo.repository'; import { GraphMongoRepository } from './mongo/graph-mongo.repository'; import { IntentionMongoRepository } from './mongo/intention-mongo.repository'; -import { CollectionMongoRepository } from './mongo/collection-mongo.repository'; -import { CollectionRepository } from './interfaces/collection.repository'; import { SystemMongoRepository } from './mongo/system-mongo.repository'; -import { SystemRepository } from './interfaces/system.repository'; -import { PersistenceUtilService } from './persistence-util.service'; + import { GraphRedisRepository } from './redis-composition/graph-redis.repository'; + +import { PersistenceUtilService } from './persistence-util.service'; import { PersistenceRedisUtilService } from './persistence-redis-util.service'; import { UtilModule } from '../util/util.module'; @@ -77,6 +83,7 @@ const redisFactory = { JwtRegistryDto, ServiceDto, ServiceInstanceDto, + PackageBuildDto, PreferenceDto, ProjectDto, ServerDto, @@ -87,6 +94,11 @@ const redisFactory = { UtilModule, ], providers: [ + BuildMongoRepository, + { + provide: BuildRepository, + useExisting: BuildMongoRepository, + }, CollectionMongoRepository, { provide: CollectionRepository, @@ -110,6 +122,7 @@ const redisFactory = { redisFactory, ], exports: [ + BuildRepository, CollectionRepository, GraphRepository, IntentionRepository, diff --git a/src/util/action.util.spec.ts b/src/util/action.util.spec.ts index d5ef0e3a..8cd0909e 100644 --- a/src/util/action.util.spec.ts +++ b/src/util/action.util.spec.ts @@ -77,4 +77,14 @@ describe('ActionUtil', () => { } as ActionDto), ).toBe(true); }); + + it('parseVersion to parse a correct version number', () => { + expect(util.parseVersion('2.1.3-snapshot+build46')).toEqual({ + major: '2', + minor: '1', + patch: '3', + prerelease: 'snapshot', + build: 'build46', + }); + }); }); diff --git a/src/util/action.util.ts b/src/util/action.util.ts index 85f6c2bf..4a635ac2 100644 --- a/src/util/action.util.ts +++ b/src/util/action.util.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { get } from 'radash'; import ejs from 'ejs'; import { + INTENTION_SERVICE_ENVIRONMENT_SEARCH_PATHS, INTENTION_SERVICE_INSTANCE_SEARCH_PATHS, VAULT_ENVIRONMENTS, VAULT_PROVISIONED_ACTION_SET, @@ -13,11 +14,21 @@ export type FindArtifactActionOptions = Partial< Pick >; +export interface SemverVersion { + major: string | undefined; + minor: string | undefined; + patch: string | undefined; + prerelease: string | undefined; + build: string | undefined; +} + @Injectable() export class ActionUtil { private readonly AUDIT_URL_TEMPLATE = process.env.AUDIT_URL_TEMPLATE ? process.env.AUDIT_URL_TEMPLATE : ''; + private readonly VERSION_REGEX = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; public resolveVaultEnvironment(action: ActionDto): string | undefined { return ( @@ -83,6 +94,19 @@ export class ActionUtil { }); } + public actionToIdString(action: ActionDto) { + return `${action.action}#${action.id}`; + } + + public environmentName(action: ActionDto) { + return INTENTION_SERVICE_ENVIRONMENT_SEARCH_PATHS.reduce( + (pv, path) => { + return get({ action }, path, pv); + }, + undefined, + ); + } + public instanceName(action: ActionDto) { return INTENTION_SERVICE_INSTANCE_SEARCH_PATHS.reduce( (pv, path) => { @@ -100,4 +124,26 @@ export class ActionUtil { public auditUrlForIntention(intention: IntentionDto): string { return ejs.render(this.AUDIT_URL_TEMPLATE, { intention }); } + + public parseVersion(version: string): SemverVersion | null { + const val = this.VERSION_REGEX.exec(version); + return val + ? { + major: val[1], + minor: val[2], + patch: val[3], + prerelease: val[4], + build: val[5], + } + : null; + } + + public isStrictSemver(parsedVersion: SemverVersion | null) { + return ( + parsedVersion && + parsedVersion.major !== undefined && + parsedVersion.minor !== undefined && + parsedVersion.patch !== undefined + ); + } } diff --git a/ui/package-lock.json b/ui/package-lock.json index ddeee76e..62f3fc1a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,16 +9,16 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { - "@angular/animations": "^17.3.5", - "@angular/cdk": "^17.3.5", - "@angular/common": "^17.3.5", - "@angular/compiler": "^17.3.5", - "@angular/core": "^17.3.5", - "@angular/forms": "^17.3.5", - "@angular/material": "^17.3.5", - "@angular/platform-browser": "^17.3.5", - "@angular/platform-browser-dynamic": "^17.3.5", - "@angular/router": "^17.3.5", + "@angular/animations": "^17.3.6", + "@angular/cdk": "^17.3.6", + "@angular/common": "^17.3.6", + "@angular/compiler": "^17.3.6", + "@angular/core": "^17.3.6", + "@angular/forms": "^17.3.6", + "@angular/material": "^17.3.6", + "@angular/platform-browser": "^17.3.6", + "@angular/platform-browser-dynamic": "^17.3.6", + "@angular/router": "^17.3.6", "@bcgov/bc-sans": "^2.1.0", "class-validator": "^0.14.1", "echarts": "^5.5.0", @@ -35,7 +35,7 @@ "devDependencies": { "@angular-devkit/build-angular": "^17.3.5", "@angular/cli": "^17.3.5", - "@angular/compiler-cli": "^17.3.5", + "@angular/compiler-cli": "^17.3.6", "@types/jasmine": "^5.1.4", "@types/url-regex-safe": "^1.0.2", "@types/uuid": "^9.0.8", @@ -372,9 +372,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.5.tgz", - "integrity": "sha512-hbfCnBxwhYQMKB+9tDcmfvckUtB8LdY1gPST6TZ7CzrWCSPddsnXxqxBZSBjBI6zXvE4FOV3kUzaUXM/Bq5sRw==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.6.tgz", + "integrity": "sha512-ev99cnmc1S/SXYz9OwOyZQyHXHiUf+ZwQFpjYBRPoyKqZV4sOYMlyBbfjBO/GgCVrsGfMvBsCI6PtY3yquuabA==", "dependencies": { "tslib": "^2.3.0" }, @@ -382,13 +382,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.5" + "@angular/core": "17.3.6" } }, "node_modules/@angular/cdk": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.5.tgz", - "integrity": "sha512-6y8+yIPWG0wTdPwHIPxKrEFCX1JxxBh4aXcmQnrNTDIvtoEPGaea9SU9XKaU8ahiZMlcpUXqKLG0BVbEhA1Oow==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.6.tgz", + "integrity": "sha512-7eKrC61/6pmMAxllU/vYKadZRF7x7GxUYpA5G70fNaQsIUUiZvxx/SJN9AuZEoPGAtF6atKlJD8QVmFoDzv/Lw==", "dependencies": { "tslib": "^2.3.0" }, @@ -481,9 +481,9 @@ "dev": true }, "node_modules/@angular/common": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.5.tgz", - "integrity": "sha512-Ox91WxSnOSrQ6I21cHi69EfT2Pxtd5Knb5AsdwpxqE57V2E7EnWMhb+LP+holCtFUhK529EGXCk788M+Elyw6g==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.6.tgz", + "integrity": "sha512-ufviCFzQQKWcwc2j3Zi8bHbwkvqh4QU6GDH0u0usOee8xd8KrjgcYl3vD0r1/yxlDsd53Wg9kNRvz/fY+5qQoQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -491,14 +491,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.5", + "@angular/core": "17.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.5.tgz", - "integrity": "sha512-lTubBFNlpH9zK46+yeVI7VJQNUELLAB8W1ucndYLCA9Rr9Jop+rYIXijmr42AGokOYr7yLc8HRiSQ5e+X2pUQg==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.6.tgz", + "integrity": "sha512-ybx9O76RGv4J97IThiSVvvWukuGcuXu50KsBDPUd874BFT3ml0OcRGhXoMh/isz7EQipiiGgsA51cJVTLES5Zw==", "dependencies": { "tslib": "^2.3.0" }, @@ -506,7 +506,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.5" + "@angular/core": "17.3.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -515,9 +515,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.5.tgz", - "integrity": "sha512-R53JNbbVDHWSGdL0e2vGQ5iJCrILOWZ1oemKjekOFB93fUBlEyi+nZmm4uTO7RU8PgjB0UpxI6ok5ZE3Amkt6A==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.6.tgz", + "integrity": "sha512-LaoUkY6uzcNocIEHJBvexvuU0a333IRQaG3Sj5IXhM1t864wTsfycn6yWJcQ7PhklB8BtNqiMbUQuEFtkxT8pg==", "dev": true, "dependencies": { "@babel/core": "7.23.9", @@ -538,14 +538,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.3.5", + "@angular/compiler": "17.3.6", "typescript": ">=5.2 <5.5" } }, "node_modules/@angular/core": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.5.tgz", - "integrity": "sha512-y6P27lcrKy3yMx/rtMuGsAnDyVEsS3BdyArTXcD0TOImVGHhVIaB0L95DUCam3ajTe2f2x39eozJZDh7QSpJaw==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.6.tgz", + "integrity": "sha512-8IoeZVNqyeHA+H2dR3VFfz76/TFN1BpXP0aABs2aIUNVQRYlKxALSm1UlavijX8IT0uvd/6GXwE3WgymTcg0wg==", "dependencies": { "tslib": "^2.3.0" }, @@ -558,9 +558,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.5.tgz", - "integrity": "sha512-Rf/8XWHdFYZQaOVTJ0QVwxQm9fDqQqIJc0yfPcH/DYL5pT7R0U2z98I5McZawzUBJUo1Zt1gijzDlzNUGf6jiA==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.6.tgz", + "integrity": "sha512-WXxWhwvgRfYLNP2dB4Qe83tavEh2LnS4H0uoiecWHXijW2R9z8304X1vEyS1EtQK7o/s8fCVDVDjeY+hxLnCLw==", "dependencies": { "tslib": "^2.3.0" }, @@ -568,16 +568,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.5", - "@angular/core": "17.3.5", - "@angular/platform-browser": "17.3.5", + "@angular/common": "17.3.6", + "@angular/core": "17.3.6", + "@angular/platform-browser": "17.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.5.tgz", - "integrity": "sha512-1+QqBQ8HVOwxOkx/v2n53JA9ALOee55yVDbnAv7TkseNN4JEDxOcE5TO5HGmdV2A4tcsXQ00MIdy04jiB4sCng==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.6.tgz", + "integrity": "sha512-sttN0JNvd2QvCCFIsxb5noiy7tgQdWrwvmrkJ+3KguHh5X84jDliA/d8N7Xgy2IBLnS/q/Hl9DdRCOiItWG1bw==", "dependencies": { "@material/animation": "15.0.0-canary.7f224ddd4.0", "@material/auto-init": "15.0.0-canary.7f224ddd4.0", @@ -630,7 +630,7 @@ }, "peerDependencies": { "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.5", + "@angular/cdk": "17.3.6", "@angular/common": "^17.0.0 || ^18.0.0", "@angular/core": "^17.0.0 || ^18.0.0", "@angular/forms": "^17.0.0 || ^18.0.0", @@ -639,9 +639,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.5.tgz", - "integrity": "sha512-ITlu/GTD64Sr0FMaFCJiHoTJrEZw8qRFXjPjv3BKhAp5dQKcwnCm02o1NOaj5d8oIItIh5fbI2zP0CSU2qNZkQ==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.6.tgz", + "integrity": "sha512-UikrgvMwtZIXp2pCP5AtkM7ibz2B5wBiGpnhhkYsqHKy9ndKVDA+3B5Z+/j9xeYYdsJAAtHl45zqILewyg+4iw==", "dependencies": { "tslib": "^2.3.0" }, @@ -649,9 +649,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.3.5", - "@angular/common": "17.3.5", - "@angular/core": "17.3.5" + "@angular/animations": "17.3.6", + "@angular/common": "17.3.6", + "@angular/core": "17.3.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -660,9 +660,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.5.tgz", - "integrity": "sha512-KuS4j3Gh1h/CEj+bIOc/IcZIdiCB/DNbtUvz1eNp1o23aM8QutqelI3A4WBnQuR4yq8Z/8M3FH9F1OVwwhn2QQ==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.6.tgz", + "integrity": "sha512-dI+mgEROmSll042+XqkSsvkMQe6Et6L9BBiYYe7VbIFaRR9Dz5Pw2SeBLb+Ou+gWaxXc2Wc+13n442WEYWZ7Ew==", "dependencies": { "tslib": "^2.3.0" }, @@ -670,16 +670,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.5", - "@angular/compiler": "17.3.5", - "@angular/core": "17.3.5", - "@angular/platform-browser": "17.3.5" + "@angular/common": "17.3.6", + "@angular/compiler": "17.3.6", + "@angular/core": "17.3.6", + "@angular/platform-browser": "17.3.6" } }, "node_modules/@angular/router": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.5.tgz", - "integrity": "sha512-KsIIs3t9IpxsdMSrJDZzO5WgIWkVE6Ep5WWiSyPIgEfA+ndGpJLmyv0d/r1yKKlYUJxz7Hde55o4thgT2n2x/A==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.6.tgz", + "integrity": "sha512-Gws3zukTlPO5lIGP0bmWBkmbRIRKvpPq6vs3BqQlbKsrfBh45SPvIRbx+BSv6WYUchQzfW7DFDXnQtiTEGGQNg==", "dependencies": { "tslib": "^2.3.0" }, @@ -687,9 +687,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.5", - "@angular/core": "17.3.5", - "@angular/platform-browser": "17.3.5", + "@angular/common": "17.3.6", + "@angular/core": "17.3.6", + "@angular/platform-browser": "17.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -13549,17 +13549,17 @@ } }, "@angular/animations": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.5.tgz", - "integrity": "sha512-hbfCnBxwhYQMKB+9tDcmfvckUtB8LdY1gPST6TZ7CzrWCSPddsnXxqxBZSBjBI6zXvE4FOV3kUzaUXM/Bq5sRw==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.6.tgz", + "integrity": "sha512-ev99cnmc1S/SXYz9OwOyZQyHXHiUf+ZwQFpjYBRPoyKqZV4sOYMlyBbfjBO/GgCVrsGfMvBsCI6PtY3yquuabA==", "requires": { "tslib": "^2.3.0" } }, "@angular/cdk": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.5.tgz", - "integrity": "sha512-6y8+yIPWG0wTdPwHIPxKrEFCX1JxxBh4aXcmQnrNTDIvtoEPGaea9SU9XKaU8ahiZMlcpUXqKLG0BVbEhA1Oow==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.6.tgz", + "integrity": "sha512-7eKrC61/6pmMAxllU/vYKadZRF7x7GxUYpA5G70fNaQsIUUiZvxx/SJN9AuZEoPGAtF6atKlJD8QVmFoDzv/Lw==", "requires": { "parse5": "^7.1.2", "tslib": "^2.3.0" @@ -13629,25 +13629,25 @@ } }, "@angular/common": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.5.tgz", - "integrity": "sha512-Ox91WxSnOSrQ6I21cHi69EfT2Pxtd5Knb5AsdwpxqE57V2E7EnWMhb+LP+holCtFUhK529EGXCk788M+Elyw6g==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.6.tgz", + "integrity": "sha512-ufviCFzQQKWcwc2j3Zi8bHbwkvqh4QU6GDH0u0usOee8xd8KrjgcYl3vD0r1/yxlDsd53Wg9kNRvz/fY+5qQoQ==", "requires": { "tslib": "^2.3.0" } }, "@angular/compiler": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.5.tgz", - "integrity": "sha512-lTubBFNlpH9zK46+yeVI7VJQNUELLAB8W1ucndYLCA9Rr9Jop+rYIXijmr42AGokOYr7yLc8HRiSQ5e+X2pUQg==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.6.tgz", + "integrity": "sha512-ybx9O76RGv4J97IThiSVvvWukuGcuXu50KsBDPUd874BFT3ml0OcRGhXoMh/isz7EQipiiGgsA51cJVTLES5Zw==", "requires": { "tslib": "^2.3.0" } }, "@angular/compiler-cli": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.5.tgz", - "integrity": "sha512-R53JNbbVDHWSGdL0e2vGQ5iJCrILOWZ1oemKjekOFB93fUBlEyi+nZmm4uTO7RU8PgjB0UpxI6ok5ZE3Amkt6A==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.6.tgz", + "integrity": "sha512-LaoUkY6uzcNocIEHJBvexvuU0a333IRQaG3Sj5IXhM1t864wTsfycn6yWJcQ7PhklB8BtNqiMbUQuEFtkxT8pg==", "dev": true, "requires": { "@babel/core": "7.23.9", @@ -13661,25 +13661,25 @@ } }, "@angular/core": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.5.tgz", - "integrity": "sha512-y6P27lcrKy3yMx/rtMuGsAnDyVEsS3BdyArTXcD0TOImVGHhVIaB0L95DUCam3ajTe2f2x39eozJZDh7QSpJaw==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.6.tgz", + "integrity": "sha512-8IoeZVNqyeHA+H2dR3VFfz76/TFN1BpXP0aABs2aIUNVQRYlKxALSm1UlavijX8IT0uvd/6GXwE3WgymTcg0wg==", "requires": { "tslib": "^2.3.0" } }, "@angular/forms": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.5.tgz", - "integrity": "sha512-Rf/8XWHdFYZQaOVTJ0QVwxQm9fDqQqIJc0yfPcH/DYL5pT7R0U2z98I5McZawzUBJUo1Zt1gijzDlzNUGf6jiA==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.6.tgz", + "integrity": "sha512-WXxWhwvgRfYLNP2dB4Qe83tavEh2LnS4H0uoiecWHXijW2R9z8304X1vEyS1EtQK7o/s8fCVDVDjeY+hxLnCLw==", "requires": { "tslib": "^2.3.0" } }, "@angular/material": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.5.tgz", - "integrity": "sha512-1+QqBQ8HVOwxOkx/v2n53JA9ALOee55yVDbnAv7TkseNN4JEDxOcE5TO5HGmdV2A4tcsXQ00MIdy04jiB4sCng==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.6.tgz", + "integrity": "sha512-sttN0JNvd2QvCCFIsxb5noiy7tgQdWrwvmrkJ+3KguHh5X84jDliA/d8N7Xgy2IBLnS/q/Hl9DdRCOiItWG1bw==", "requires": { "@material/animation": "15.0.0-canary.7f224ddd4.0", "@material/auto-init": "15.0.0-canary.7f224ddd4.0", @@ -13732,25 +13732,25 @@ } }, "@angular/platform-browser": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.5.tgz", - "integrity": "sha512-ITlu/GTD64Sr0FMaFCJiHoTJrEZw8qRFXjPjv3BKhAp5dQKcwnCm02o1NOaj5d8oIItIh5fbI2zP0CSU2qNZkQ==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.6.tgz", + "integrity": "sha512-UikrgvMwtZIXp2pCP5AtkM7ibz2B5wBiGpnhhkYsqHKy9ndKVDA+3B5Z+/j9xeYYdsJAAtHl45zqILewyg+4iw==", "requires": { "tslib": "^2.3.0" } }, "@angular/platform-browser-dynamic": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.5.tgz", - "integrity": "sha512-KuS4j3Gh1h/CEj+bIOc/IcZIdiCB/DNbtUvz1eNp1o23aM8QutqelI3A4WBnQuR4yq8Z/8M3FH9F1OVwwhn2QQ==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.6.tgz", + "integrity": "sha512-dI+mgEROmSll042+XqkSsvkMQe6Et6L9BBiYYe7VbIFaRR9Dz5Pw2SeBLb+Ou+gWaxXc2Wc+13n442WEYWZ7Ew==", "requires": { "tslib": "^2.3.0" } }, "@angular/router": { - "version": "17.3.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.5.tgz", - "integrity": "sha512-KsIIs3t9IpxsdMSrJDZzO5WgIWkVE6Ep5WWiSyPIgEfA+ndGpJLmyv0d/r1yKKlYUJxz7Hde55o4thgT2n2x/A==", + "version": "17.3.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.6.tgz", + "integrity": "sha512-Gws3zukTlPO5lIGP0bmWBkmbRIRKvpPq6vs3BqQlbKsrfBh45SPvIRbx+BSv6WYUchQzfW7DFDXnQtiTEGGQNg==", "requires": { "tslib": "^2.3.0" } diff --git a/ui/package.json b/ui/package.json index d56d8a1e..1584233f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,16 +12,16 @@ "private": true, "license": "Apache-2.0", "dependencies": { - "@angular/animations": "^17.3.5", - "@angular/cdk": "^17.3.5", - "@angular/common": "^17.3.5", - "@angular/compiler": "^17.3.5", - "@angular/core": "^17.3.5", - "@angular/forms": "^17.3.5", - "@angular/material": "^17.3.5", - "@angular/platform-browser": "^17.3.5", - "@angular/platform-browser-dynamic": "^17.3.5", - "@angular/router": "^17.3.5", + "@angular/animations": "^17.3.6", + "@angular/cdk": "^17.3.6", + "@angular/common": "^17.3.6", + "@angular/compiler": "^17.3.6", + "@angular/core": "^17.3.6", + "@angular/forms": "^17.3.6", + "@angular/material": "^17.3.6", + "@angular/platform-browser": "^17.3.6", + "@angular/platform-browser-dynamic": "^17.3.6", + "@angular/router": "^17.3.6", "@bcgov/bc-sans": "^2.1.0", "class-validator": "^0.14.1", "echarts": "^5.5.0", @@ -38,7 +38,7 @@ "devDependencies": { "@angular-devkit/build-angular": "^17.3.5", "@angular/cli": "^17.3.5", - "@angular/compiler-cli": "^17.3.5", + "@angular/compiler-cli": "^17.3.6", "@types/jasmine": "^5.1.4", "@types/url-regex-safe": "^1.0.2", "@types/uuid": "^9.0.8", diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 2f457937..b688f4c1 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -4,15 +4,35 @@ - + home
Home
- + account_tree
Graph
- + + table_view +
Browse
+
+ groups
Teams
diff --git a/ui/src/app/graph/inspector-instances/inspector-instances.component.html b/ui/src/app/graph/inspector-instances/inspector-instances.component.html index f71e0a4e..a5718417 100644 --- a/ui/src/app/graph/inspector-instances/inspector-instances.component.html +++ b/ui/src/app/graph/inspector-instances/inspector-instances.component.html @@ -101,3 +101,10 @@

{{ element.env.title }}

class="group-row"> + + + +@if (builds) { + +} \ No newline at end of file diff --git a/ui/src/app/graph/inspector-instances/inspector-instances.component.ts b/ui/src/app/graph/inspector-instances/inspector-instances.component.ts index af652e5b..1dd14706 100644 --- a/ui/src/app/graph/inspector-instances/inspector-instances.component.ts +++ b/ui/src/app/graph/inspector-instances/inspector-instances.component.ts @@ -31,6 +31,7 @@ import { } from '@angular/animations'; import { InspectorInstallsComponent } from '../inspector-installs/inspector-installs.component'; import { OutcomeIconComponent } from '../../shared/outcome-icon/outcome-icon.component'; +import { InspectorReleasesComponent } from '../inspector-releases/inspector-releases.component'; @Component({ selector: 'app-inspector-instances', @@ -42,6 +43,7 @@ import { OutcomeIconComponent } from '../../shared/outcome-icon/outcome-icon.com MatTableModule, InspectorInstallsComponent, InspectorInstanceDialogComponent, + InspectorReleasesComponent, OutcomeIconComponent, ], templateUrl: './inspector-instances.component.html', @@ -61,6 +63,7 @@ export class InspectorInstancesComponent implements OnChanges { @Input() vertices!: VertexNavigation | null; @Input() service!: ServiceRestDto; data: any; + builds: any; tableData: any[] = []; environments: any[] = []; @Output() refreshData = new EventEmitter(); @@ -97,6 +100,7 @@ export class InspectorInstancesComponent implements OnChanges { this.collectionApi .getServiceDetails(this.service.id) .subscribe((data: any) => { + this.builds = data.builds; this.data = data.serviceInstance.reduce((pv: any, cv: any) => { const env = cv.environment.name; if (pv[env]) { diff --git a/ui/src/app/graph/inspector-intentions/inspector-intentions.component.spec.ts b/ui/src/app/graph/inspector-intentions/inspector-intentions.component.spec.ts index ad764c50..8dfcafb8 100644 --- a/ui/src/app/graph/inspector-intentions/inspector-intentions.component.spec.ts +++ b/ui/src/app/graph/inspector-intentions/inspector-intentions.component.spec.ts @@ -8,7 +8,7 @@ describe('InspectorIntentionsComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [InspectorIntentionsComponent] + imports: [InspectorIntentionsComponent], }); fixture = TestBed.createComponent(InspectorIntentionsComponent); component = fixture.componentInstance; diff --git a/ui/src/app/graph/inspector-releases/inspector-releases.component.html b/ui/src/app/graph/inspector-releases/inspector-releases.component.html new file mode 100644 index 00000000..cb904629 --- /dev/null +++ b/ui/src/app/graph/inspector-releases/inspector-releases.component.html @@ -0,0 +1,50 @@ +
+

Recent Releases

+
+@if (builds.meta.total == 0) { +
None
+} @else { + + + + + + + + + + + + + + + + + + + + + + + + + +
Name {{element.name}} + Version {{element.semvar}} + Date {{element.timestamps.createdAt}} +
+ +} +@if (builds.meta.total > 0) { +
+
{{builds.meta.total}} Total
+ +
+} + diff --git a/ui/src/app/graph/inspector-releases/inspector-releases.component.scss b/ui/src/app/graph/inspector-releases/inspector-releases.component.scss new file mode 100644 index 00000000..281deb22 --- /dev/null +++ b/ui/src/app/graph/inspector-releases/inspector-releases.component.scss @@ -0,0 +1,21 @@ + +.edge-container { + margin-left: 16px; +} + +.heading { + margin: 8px 0; +} + +.more-panel { + display: flex; + justify-content: space-between; + align-items: center; + margin-left: 16px; + margin-bottom: 16px; + margin-top: 16px; +} + +.intention-none { + margin-bottom: 16px; +} \ No newline at end of file diff --git a/ui/src/app/graph/inspector-releases/inspector-releases.component.spec.ts b/ui/src/app/graph/inspector-releases/inspector-releases.component.spec.ts new file mode 100644 index 00000000..afaa7763 --- /dev/null +++ b/ui/src/app/graph/inspector-releases/inspector-releases.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InspectorReleasesComponent } from './inspector-releases.component'; + +describe('InspectorReleasesComponent', () => { + let component: InspectorReleasesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InspectorReleasesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InspectorReleasesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/graph/inspector-releases/inspector-releases.component.ts b/ui/src/app/graph/inspector-releases/inspector-releases.component.ts new file mode 100644 index 00000000..ee0e65ad --- /dev/null +++ b/ui/src/app/graph/inspector-releases/inspector-releases.component.ts @@ -0,0 +1,27 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatTableModule } from '@angular/material/table'; + +@Component({ + selector: 'app-inspector-releases', + standalone: true, + imports: [CommonModule, MatButtonModule, MatDividerModule, MatTableModule], + templateUrl: './inspector-releases.component.html', + styleUrl: './inspector-releases.component.scss', +}) +export class InspectorReleasesComponent { + @Input() builds!: any; + total = 0; + + propDisplayedColumns: string[] = ['name', 'version']; + + navigateByService() { + console.log('navigateByService'); + } + + // private loadBuilds() { + // this.buildApi.searchBuilds(this.id, 0, 5); + // } +} diff --git a/ui/src/app/intention/history-table/history-table.component.html b/ui/src/app/intention/history-table/history-table.component.html index 02e5ac37..af26ed3c 100644 --- a/ui/src/app/intention/history-table/history-table.component.html +++ b/ui/src/app/intention/history-table/history-table.component.html @@ -101,7 +101,9 @@ - View + View @@ -246,6 +248,10 @@

Details

+ @if (element.event.url) { { + let service: BuildApiService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BuildApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/ui/src/app/service/build-api.service.ts b/ui/src/app/service/build-api.service.ts new file mode 100644 index 00000000..a451253b --- /dev/null +++ b/ui/src/app/service/build-api.service.ts @@ -0,0 +1,19 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class BuildApiService { + constructor(private readonly http: HttpClient) {} + + searchBuilds(id: string, offset = 0, limit = 5) { + return this.http.post( + `${environment.apiUrl}/v1/build/search?id=${id}&offset=${offset}&limit=${limit}`, + { + responseType: 'json', + }, + ); + } +} diff --git a/ui/src/app/service/dto/broker-account-rest.dto.ts b/ui/src/app/service/dto/broker-account-rest.dto.ts index fe8780f2..55d27a2d 100644 --- a/ui/src/app/service/dto/broker-account-rest.dto.ts +++ b/ui/src/app/service/dto/broker-account-rest.dto.ts @@ -1,13 +1,15 @@ // Shared DTO: Copy in back-end and front-end should be identical -export interface BrokerAccountRestDto { - id: string; - email: string; - clientId: string; - name: string; - vertex: string; - requireRoleId: boolean; - requireProjectExists: boolean; - requireServiceExists: boolean; - tags: string[]; +import { VertexPointerRestDto } from './vertex-pointer-rest.dto'; + +export class BrokerAccountRestDto extends VertexPointerRestDto { + id!: string; + email!: string; + clientId!: string; + name!: string; + requireRoleId!: boolean; + requireProjectExists!: boolean; + requireServiceExists!: boolean; + skipUserValidation!: boolean; + maskSemverFailures!: boolean; }