diff --git a/.github/workflows/cloud-runner-local-pipeline.yml b/.github/workflows/cloud-runner-local-pipeline.yml new file mode 100644 index 0000000..f44198b --- /dev/null +++ b/.github/workflows/cloud-runner-local-pipeline.yml @@ -0,0 +1,95 @@ +name: Cloud Runner Local + +on: + push: { branches: ['!cloud-runner-develop', '!cloud-runner-preview', '!main'] } +# push: { branches: [main] } +# pull_request: +# paths-ignore: +# - '.github/**' + +jobs: + integrationTests: + name: Integration Tests + if: github.event.event_type != 'pull_request_target' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cloudRunnerCluster: + - local-docker + targetPlatform: + - StandaloneWindows64 # Build a Windows 64-bit standalone. + # steps + steps: + - name: Checkout (default) + uses: actions/checkout@v2 + with: + lfs: true + - run: yarn + - run: yarn run cli --help + - run: yarn run test-i --detectOpenHandles --forceExit --runInBand + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + PROJECT_PATH: ${{ matrix.projectPath }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET_PLATFORM: ${{ matrix.targetPlatform }} + cloudRunnerTests: true + versioning: None + CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + buildTests: + name: Build Tests + if: github.event.event_type != 'pull_request_target' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cloudRunnerCluster: + - local-docker + targetPlatform: + - StandaloneOSX # Build a macOS standalone (Intel 64-bit). + - StandaloneWindows64 # Build a Windows 64-bit standalone. + - StandaloneLinux64 # Build a Linux 64-bit standalone. + - WebGL # WebGL. + - iOS # Build an iOS player. + - Android # Build an Android .apk. + # - StandaloneWindows # Build a Windows standalone. + # - WSAPlayer # Build an Windows Store Apps player. + # - PS4 # Build a PS4 Standalone. + # - XboxOne # Build a Xbox One Standalone. + # - tvOS # Build to Apple's tvOS platform. + # - Switch # Build a Nintendo Switch player + # steps + steps: + - name: Checkout (default) + uses: actions/checkout@v2 + with: + lfs: true + - uses: ./ + id: unity-build + timeout-minutes: 25 + env: + CLOUD_RUNNER_BRANCH: ${{ github.ref }} + CLOUD_RUNNER_DEBUG: true + CLOUD_RUNNER_DEBUG_TREE: true + DEBUG: true + PROJECT_PATH: test-project + UNITY_VERSION: 2019.3.15f1 + USE_IL2CPP: false + with: + cloudRunnerTests: true + versioning: None + projectPath: ${{ matrix.projectPath }} + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} + targetPlatform: ${{ matrix.targetPlatform }} + cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} + - run: | + mv ./cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + ls + ########################### + # Upload # + ########################### + - uses: actions/upload-artifact@v2 + with: + name: Local Build (${{ matrix.targetPlatform }}) + path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + retention-days: 14 diff --git a/.github/workflows/cloud-runner-pipeline.yml b/.github/workflows/cloud-runner-pipeline.yml new file mode 100644 index 0000000..4f3b667 --- /dev/null +++ b/.github/workflows/cloud-runner-pipeline.yml @@ -0,0 +1,211 @@ +name: Cloud Runner CI Pipeline + +on: + push: { branches: [cloud-runner-develop, cloud-runner-preview, main] } + +env: + GKE_ZONE: 'us-central1' + GKE_REGION: 'us-central1' + GKE_PROJECT: 'unitykubernetesbuilder' + GKE_CLUSTER: 'game-ci-github-pipelines' + GCP_LOGGING: true + GCP_PROJECT: unitykubernetesbuilder + GCP_LOG_FILE: ${{ github.workspace }}/cloud-runner-logs.txt + AWS_REGION: eu-west-2 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: eu-west-2 + AWS_BASE_STACK_NAME: game-ci-github-pipelines + CLOUD_RUNNER_BRANCH: ${{ github.ref }} + CLOUD_RUNNER_DEBUG: true + CLOUD_RUNNER_DEBUG_TREE: true + DEBUG: true + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + PROJECT_PATH: test-project + UNITY_VERSION: 2019.3.15f1 + USE_IL2CPP: false + +jobs: + integrationTests: + name: Integration Tests + if: github.event.event_type != 'pull_request_target' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cloudRunnerCluster: + - aws + - local-docker + - k8s + steps: + - name: Checkout (default) + uses: actions/checkout@v2 + with: + lfs: true + - uses: google-github-actions/setup-gcloud@v0 + with: + version: '288.0.0' + service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} + service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + - name: Get GKE cluster credentials + run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-2 + - run: yarn + - run: yarn run cli --help + - run: yarn run test "cloud-runner-run-twice-retaining" --detectOpenHandles --forceExit --runInBand + if: matrix.CloudRunnerCluster == 'aws' || matrix.CloudRunnerCluster == 'k8s' + timeout-minutes: 180 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + PROJECT_PATH: test-project + GIT_PRIVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET_PLATFORM: StandaloneWindows64 + cloudRunnerTests: true + versioning: None + CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + - run: yarn run test-i --detectOpenHandles --forceExit --runInBand + if: matrix.CloudRunnerCluster == 'local-docker' + timeout-minutes: 180 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + PROJECT_PATH: test-project + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET_PLATFORM: StandaloneWindows64 + cloudRunnerTests: true + versioning: None + CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + + buildTargetTests: + name: Build Tests - Targets + if: github.event.event_type != 'pull_request_target' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cloudRunnerCluster: + #- aws + - local-docker + #- k8s + targetPlatform: + - StandaloneOSX # Build a macOS standalone (Intel 64-bit). + # - StandaloneWindows64 # Build a Windows 64-bit standalone. + - StandaloneLinux64 # Build a Linux 64-bit standalone. + - WebGL # WebGL. + - iOS # Build an iOS player. + - Android # Build an Android .apk. + steps: + - name: Checkout (default) + uses: actions/checkout@v2 + with: + lfs: true + + - uses: google-github-actions/setup-gcloud@v0 + with: + version: '288.0.0' + service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} + service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + - name: Get GKE cluster credentials + run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-2 + - run: yarn + - uses: ./ + id: unity-build + timeout-minutes: 90 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + with: + cloudRunnerTests: true + versioning: None + projectPath: test-project + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} + targetPlatform: ${{ matrix.targetPlatform }} + cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} + customStepFiles: aws-s3-upload-build,aws-s3-pull-cache,aws-s3-upload-cache + - run: | + aws s3 cp s3://game-ci-test-storage/cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + ls + - run: yarn run cli -m list-resources + env: + cloudRunnerTests: true + CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + - uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }}) + path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + retention-days: 14 + buildTests: + name: Build Tests - Providers + if: github.event.event_type != 'pull_request_target' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cloudRunnerCluster: + - aws + - local-docker + - k8s + targetPlatform: + #- StandaloneOSX # Build a macOS standalone (Intel 64-bit). + - StandaloneWindows64 # Build a Windows 64-bit standalone. + #- StandaloneLinux64 # Build a Linux 64-bit standalone. + #- WebGL # WebGL. + #- iOS # Build an iOS player. + #- Android # Build an Android .apk. + # steps + steps: + - name: Checkout (default) + uses: actions/checkout@v2 + with: + lfs: true + + - uses: google-github-actions/setup-gcloud@v0 + with: + version: '288.0.0' + service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} + service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + - name: Get GKE cluster credentials + run: gcloud container clusters get-credentials $GKE_CLUSTER --zone $GKE_ZONE --project $GKE_PROJECT + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-2 + - run: yarn + - uses: ./ + id: unity-build + timeout-minutes: 90 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + with: + cloudRunnerTests: true + versioning: None + projectPath: test-project + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} + targetPlatform: ${{ matrix.targetPlatform }} + cloudRunnerCluster: ${{ matrix.cloudRunnerCluster }} + customStepFiles: aws-s3-upload-build,aws-s3-pull-cache,aws-s3-upload-cache + - run: | + aws s3 cp s3://game-ci-test-storage/cloud-runner-cache/${{ steps.unity-build.outputs.CACHE_KEY }}/build/build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + ls + - run: yarn run cli -m list-resources + if: always() + env: + cloudRunnerTests: true + CLOUD_RUNNER_CLUSTER: ${{ matrix.cloudRunnerCluster }} + - uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.cloudRunnerCluster }} Build (${{ matrix.targetPlatform }}) + path: build-${{ steps.unity-build.outputs.BUILD_GUID }}.tar.lz4 + retention-days: 14 diff --git a/src/model/cloud-runner/cloud-runner-options.ts b/src/model/cloud-runner/cloud-runner-options.ts new file mode 100644 index 0000000..b8e3fa2 --- /dev/null +++ b/src/model/cloud-runner/cloud-runner-options.ts @@ -0,0 +1,265 @@ +import { Cli } from '../cli/cli'; +import CloudRunnerQueryOverride from './services/cloud-runner-query-override'; +import GitHub from '../github'; +const core = require('@actions/core'); + +class CloudRunnerOptions { + // ### ### ### + // Input Handling + // ### ### ### + public static getInput(query) { + if (GitHub.githubInputEnabled) { + const coreInput = core.getInput(query); + if (coreInput && coreInput !== '') { + return coreInput; + } + } + const alternativeQuery = CloudRunnerOptions.ToEnvVarFormat(query); + + // Query input sources + if (Cli.query(query, alternativeQuery)) { + return Cli.query(query, alternativeQuery); + } + + if (CloudRunnerQueryOverride.query(query, alternativeQuery)) { + return CloudRunnerQueryOverride.query(query, alternativeQuery); + } + + if (process.env[query] !== undefined) { + return process.env[query]; + } + + if (alternativeQuery !== query && process.env[alternativeQuery] !== undefined) { + return process.env[alternativeQuery]; + } + + return; + } + + public static ToEnvVarFormat(input: string) { + if (input.toUpperCase() === input) { + return input; + } + + return input + .replace(/([A-Z])/g, ' $1') + .trim() + .toUpperCase() + .replace(/ /g, '_'); + } + + // ### ### ### + // Provider parameters + // ### ### ### + + static get region(): string { + return CloudRunnerOptions.getInput('region') || 'eu-west-2'; + } + + // ### ### ### + // Git syncronization parameters + // ### ### ### + + static get githubRepo() { + return CloudRunnerOptions.getInput('GITHUB_REPOSITORY') || CloudRunnerOptions.getInput('GITHUB_REPO') || undefined; + } + static get branch() { + if (CloudRunnerOptions.getInput(`GITHUB_REF`)) { + return CloudRunnerOptions.getInput(`GITHUB_REF`).replace('refs/', '').replace(`head/`, '').replace(`heads/`, ''); + } else if (CloudRunnerOptions.getInput('branch')) { + return CloudRunnerOptions.getInput('branch'); + } else { + return ''; + } + } + + static get gitSha() { + if (CloudRunnerOptions.getInput(`GITHUB_SHA`)) { + return CloudRunnerOptions.getInput(`GITHUB_SHA`); + } else if (CloudRunnerOptions.getInput(`GitSHA`)) { + return CloudRunnerOptions.getInput(`GitSHA`); + } + } + + // ### ### ### + // Cloud Runner parameters + // ### ### ### + + static get cloudRunnerBuilderPlatform() { + const input = CloudRunnerOptions.getInput('cloudRunnerBuilderPlatform'); + if (input) { + return input; + } + if (CloudRunnerOptions.cloudRunnerCluster !== 'local') { + return 'linux'; + } + + return; + } + + static get cloudRunnerBranch() { + return CloudRunnerOptions.getInput('cloudRunnerBranch') || 'cloud-runner-develop'; + } + + static get cloudRunnerCluster() { + if (Cli.isCliMode) { + return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'aws'; + } + + return CloudRunnerOptions.getInput('cloudRunnerCluster') || 'local'; + } + + static get cloudRunnerCpu() { + return CloudRunnerOptions.getInput('cloudRunnerCpu'); + } + + static get cloudRunnerMemory() { + return CloudRunnerOptions.getInput('cloudRunnerMemory'); + } + + static get customJob() { + return CloudRunnerOptions.getInput('customJob') || ''; + } + + // ### ### ### + // Custom commands from files parameters + // ### ### ### + + static get customStepFiles() { + return CloudRunnerOptions.getInput('customStepFiles')?.split(`,`) || []; + } + + static get customHookFiles() { + return CloudRunnerOptions.getInput('customHookFiles')?.split(`,`) || []; + } + + // ### ### ### + // Custom commands from yaml parameters + // ### ### ### + + static customJobHooks() { + return CloudRunnerOptions.getInput('customJobHooks') || ''; + } + + static get postBuildSteps() { + return CloudRunnerOptions.getInput('postBuildSteps') || ''; + } + + static get preBuildSteps() { + return CloudRunnerOptions.getInput('preBuildSteps') || ''; + } + + // ### ### ### + // Input override handling + // ### ### ### + + static readInputFromOverrideList() { + return CloudRunnerOptions.getInput('readInputFromOverrideList') || ''; + } + + static readInputOverrideCommand() { + const value = CloudRunnerOptions.getInput('readInputOverrideCommand'); + + if (value === 'gcp-secret-manager') { + return 'gcloud secrets versions access 1 --secret="{0}"'; + } else if (value === 'aws-secret-manager') { + return 'aws secretsmanager get-secret-value --secret-id {0}'; + } + + return value || ''; + } + + // ### ### ### + // Aws + // ### ### ### + + static get awsBaseStackName() { + return CloudRunnerOptions.getInput('awsBaseStackName') || 'game-ci'; + } + + // ### ### ### + // K8s + // ### ### ### + + static get kubeConfig() { + return CloudRunnerOptions.getInput('kubeConfig') || ''; + } + + static get kubeVolume() { + return CloudRunnerOptions.getInput('kubeVolume') || ''; + } + + static get kubeVolumeSize() { + return CloudRunnerOptions.getInput('kubeVolumeSize') || '5Gi'; + } + + static get kubeStorageClass(): string { + return CloudRunnerOptions.getInput('kubeStorageClass') || ''; + } + + // ### ### ### + // Caching + // ### ### ### + + static get cacheKey(): string { + return CloudRunnerOptions.getInput('cacheKey') || CloudRunnerOptions.branch; + } + + // ### ### ### + // Utility Parameters + // ### ### ### + + static get cloudRunnerDebug(): boolean { + return CloudRunnerOptions.getInput(`cloudRunnerTests`) || CloudRunnerOptions.getInput(`cloudRunnerDebug`) || false; + } + static get cloudRunnerDebugTree(): boolean { + return CloudRunnerOptions.getInput(`cloudRunnerDebugTree`) || false; + } + static get cloudRunnerDebugEnv(): boolean { + return CloudRunnerOptions.getInput(`cloudRunnerDebugEnv`) || false; + } + + static get watchCloudRunnerToEnd(): boolean { + const input = CloudRunnerOptions.getInput(`watchToEnd`); + + return input !== 'false'; + } + + public static get useSharedLargePackages(): boolean { + return CloudRunnerOptions.getInput(`useSharedLargePackages`) || false; + } + + public static get useSharedBuilder(): boolean { + return CloudRunnerOptions.getInput(`useSharedBuilder`) || true; + } + + public static get useLz4Compression(): boolean { + return CloudRunnerOptions.getInput(`useLz4Compression`) || true; + } + + // ### ### ### + // Retained Workspace + // ### ### ### + + public static get retainWorkspaces(): boolean { + return CloudRunnerOptions.getInput(`retainWorkspaces`) || false; + } + + static get maxRetainedWorkspaces(): number { + return Number(CloudRunnerOptions.getInput(`maxRetainedWorkspaces`)) || 3; + } + + // ### ### ### + // Garbage Collection + // ### ### ### + + static get constantGarbageCollection(): boolean { + return CloudRunnerOptions.getInput(`constantGarbageCollection`) || true; + } + + static get garbageCollectionMaxAge(): number { + return Number(CloudRunnerOptions.getInput(`garbageCollectionMaxAge`)) || 24; + } +} + +export default CloudRunnerOptions; diff --git a/src/model/cloud-runner/cloud-runner-step-state.ts b/src/model/cloud-runner/cloud-runner-step-state.ts index 6028821..94f744b 100644 --- a/src/model/cloud-runner/cloud-runner-step-state.ts +++ b/src/model/cloud-runner/cloud-runner-step-state.ts @@ -1,5 +1,5 @@ -import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable.ts'; -import CloudRunnerSecret from './services/cloud-runner-secret.ts'; +import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable'; +import CloudRunnerSecret from './services/cloud-runner-secret'; export class CloudRunnerStepState { public image: string; diff --git a/src/model/cloud-runner/cloud-runner.test.ts b/src/model/cloud-runner/cloud-runner.test.ts deleted file mode 100644 index 38de9ce..0000000 --- a/src/model/cloud-runner/cloud-runner.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Parameters, RunnerImageTag } from '..'; -import CloudRunner from './cloud-runner.ts'; -import Input from '../input.ts'; -import { CloudRunnerStatics } from './cloud-runner-statics.ts'; -import { TaskParameterSerializer } from './services/task-parameter-serializer.ts'; -import UnityVersionDetector from '../../middleware/engine-detection/unity-version-detector.ts'; -import { Cli } from '../cli/cli.ts'; -import CloudRunnerLogger from './services/cloud-runner-logger.ts'; -import { v4 as uuidv4 } from '../../../node_modules/uuid'; - -describe('Cloud Runner', () => { - it('responds', () => {}); -}); -describe('Cloud Runner', () => { - const testSecretName = 'testSecretName'; - const testSecretValue = 'testSecretValue'; - if (Input.cloudRunnerTests) { - it('All build parameters sent to cloud runner as env vars', async () => { - // Build parameters - Cli.options = { - versioning: 'None', - projectPath: 'test-project', - engineVersion: UnityVersionDetector.read('test-project'), - targetPlatform: 'StandaloneLinux64', - customJob: ` - - name: 'step 1' - image: 'alpine' - commands: 'printenv' - secrets: - - name: '${testSecretName}' - value: '${testSecretValue}' - `, - }; - Input.githubInputEnabled = false; - - // Setup parameters - const buildParameter = await Parameters.create(); - Input.githubInputEnabled = true; - const baseImage = new RunnerImageTag(buildParameter); - - // Run the job - const file = await CloudRunner.run(buildParameter, baseImage.toString()); - - // Assert results - expect(file).toContain(JSON.stringify(buildParameter)); - expect(file).toContain(`${Input.toEnvVarFormat(testSecretName)}=${testSecretValue}`); - const environmentVariables = TaskParameterSerializer.readBuildEnvironmentVariables(); - const newLinePurgedFile = file - .replace(/\s+/g, '') - .replace(new RegExp(`\\[${CloudRunnerStatics.logPrefix}\\]`, 'g'), ''); - for (const element of environmentVariables) { - if (element.value !== undefined && typeof element.value !== 'function') { - if (typeof element.value === `string`) { - element.value = element.value.replace(/\s+/g, ''); - } - CloudRunnerLogger.log(`checking input/build param ${element.name} ${element.value}`); - } - } - for (const element of environmentVariables) { - if (element.value !== undefined && typeof element.value !== 'function') { - expect(newLinePurgedFile).toContain(`${element.name}`); - expect(newLinePurgedFile).toContain(`${element.name}=${element.value}`); - } - } - delete Cli.options; - }, 1_000_000); - it('Run one build it should not use cache, run subsequent build which should use cache', async () => { - Cli.options = { - versioning: 'None', - projectPath: 'test-project', - engineVersion: UnityVersionDetector.determineUnityVersion('test-project', UnityVersionDetector.read('test-project')), - targetPlatform: 'StandaloneLinux64', - cacheKey: `test-case-${uuidv4()}`, - }; - Input.githubInputEnabled = false; - const buildParameter = await Parameters.create(); - const baseImage = new RunnerImageTag(buildParameter); - const results = await CloudRunner.run(buildParameter, baseImage.toString()); - const libraryString = 'Rebuilding Library because the asset database could not be found!'; - const buildSucceededString = 'Build succeeded'; - expect(results).toContain(libraryString); - expect(results).toContain(buildSucceededString); - CloudRunnerLogger.log(`run 1 succeeded`); - const buildParameter2 = await Parameters.create(); - const baseImage2 = new RunnerImageTag(buildParameter2); - const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); - CloudRunnerLogger.log(`run 2 succeeded`); - expect(results2).toContain(buildSucceededString); - expect(results2).toEqual(expect.not.stringContaining(libraryString)); - Input.githubInputEnabled = true; - delete Cli.options; - }, 1_000_000); - } - it('Local cloud runner returns commands', async () => { - // Build parameters - Cli.options = { - versioning: 'None', - projectPath: 'test-project', - engineVersion: UnityVersionDetector.read('test-project'), - cloudRunnerCluster: 'local-system', - targetPlatform: 'StandaloneLinux64', - customJob: ` - - name: 'step 1' - image: 'alpine' - commands: 'dir' - secrets: - - name: '${testSecretName}' - value: '${testSecretValue}' - `, - }; - Input.githubInputEnabled = false; - - // Setup parameters - const buildParameter = await Parameters.create(); - const baseImage = new RunnerImageTag(buildParameter); - - // Run the job - await expect(CloudRunner.run(buildParameter, baseImage.toString())).resolves.not.toThrow(); - Input.githubInputEnabled = true; - delete Cli.options; - }, 1_000_000); - it('Test cloud runner returns commands', async () => { - // Build parameters - Cli.options = { - versioning: 'None', - projectPath: 'test-project', - engineVersion: UnityVersionDetector.read('test-project'), - cloudRunnerCluster: 'test', - targetPlatform: 'StandaloneLinux64', - }; - Input.githubInputEnabled = false; - - // Setup parameters - const buildParameter = await Parameters.create(); - const baseImage = new RunnerImageTag(buildParameter); - - // Run the job - await expect(CloudRunner.run(buildParameter, baseImage.toString())).resolves.not.toThrow(); - Input.githubInputEnabled = true; - delete Cli.options; - }, 1_000_000); -}); diff --git a/src/model/cloud-runner/cloud-runner.ts b/src/model/cloud-runner/cloud-runner.ts index 260f725..d2916fc 100644 --- a/src/model/cloud-runner/cloud-runner.ts +++ b/src/model/cloud-runner/cloud-runner.ts @@ -1,42 +1,50 @@ -import AwsBuildPlatform from './providers/aws/index.ts'; -import { Parameters, Input } from '../index.ts'; -import Kubernetes from './providers/k8s/index.ts'; -import CloudRunnerLogger from './services/cloud-runner-logger.ts'; -import { CloudRunnerStepState } from './cloud-runner-step-state.ts'; -import { WorkflowCompositionRoot } from './workflows/workflow-composition-root.ts'; -import { CloudRunnerError } from './error/cloud-runner-error.ts'; -import { TaskParameterSerializer } from './services/task-parameter-serializer.ts'; -import { core } from '../../dependencies.ts'; -import CloudRunnerSecret from './services/cloud-runner-secret.ts'; -import { ProviderInterface } from './providers/provider-interface.ts'; -import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable.ts'; -import TestCloudRunner from './providers/test/index.ts'; -import LocalCloudRunner from './providers/local/index.ts'; -import LocalDockerCloudRunner from './providers/local-docker/index.ts'; +import AwsBuildPlatform from './providers/aws'; +import { BuildParameters, Input } from '..'; +import Kubernetes from './providers/k8s'; +import CloudRunnerLogger from './services/cloud-runner-logger'; +import { CloudRunnerStepState } from './cloud-runner-step-state'; +import { WorkflowCompositionRoot } from './workflows/workflow-composition-root'; +import { CloudRunnerError } from './error/cloud-runner-error'; +import { TaskParameterSerializer } from './services/task-parameter-serializer'; +import * as core from '@actions/core'; +import CloudRunnerSecret from './services/cloud-runner-secret'; +import { ProviderInterface } from './providers/provider-interface'; +import CloudRunnerEnvironmentVariable from './services/cloud-runner-environment-variable'; +import TestCloudRunner from './providers/test'; +import LocalCloudRunner from './providers/local'; +import LocalDockerCloudRunner from './providers/docker'; +import GitHub from '../github'; +import SharedWorkspaceLocking from './services/shared-workspace-locking'; class CloudRunner { public static Provider: ProviderInterface; - static buildParameters: Parameters; - public static defaultSecrets: CloudRunnerSecret[]; - public static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[]; - private static setup(buildParameters: Parameters) { + public static buildParameters: BuildParameters; + private static defaultSecrets: CloudRunnerSecret[]; + private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[]; + static lockedWorkspace: string | undefined; + public static readonly retainedWorkspacePrefix: string = `retained-workspace`; + public static setup(buildParameters: BuildParameters) { CloudRunnerLogger.setup(); + CloudRunnerLogger.log(`Setting up cloud runner`); CloudRunner.buildParameters = buildParameters; - CloudRunner.setupBuildPlatform(); + CloudRunner.setupSelectedBuildPlatform(); CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets(); - CloudRunner.cloudRunnerEnvironmentVariables = TaskParameterSerializer.readBuildEnvironmentVariables(); - if (!buildParameters.isCliMode) { + CloudRunner.cloudRunnerEnvironmentVariables = + TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters); + if (GitHub.githubInputEnabled) { const buildParameterPropertyNames = Object.getOwnPropertyNames(buildParameters); for (const element of CloudRunner.cloudRunnerEnvironmentVariables) { - core.setOutput(Input.toEnvVarFormat(element.name), element.value); + // CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element.name)} = ${element.value}`); + core.setOutput(Input.ToEnvVarFormat(element.name), element.value); } for (const element of buildParameterPropertyNames) { - core.setOutput(Input.toEnvVarFormat(element), buildParameters[element]); + // CloudRunnerLogger.log(`Cloud Runner output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`); + core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]); } } } - private static setupBuildPlatform() { + private static setupSelectedBuildPlatform() { CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.cloudRunnerCluster}`); switch (CloudRunner.buildParameters.cloudRunnerCluster) { case 'k8s': @@ -48,20 +56,41 @@ class CloudRunner { case 'test': CloudRunner.Provider = new TestCloudRunner(); break; - case 'local-system': - CloudRunner.Provider = new LocalCloudRunner(); - break; case 'local-docker': CloudRunner.Provider = new LocalDockerCloudRunner(); break; + case 'local-system': + CloudRunner.Provider = new LocalCloudRunner(); + break; } } - static async run(buildParameters: Parameters, baseImage: string) { + static async run(buildParameters: BuildParameters, baseImage: string) { CloudRunner.setup(buildParameters); try { + if (buildParameters.retainWorkspace) { + CloudRunner.lockedWorkspace = `${CloudRunner.retainedWorkspacePrefix}-${CloudRunner.buildParameters.buildGuid}`; + + const result = await SharedWorkspaceLocking.GetOrCreateLockedWorkspace( + CloudRunner.lockedWorkspace, + CloudRunner.buildParameters.buildGuid, + CloudRunner.buildParameters, + ); + + if (result) { + CloudRunnerLogger.logLine(`Using retained workspace ${CloudRunner.lockedWorkspace}`); + CloudRunner.cloudRunnerEnvironmentVariables = [ + ...CloudRunner.cloudRunnerEnvironmentVariables, + { name: `LOCKED_WORKSPACE`, value: CloudRunner.lockedWorkspace }, + ]; + } else { + CloudRunnerLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`); + buildParameters.retainWorkspace = false; + CloudRunner.lockedWorkspace = undefined; + } + } if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Setup shared cloud runner resources'); - await CloudRunner.Provider.setup( + await CloudRunner.Provider.setupWorkflow( CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters, CloudRunner.buildParameters.branch, @@ -72,7 +101,7 @@ class CloudRunner { new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets), ); if (!CloudRunner.buildParameters.isCliMode) core.startGroup('Cleanup shared cloud runner resources'); - await CloudRunner.Provider.cleanup( + await CloudRunner.Provider.cleanupWorkflow( CloudRunner.buildParameters.buildGuid, CloudRunner.buildParameters, CloudRunner.buildParameters.branch, @@ -81,10 +110,23 @@ class CloudRunner { CloudRunnerLogger.log(`Cleanup complete`); if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + if (CloudRunner.buildParameters.retainWorkspace) { + await SharedWorkspaceLocking.ReleaseWorkspace( + CloudRunner.lockedWorkspace || ``, + CloudRunner.buildParameters.buildGuid, + CloudRunner.buildParameters, + ); + CloudRunner.lockedWorkspace = undefined; + } + + if (buildParameters.constantGarbageCollection) { + CloudRunner.Provider.garbageCollect(``, true, buildParameters.garbageCollectionMaxAge, true, true); + } + return output; } catch (error) { if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); - await CloudRunnerError.handleException(error); + await CloudRunnerError.handleException(error, CloudRunner.buildParameters, CloudRunner.defaultSecrets); throw error; } } diff --git a/src/model/cloud-runner/error/cloud-runner-error.ts b/src/model/cloud-runner/error/cloud-runner-error.ts index 73a9810..e9b838b 100644 --- a/src/model/cloud-runner/error/cloud-runner-error.ts +++ b/src/model/cloud-runner/error/cloud-runner-error.ts @@ -1,19 +1,20 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger.ts'; -import { core } from '../../../dependencies.ts'; -import CloudRunner from '../cloud-runner.ts'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import * as core from '@actions/core'; +import CloudRunner from '../cloud-runner'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import BuildParameters from '../../build-parameters'; export class CloudRunnerError { - public static async handleException(error: unknown) { + public static async handleException(error: unknown, buildParameters: BuildParameters, secrets: CloudRunnerSecret[]) { CloudRunnerLogger.error(JSON.stringify(error, undefined, 4)); - log.error('Cloud runner failed'); - - await CloudRunner.Provider.cleanup( - CloudRunner.buildParameters.buildGuid, - CloudRunner.buildParameters, - CloudRunner.buildParameters.branch, - CloudRunner.defaultSecrets, - ); - - Deno.exit(1); + core.setFailed('Cloud Runner failed'); + if (CloudRunner.Provider !== undefined) { + await CloudRunner.Provider.cleanupWorkflow( + buildParameters.buildGuid, + buildParameters, + buildParameters.branch, + secrets, + ); + } } } diff --git a/src/model/cloud-runner/providers/aws/aws-base-stack.ts b/src/model/cloud-runner/providers/aws/aws-base-stack.ts index d34eb66..a60cc43 100644 --- a/src/model/cloud-runner/providers/aws/aws-base-stack.ts +++ b/src/model/cloud-runner/providers/aws/aws-base-stack.ts @@ -1,6 +1,8 @@ -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { core, aws, crypto } from '../../../../dependencies.ts'; -import { BaseStackFormation } from './cloud-formations/base-stack-formation.ts'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import * as core from '@actions/core'; +import * as SDK from 'aws-sdk'; +import { BaseStackFormation } from './cloud-formations/base-stack-formation'; +const crypto = require('crypto'); export class AWSBaseStack { constructor(baseStackName: string) { @@ -8,33 +10,33 @@ export class AWSBaseStack { } private baseStackName: string; - async setupBaseStack(CF: aws.CloudFormation) { + async setupBaseStack(CF: SDK.CloudFormation) { const baseStackName = this.baseStackName; const baseStack = BaseStackFormation.formation; // Cloud Formation Input - const describeStackInput: aws.CloudFormation.DescribeStacksInput = { + const describeStackInput: SDK.CloudFormation.DescribeStacksInput = { StackName: baseStackName, }; - const parametersWithoutHash: aws.CloudFormation.Parameter[] = [ + const parametersWithoutHash: SDK.CloudFormation.Parameter[] = [ { ParameterKey: 'EnvironmentName', ParameterValue: baseStackName }, ]; const parametersHash = crypto .createHash('md5') .update(baseStack + JSON.stringify(parametersWithoutHash)) .digest('hex'); - const parameters: aws.CloudFormation.Parameter[] = [ + const parameters: SDK.CloudFormation.Parameter[] = [ ...parametersWithoutHash, - { ParameterKey: 'Version', ParameterValue: parametersHash }, + ...[{ ParameterKey: 'Version', ParameterValue: parametersHash }], ]; - const updateInput: aws.CloudFormation.UpdateStackInput = { + const updateInput: SDK.CloudFormation.UpdateStackInput = { StackName: baseStackName, TemplateBody: baseStack, Parameters: parameters, Capabilities: ['CAPABILITY_IAM'], }; - const createStackInput: aws.CloudFormation.CreateStackInput = { + const createStackInput: SDK.CloudFormation.CreateStackInput = { StackName: baseStackName, TemplateBody: baseStack, Parameters: parameters, @@ -55,7 +57,7 @@ export class AWSBaseStack { await CF.createStack(createStackInput).promise(); CloudRunnerLogger.log(`created stack (version: ${parametersHash})`); } - let CFState = await describeStack(); + const CFState = await describeStack(); let stack = CFState.Stacks?.[0]; if (!stack) { throw new Error(`Base stack doesn't exist, even after creation, stackExists check: ${stackExists}`); @@ -84,9 +86,7 @@ export class AWSBaseStack { } else { CloudRunnerLogger.log(`No update required`); } - - CFState = await describeStack(); - stack = CFState.Stacks?.[0]; + stack = (await describeStack()).Stacks?.[0]; if (!stack) { throw new Error( `Base stack doesn't exist, even after updating and creation, stackExists check: ${stackExists}`, @@ -98,7 +98,7 @@ export class AWSBaseStack { } CloudRunnerLogger.log('base stack is now ready'); } catch (error) { - log.error(JSON.stringify(await describeStack(), undefined, 4)); + core.error(JSON.stringify(await describeStack(), undefined, 4)); throw error; } } diff --git a/src/model/cloud-runner/providers/aws/aws-cloud-formation-templates.ts b/src/model/cloud-runner/providers/aws/aws-cloud-formation-templates.ts index fd3ea23..0d0f83a 100644 --- a/src/model/cloud-runner/providers/aws/aws-cloud-formation-templates.ts +++ b/src/model/cloud-runner/providers/aws/aws-cloud-formation-templates.ts @@ -1,4 +1,4 @@ -import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation.ts'; +import { TaskDefinitionFormation } from './cloud-formations/task-definition-formation'; export class AWSCloudFormationTemplates { public static getParameterTemplate(p1) { diff --git a/src/model/cloud-runner/providers/aws/aws-error.ts b/src/model/cloud-runner/providers/aws/aws-error.ts index 26b706c..3b46875 100644 --- a/src/model/cloud-runner/providers/aws/aws-error.ts +++ b/src/model/cloud-runner/providers/aws/aws-error.ts @@ -1,15 +1,15 @@ -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { aws } from '../../../../dependencies.ts'; -import CloudRunner from '../../cloud-runner.ts'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import * as SDK from 'aws-sdk'; +import * as core from '@actions/core'; +import CloudRunner from '../../cloud-runner'; export class AWSError { - static async handleStackCreationFailure(error: any, CF: aws.CloudFormation, taskDefStackName: string) { + static async handleStackCreationFailure(error: any, CF: SDK.CloudFormation, taskDefStackName: string) { CloudRunnerLogger.log('aws error: '); - log.error(JSON.stringify(error, undefined, 4)); - if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) { + core.error(JSON.stringify(error, undefined, 4)); + if (CloudRunner.buildParameters.cloudRunnerDebug) { CloudRunnerLogger.log('Getting events and resources for task stack'); - const stackEventsDescription = await CF.describeStackEvents({ StackName: taskDefStackName }).promise(); - const events = stackEventsDescription.StackEvents; + const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents; CloudRunnerLogger.log(JSON.stringify(events, undefined, 4)); } } diff --git a/src/model/cloud-runner/providers/aws/aws-job-stack.ts b/src/model/cloud-runner/providers/aws/aws-job-stack.ts index 8dba533..9c7703f 100644 --- a/src/model/cloud-runner/providers/aws/aws-job-stack.ts +++ b/src/model/cloud-runner/providers/aws/aws-job-stack.ts @@ -1,10 +1,10 @@ -import { aws } from '../../../../dependencies.ts'; -import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; -import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { AWSError } from './aws-error.ts'; -import CloudRunner from '../../cloud-runner.ts'; +import * as SDK from 'aws-sdk'; +import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { AWSError } from './aws-error'; +import CloudRunner from '../../cloud-runner'; export class AWSJobStack { private baseStackName: string; @@ -13,7 +13,7 @@ export class AWSJobStack { } public async setupCloudFormations( - CF: aws.CloudFormation, + CF: SDK.CloudFormation, buildGuid: string, image: string, entrypoint: string[], @@ -69,6 +69,7 @@ export class AWSJobStack { const secretsMappedToCloudFormationParameters = secrets.map((x) => { return { ParameterKey: x.ParameterKey.replace(/[^\dA-Za-z]/g, ''), ParameterValue: x.ParameterValue }; }); + const logGroupName = `${this.baseStackName}/${taskDefStackName}`; const parameters = [ { ParameterKey: 'EnvironmentName', @@ -82,6 +83,10 @@ export class AWSJobStack { ParameterKey: 'ServiceName', ParameterValue: taskDefStackName, }, + { + ParameterKey: 'LogGroupName', + ParameterValue: logGroupName, + }, { ParameterKey: 'Command', ParameterValue: 'echo "this template should be overwritten when running a task"', @@ -115,10 +120,11 @@ export class AWSJobStack { if (element.StackName === taskDefStackName && element.StackStatus !== 'DELETE_COMPLETE') { previousStackExists = true; CloudRunnerLogger.log(`Previous stack still exists: ${JSON.stringify(element)}`); + await new Promise((promise) => setTimeout(promise, 5000)); } } } - const createStackInput: aws.CloudFormation.CreateStackInput = { + const createStackInput: SDK.CloudFormation.CreateStackInput = { StackName: taskDefStackName, TemplateBody: taskDefCloudFormation, Capabilities: ['CAPABILITY_IAM'], @@ -134,13 +140,13 @@ export class AWSJobStack { throw error; } - const { StackResources: taskDefResources } = await CF.describeStackResources({ - StackName: taskDefStackName, - }).promise(); + const taskDefResources = ( + await CF.describeStackResources({ + StackName: taskDefStackName, + }).promise() + ).StackResources; - const { StackResources: baseResources } = await CF.describeStackResources({ - StackName: this.baseStackName, - }).promise(); + const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources; return { taskDefStackName, diff --git a/src/model/cloud-runner/providers/aws/aws-task-runner.ts b/src/model/cloud-runner/providers/aws/aws-task-runner.ts index 424e89f..6822e38 100644 --- a/src/model/cloud-runner/providers/aws/aws-task-runner.ts +++ b/src/model/cloud-runner/providers/aws/aws-task-runner.ts @@ -1,21 +1,23 @@ -import { aws, core, compress } from '../../../../dependencies.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { Input } from '../../../index.ts'; -import CloudRunner from '../../cloud-runner.ts'; -import { CloudRunnerBuildCommandProcessor } from '../../services/cloud-runner-build-command-process.ts'; -import { FollowLogStreamService } from '../../services/follow-log-stream-service.ts'; +import * as AWS from 'aws-sdk'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import * as core from '@actions/core'; +import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; +import * as zlib from 'zlib'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { Input } from '../../..'; +import CloudRunner from '../../cloud-runner'; +import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks'; +import { FollowLogStreamService } from '../../services/follow-log-stream-service'; +import CloudRunnerOptions from '../../cloud-runner-options'; class AWSTaskRunner { + public static ECS: AWS.ECS; + public static Kinesis: AWS.Kinesis; static async runTask( taskDef: CloudRunnerAWSTaskDef, - ECS: aws.ECS, - CF: aws.CloudFormation, environment: CloudRunnerEnvironmentVariable[], - buildGuid: string, commands: string, - ) { + ): Promise<{ output: string; shouldCleanup: boolean }> { const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || ''; const taskDefinition = taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || ''; @@ -28,7 +30,7 @@ class AWSTaskRunner { const streamName = taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || ''; - const task = await ECS.runTask({ + const task = await AWSTaskRunner.ECS.runTask({ cluster, taskDefinition, platformVersion: '1.4.0', @@ -37,7 +39,7 @@ class AWSTaskRunner { { name: taskDef.taskDefStackName, environment, - command: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(commands, CloudRunner.buildParameters)], + command: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(commands, CloudRunner.buildParameters)], }, ], }, @@ -52,19 +54,25 @@ class AWSTaskRunner { }).promise(); const taskArn = task.tasks?.[0].taskArn || ''; CloudRunnerLogger.log('Cloud runner job is starting'); - await AWSTaskRunner.waitUntilTaskRunning(ECS, taskArn, cluster); - const { lastStatus } = await AWSTaskRunner.describeTasks(ECS, cluster, taskArn); - CloudRunnerLogger.log(`Cloud runner job status is running ${lastStatus}`); - const { output, shouldCleanup } = await this.streamLogsUntilTaskStops( - ECS, - CF, - taskDef, - cluster, - taskArn, - streamName, + await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster); + CloudRunnerLogger.log( + `Cloud runner job status is running ${(await AWSTaskRunner.describeTasks(cluster, taskArn))?.lastStatus}`, ); - const taskData = await AWSTaskRunner.describeTasks(ECS, cluster, taskArn); - const exitCode = taskData.containers?.[0].exitCode; + if (!CloudRunnerOptions.watchCloudRunnerToEnd) { + const shouldCleanup: boolean = false; + const output: string = ''; + CloudRunnerLogger.log(`Watch Cloud Runner To End: false`); + + return { output, shouldCleanup }; + } + + CloudRunnerLogger.log(`Streaming...`); + const { output, shouldCleanup } = await this.streamLogsUntilTaskStops(cluster, taskArn, streamName); + await new Promise((resolve) => resolve(5000)); + const taskData = await AWSTaskRunner.describeTasks(cluster, taskArn); + const containerState = taskData.containers?.[0]; + const exitCode = containerState?.exitCode || undefined; + CloudRunnerLogger.log(`Container State: ${JSON.stringify(containerState, undefined, 4)}`); const wasSuccessful = exitCode === 0 || (exitCode === undefined && taskData.lastStatus === 'RUNNING'); if (wasSuccessful) { CloudRunnerLogger.log(`Cloud runner job has finished successfully`); @@ -82,22 +90,25 @@ class AWSTaskRunner { } } - private static async waitUntilTaskRunning(ECS: aws.ECS, taskArn: string, cluster: string) { + private static async waitUntilTaskRunning(taskArn: string, cluster: string) { try { - await ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise(); + await AWSTaskRunner.ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise(); } catch (error_) { const error = error_ as Error; await new Promise((resolve) => setTimeout(resolve, 3000)); - const tasksDescription = await AWSTaskRunner.describeTasks(ECS, cluster, taskArn); - CloudRunnerLogger.log(`Cloud runner job has ended ${tasksDescription.containers?.[0].lastStatus}`); - - log.error(error); - Deno.exit(1); + CloudRunnerLogger.log( + `Cloud runner job has ended ${ + (await AWSTaskRunner.describeTasks(cluster, taskArn)).containers?.[0].lastStatus + }`, + ); + + core.setFailed(error); + core.error(error); } } - static async describeTasks(ECS: aws.ECS, clusterName: string, taskArn: string) { - const tasks = await ECS.describeTasks({ + static async describeTasks(clusterName: string, taskArn: string) { + const tasks = await AWSTaskRunner.ECS.describeTasks({ cluster: clusterName, tasks: [taskArn], }).promise(); @@ -108,17 +119,11 @@ class AWSTaskRunner { } } - static async streamLogsUntilTaskStops( - ECS: aws.ECS, - CF: aws.CloudFormation, - taskDef: CloudRunnerAWSTaskDef, - clusterName: string, - taskArn: string, - kinesisStreamName: string, - ) { - const kinesis = new aws.Kinesis(); - const stream = await AWSTaskRunner.getLogStream(kinesis, kinesisStreamName); - let iterator = await AWSTaskRunner.getLogIterator(kinesis, stream); + static async streamLogsUntilTaskStops(clusterName: string, taskArn: string, kinesisStreamName: string) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + CloudRunnerLogger.log(`Streaming...`); + const stream = await AWSTaskRunner.getLogStream(kinesisStreamName); + let iterator = await AWSTaskRunner.getLogIterator(stream); const logBaseUrl = `https://${Input.region}.console.aws.amazon.com/cloudwatch/home?region=${Input.region}#logsV2:log-groups/log-group/${CloudRunner.buildParameters.awsBaseStackName}-${CloudRunner.buildParameters.buildGuid}`; CloudRunnerLogger.log(`You view the log stream on AWS Cloud Watch: ${logBaseUrl}`); @@ -128,13 +133,11 @@ class AWSTaskRunner { let output = ''; while (shouldReadLogs) { await new Promise((resolve) => setTimeout(resolve, 1500)); - const taskData = await AWSTaskRunner.describeTasks(ECS, clusterName, taskArn); + const taskData = await AWSTaskRunner.describeTasks(clusterName, taskArn); ({ timestamp, shouldReadLogs } = AWSTaskRunner.checkStreamingShouldContinue(taskData, timestamp, shouldReadLogs)); ({ iterator, shouldReadLogs, output, shouldCleanup } = await AWSTaskRunner.handleLogStreamIteration( - kinesis, iterator, shouldReadLogs, - taskDef, output, shouldCleanup, )); @@ -144,23 +147,18 @@ class AWSTaskRunner { } private static async handleLogStreamIteration( - kinesis: aws.Kinesis, iterator: string, shouldReadLogs: boolean, - taskDef: CloudRunnerAWSTaskDef, output: string, shouldCleanup: boolean, ) { - const records = await kinesis - .getRecords({ - ShardIterator: iterator, - }) - .promise(); + const records = await AWSTaskRunner.Kinesis.getRecords({ + ShardIterator: iterator, + }).promise(); iterator = records.NextShardIterator || ''; ({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords( records, iterator, - taskDef, shouldReadLogs, output, shouldCleanup, @@ -169,7 +167,7 @@ class AWSTaskRunner { return { iterator, shouldReadLogs, output, shouldCleanup }; } - private static checkStreamingShouldContinue(taskData: aws.ECS.Task, timestamp: number, shouldReadLogs: boolean) { + private static checkStreamingShouldContinue(taskData: AWS.ECS.Task, timestamp: number, shouldReadLogs: boolean) { if (taskData?.lastStatus === 'UNKNOWN') { CloudRunnerLogger.log('## Cloud runner job unknwon'); } @@ -178,7 +176,7 @@ class AWSTaskRunner { CloudRunnerLogger.log('## Cloud runner job stopped, streaming end of logs'); timestamp = Date.now(); } - if (timestamp !== 0 && Date.now() - timestamp > 30_000) { + if (timestamp !== 0 && Date.now() - timestamp > 30000) { CloudRunnerLogger.log('## Cloud runner status is not RUNNING for 30 seconds, last query for logs'); shouldReadLogs = false; } @@ -191,7 +189,6 @@ class AWSTaskRunner { private static logRecords( records, iterator: string, - taskDef: CloudRunnerAWSTaskDef, shouldReadLogs: boolean, output: string, shouldCleanup: boolean, @@ -199,7 +196,7 @@ class AWSTaskRunner { if (records.Records.length > 0 && iterator) { for (let index = 0; index < records.Records.length; index++) { const json = JSON.parse( - compress.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'), + zlib.gunzipSync(Buffer.from(records.Records[index].Data as string, 'base64')).toString('utf8'), ); if (json.messageType === 'DATA_MESSAGE') { for (let logEventsIndex = 0; logEventsIndex < json.logEvents.length; logEventsIndex++) { @@ -218,24 +215,22 @@ class AWSTaskRunner { return { shouldReadLogs, output, shouldCleanup }; } - private static async getLogStream(kinesis: aws.Kinesis, kinesisStreamName: string) { - return await kinesis - .describeStream({ - StreamName: kinesisStreamName, - }) - .promise(); + private static async getLogStream(kinesisStreamName: string) { + return await AWSTaskRunner.Kinesis.describeStream({ + StreamName: kinesisStreamName, + }).promise(); } - private static async getLogIterator(kinesis: aws.Kinesis, stream) { - const description = await kinesis - .getShardIterator({ - ShardIteratorType: 'TRIM_HORIZON', - StreamName: stream.StreamDescription.StreamName, - ShardId: stream.StreamDescription.Shards[0].ShardId, - }) - .promise(); - - return description.ShardIterator || ''; + private static async getLogIterator(stream) { + return ( + ( + await AWSTaskRunner.Kinesis.getShardIterator({ + ShardIteratorType: 'TRIM_HORIZON', + StreamName: stream.StreamDescription.StreamName, + ShardId: stream.StreamDescription.Shards[0].ShardId, + }).promise() + ).ShardIterator || '' + ); } } export default AWSTaskRunner; diff --git a/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts b/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts index 78a9809..44de060 100644 --- a/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts +++ b/src/model/cloud-runner/providers/aws/cloud-formations/task-definition-formation.ts @@ -11,6 +11,10 @@ Parameters: Type: String Default: example Description: A name for the service + LogGroupName: + Type: String + Default: example + Description: Name to use for the log group created for this task ImageUrl: Type: String Default: nginx @@ -68,7 +72,7 @@ Resources: LogGroup: Type: 'AWS::Logs::LogGroup' Properties: - LogGroupName: !Ref ServiceName + LogGroupName: !Ref LogGroupName Metadata: 'AWS::CloudFormation::Designer': id: aece53ae-b82d-4267-bc16-ed964b05db27 @@ -78,7 +82,7 @@ Resources: FilterPattern: '' RoleArn: 'Fn::ImportValue': !Sub '${'${EnvironmentName}'}:CloudWatchIAMRole' - LogGroupName: !Ref ServiceName + LogGroupName: !Ref LogGroupName DestinationArn: 'Fn::GetAtt': - KinesisStream @@ -147,7 +151,7 @@ Resources: LogConfiguration: LogDriver: awslogs Options: - awslogs-group: !Ref ServiceName + awslogs-group: !Ref LogGroupName awslogs-region: !Ref 'AWS::Region' awslogs-stream-prefix: !Ref ServiceName DependsOn: diff --git a/src/model/cloud-runner/providers/aws/cloud-runner-aws-task-def.ts b/src/model/cloud-runner/providers/aws/cloud-runner-aws-task-def.ts index 5439bff..4ec1619 100644 --- a/src/model/cloud-runner/providers/aws/cloud-runner-aws-task-def.ts +++ b/src/model/cloud-runner/providers/aws/cloud-runner-aws-task-def.ts @@ -1,9 +1,9 @@ -import { aws } from '../../../../dependencies.ts'; +import * as AWS from 'aws-sdk'; class CloudRunnerAWSTaskDef { public taskDefStackName!: string; public taskDefCloudFormation!: string; - public taskDefResources: aws.CloudFormation.StackResources | undefined; - public baseResources: aws.CloudFormation.StackResources | undefined; + public taskDefResources: AWS.CloudFormation.StackResources | undefined; + public baseResources: AWS.CloudFormation.StackResources | undefined; } export default CloudRunnerAWSTaskDef; diff --git a/src/model/cloud-runner/providers/aws/commands/aws-cli-commands.ts b/src/model/cloud-runner/providers/aws/commands/aws-cli-commands.ts deleted file mode 100644 index 1aee8e1..0000000 --- a/src/model/cloud-runner/providers/aws/commands/aws-cli-commands.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { aws } from '../../../../../dependencies.ts'; -import { CliFunction } from '../../../../cli/cli-functions-repository.ts'; -import Input from '../../../../input.ts'; -import CloudRunnerLogger from '../../../services/cloud-runner-logger.ts'; -import { BaseStackFormation } from '../cloud-formations/base-stack-formation.ts'; - -export class AwsCliCommands { - @CliFunction(`aws-list-all`, `List all resources`) - static async awsListAll() { - await AwsCliCommands.awsListStacks(undefined, true); - await AwsCliCommands.awsListTasks(); - await AwsCliCommands.awsListLogGroups(undefined, true); - } - @CliFunction(`aws-garbage-collect`, `garbage collect aws resources not in use !WIP!`) - static async garbageCollectAws() { - await AwsCliCommands.cleanup(false); - } - @CliFunction(`aws-garbage-collect-all`, `garbage collect aws resources regardless of whether they are in use`) - static async garbageCollectAwsAll() { - await AwsCliCommands.cleanup(true); - } - @CliFunction( - `aws-garbage-collect-all-1d-older`, - `garbage collect aws resources created more than 1d ago (ignore if they are in use)`, - ) - static async garbageCollectAwsAllOlderThanOneDay() { - await AwsCliCommands.cleanup(true, true); - } - static isOlderThan1day(date: any) { - const ageDate = new Date(date.getTime() - Date.now()); - - return ageDate.getDay() > 0; - } - @CliFunction(`aws-list-stacks`, `List stacks`) - static async awsListStacks(perResultCallback: any = false, verbose: boolean = false) { - Deno.env.set('AWS_REGION', Input.region); - const CF = new aws.CloudFormation(); - let cfStacks = await CF.listStacks().promise(); - const stacks = - cfStacks.StackSummaries?.filter( - (_x) => _x.StackStatus !== 'DELETE_COMPLETE', // && - // _x.TemplateDescription === TaskDefinitionFormation.description.replace('\n', ''), - ) || []; - CloudRunnerLogger.log(`Stacks ${stacks.length}`); - for (const element of stacks) { - const ageDate = new Date(element.CreationTime.getTime() - Date.now()); - if (verbose) - CloudRunnerLogger.log( - `Task Stack ${element.StackName} - Age D${ageDate.getDay()} H${ageDate.getHours()} M${ageDate.getMinutes()}`, - ); - if (perResultCallback) await perResultCallback(element); - } - cfStacks = await CF.listStacks().promise(); - const baseStacks = - cfStacks.StackSummaries?.filter( - (_x) => - _x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription, - ) || []; - CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`); - for (const element of baseStacks) { - const ageDate = new Date(element.CreationTime.getTime() - Date.now()); - if (verbose) - CloudRunnerLogger.log( - `Base Stack ${ - element.StackName - } - Age D${ageDate.getHours()} H${ageDate.getHours()} M${ageDate.getMinutes()}`, - ); - if (perResultCallback) await perResultCallback(element); - } - if (stacks === undefined) { - return; - } - } - @CliFunction(`aws-list-tasks`, `List tasks`) - static async awsListTasks(perResultCallback: any = false) { - Deno.env.set('AWS_REGION', Input.region); - const ecs = new aws.ECS(); - const ecsClusters = await ecs.listClusters().promise(); - const clusters = ecsClusters.clusterArns || []; - CloudRunnerLogger.log(`Clusters ${clusters.length}`); - for (const element of clusters) { - const input: aws.ECS.ListTasksRequest = { - cluster: element, - }; - - const listedTasks = await ecs.listTasks(input).promise(); - const list = listedTasks.taskArns || []; - if (list.length > 0) { - const describeInput: aws.ECS.DescribeTasksRequest = { tasks: list, cluster: element }; - const tasksDescription = await ecs.describeTasks(describeInput).promise(); - const describeList = tasksDescription.tasks || []; - if (describeList === []) { - continue; - } - CloudRunnerLogger.log(`Tasks ${describeList.length}`); - for (const taskElement of describeList) { - if (taskElement === undefined) { - continue; - } - taskElement.overrides = {}; - taskElement.attachments = []; - if (taskElement.createdAt === undefined) { - CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`); - continue; - } - if (perResultCallback) await perResultCallback(taskElement, element); - } - } - } - } - @CliFunction(`aws-list-log-groups`, `List tasks`) - static async awsListLogGroups(perResultCallback: any = false, verbose: boolean = false) { - Deno.env.set('AWS_REGION', Input.region); - const ecs = new aws.CloudWatchLogs(); - let logStreamInput: aws.CloudWatchLogs.DescribeLogGroupsRequest = { - /* logGroupNamePrefix: 'game-ci' */ - }; - let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise(); - const logGroups = logGroupsDescribe.logGroups || []; - while (logGroupsDescribe.nextToken) { - logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken }; - logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise(); - logGroups.push(...(logGroupsDescribe?.logGroups || [])); - } - - CloudRunnerLogger.log(`Log Groups ${logGroups.length}`); - for (const element of logGroups) { - if (element.creationTime === undefined) { - CloudRunnerLogger.log(`Skipping ${element.logGroupName} no createdAt date`); - continue; - } - const ageDate = new Date(new Date(element.creationTime).getTime() - Date.now()); - if (verbose) - CloudRunnerLogger.log( - `Log Group Name ${ - element.logGroupName - } - Age D${ageDate.getDay()} H${ageDate.getHours()} M${ageDate.getMinutes()} - 1d old ${AwsCliCommands.isOlderThan1day( - new Date(element.creationTime), - )}`, - ); - if (perResultCallback) await perResultCallback(element, element); - } - } - - private static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) { - Deno.env.set('AWS_REGION', Input.region); - const CF = new aws.CloudFormation(); - const ecs = new aws.ECS(); - const cwl = new aws.CloudWatchLogs(); - await AwsCliCommands.awsListStacks(async (element) => { - if (deleteResources && (!OneDayOlderOnly || AwsCliCommands.isOlderThan1day(element.CreationTime))) { - if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') { - CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`); - - return; - } - CloudRunnerLogger.log(`Deleting ${element.logGroupName}`); - const deleteStackInput: aws.CloudFormation.DeleteStackInput = { StackName: element.StackName }; - await CF.deleteStack(deleteStackInput).promise(); - } - }); - await AwsCliCommands.awsListTasks(async (taskElement, element) => { - if (deleteResources && (!OneDayOlderOnly || AwsCliCommands.isOlderThan1day(taskElement.CreatedAt))) { - CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`); - await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise(); - } - }); - await AwsCliCommands.awsListLogGroups(async (element) => { - if (deleteResources && (!OneDayOlderOnly || AwsCliCommands.isOlderThan1day(new Date(element.createdAt)))) { - CloudRunnerLogger.log(`Deleting ${element.logGroupName}`); - await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise(); - } - }); - } -} diff --git a/src/model/cloud-runner/providers/aws/index.ts b/src/model/cloud-runner/providers/aws/index.ts index b59b347..fa40e74 100644 --- a/src/model/cloud-runner/providers/aws/index.ts +++ b/src/model/cloud-runner/providers/aws/index.ts @@ -1,35 +1,84 @@ -import { aws } from '../../../../dependencies.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def.ts'; -import AWSTaskRunner from './aws-task-runner.ts'; -import { ProviderInterface } from '../provider-interface.ts'; -import Parameters from '../../../parameters.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { AWSJobStack } from './aws-job-stack.ts'; -import { AWSBaseStack } from './aws-base-stack.ts'; -import { Input } from '../../../index.ts'; +import * as SDK from 'aws-sdk'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; +import AwsTaskRunner from './aws-task-runner'; +import { ProviderInterface } from '../provider-interface'; +import BuildParameters from '../../../build-parameters'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { AWSJobStack as AwsJobStack } from './aws-job-stack'; +import { AWSBaseStack as AwsBaseStack } from './aws-base-stack'; +import { Input } from '../../..'; +import { TertiaryResourcesService } from './services/tertiary-resources-service'; +import { GarbageCollectionService } from './services/garbage-collection-service'; +import { ProviderResource } from '../provider-resource'; +import { ProviderWorkflow } from '../provider-workflow'; +import { TaskService } from './services/task-service'; class AWSBuildEnvironment implements ProviderInterface { private baseStackName: string; - constructor(buildParameters: Parameters) { + constructor(buildParameters: BuildParameters) { this.baseStackName = buildParameters.awsBaseStackName; } - async cleanup( + async listResources(): Promise { + await TaskService.awsListJobs(); + await TertiaryResourcesService.awsListLogGroups(); + await TaskService.awsListTasks(); + await TaskService.awsListStacks(); + + return []; + } + listWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + async watchWorkflow(): Promise { + return await TaskService.watch(); + } + + async listOtherResources(): Promise { + await TertiaryResourcesService.awsListLogGroups(); + + return ''; + } + + async garbageCollect( + filter: string, + previewOnly: boolean, + // eslint-disable-next-line no-unused-vars + olderThan: Number, + // eslint-disable-next-line no-unused-vars + fullCache: boolean, + // eslint-disable-next-line no-unused-vars + baseDependencies: boolean, + ): Promise { + await GarbageCollectionService.cleanup(!previewOnly); + + return ``; + } + + async cleanupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) {} - async setup( + async setupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) {} - async runTask( + async runTaskInWorkflow( buildGuid: string, image: string, commands: string, @@ -38,15 +87,17 @@ class AWSBuildEnvironment implements ProviderInterface { environment: CloudRunnerEnvironmentVariable[], secrets: CloudRunnerSecret[], ): Promise { - Deno.env.set('AWS_REGION', Input.region); - const ECS = new aws.ECS(); - const CF = new aws.CloudFormation(); + process.env.AWS_REGION = Input.region; + const ECS = new SDK.ECS(); + const CF = new SDK.CloudFormation(); + AwsTaskRunner.ECS = ECS; + AwsTaskRunner.Kinesis = new SDK.Kinesis(); CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`); const entrypoint = ['/bin/sh']; const startTimeMs = Date.now(); - await new AWSBaseStack(this.baseStackName).setupBaseStack(CF); - const taskDef = await new AWSJobStack(this.baseStackName).setupCloudFormations( + await new AwsBaseStack(this.baseStackName).setupBaseStack(CF); + const taskDef = await new AwsJobStack(this.baseStackName).setupCloudFormations( CF, buildGuid, image, @@ -61,7 +112,7 @@ class AWSBuildEnvironment implements ProviderInterface { try { const postSetupStacksTimeMs = Date.now(); CloudRunnerLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`); - const { output, shouldCleanup } = await AWSTaskRunner.runTask(taskDef, ECS, CF, environment, buildGuid, commands); + const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, commands); postRunTaskTimeMs = Date.now(); CloudRunnerLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`); if (shouldCleanup) { @@ -73,12 +124,13 @@ class AWSBuildEnvironment implements ProviderInterface { return output; } catch (error) { + CloudRunnerLogger.log(`error running task ${error}`); await this.cleanupResources(CF, taskDef); throw error; } } - async cleanupResources(CF: aws.CloudFormation, taskDef: CloudRunnerAWSTaskDef) { + async cleanupResources(CF: SDK.CloudFormation, taskDef: CloudRunnerAWSTaskDef) { CloudRunnerLogger.log('Cleanup starting'); await CF.deleteStack({ StackName: taskDef.taskDefStackName, diff --git a/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts b/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts new file mode 100644 index 0000000..51f5809 --- /dev/null +++ b/src/model/cloud-runner/providers/aws/services/garbage-collection-service.ts @@ -0,0 +1,58 @@ +import AWS from 'aws-sdk'; +import Input from '../../../../input'; +import CloudRunnerLogger from '../../../services/cloud-runner-logger'; +import { TaskService } from './task-service'; +import { TertiaryResourcesService } from './tertiary-resources-service'; + +export class GarbageCollectionService { + static isOlderThan1day(date: any) { + const ageDate = new Date(date.getTime() - Date.now()); + + return ageDate.getDay() > 0; + } + + public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) { + process.env.AWS_REGION = Input.region; + const CF = new AWS.CloudFormation(); + const ecs = new AWS.ECS(); + const cwl = new AWS.CloudWatchLogs(); + const taskDefinitionsInUse = new Array(); + await TaskService.awsListTasks(async (taskElement, element) => { + taskDefinitionsInUse.push(taskElement.taskDefinitionArn); + if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.CreatedAt))) { + CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`); + await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise(); + } + }); + await TaskService.awsListStacks(async (element) => { + if ( + (await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some( + (x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId), + ) + ) { + CloudRunnerLogger.log(`Skipping ${element.StackName} - active task was running not deleting`); + + return; + } + if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) { + if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') { + CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`); + + return; + } + CloudRunnerLogger.log(`Deleting ${element.logGroupName}`); + const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName }; + await CF.deleteStack(deleteStackInput).promise(); + } + }); + await TertiaryResourcesService.awsListLogGroups(async (element) => { + if ( + deleteResources && + (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.createdAt))) + ) { + CloudRunnerLogger.log(`Deleting ${element.logGroupName}`); + await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise(); + } + }); + } +} diff --git a/src/model/cloud-runner/providers/aws/services/task-service.ts b/src/model/cloud-runner/providers/aws/services/task-service.ts new file mode 100644 index 0000000..d8ba36f --- /dev/null +++ b/src/model/cloud-runner/providers/aws/services/task-service.ts @@ -0,0 +1,134 @@ +import AWS from 'aws-sdk'; +import Input from '../../../../input'; +import CloudRunnerLogger from '../../../services/cloud-runner-logger'; +import { BaseStackFormation } from '../cloud-formations/base-stack-formation'; +import AwsTaskRunner from '../aws-task-runner'; + +export class TaskService { + static async watch() { + // eslint-disable-next-line no-unused-vars + const { output, shouldCleanup } = await AwsTaskRunner.streamLogsUntilTaskStops( + process.env.cluster || ``, + process.env.taskArn || ``, + process.env.streamName || ``, + ); + + return output; + } + public static async awsListStacks(perResultCallback: any = false) { + CloudRunnerLogger.log(`List Stacks`); + process.env.AWS_REGION = Input.region; + const CF = new AWS.CloudFormation(); + const stacks = + (await CF.listStacks().promise()).StackSummaries?.filter( + (_x) => _x.StackStatus !== 'DELETE_COMPLETE', // && + // _x.TemplateDescription === TaskDefinitionFormation.description.replace('\n', ''), + ) || []; + CloudRunnerLogger.log(`Stacks ${stacks.length}`); + for (const element of stacks) { + const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime()); + + CloudRunnerLogger.log( + `Task Stack ${element.StackName} - Age D${Math.floor( + ageDate.getHours() / 24, + )} H${ageDate.getHours()} M${ageDate.getMinutes()}`, + ); + if (perResultCallback) await perResultCallback(element); + } + const baseStacks = + (await CF.listStacks().promise()).StackSummaries?.filter( + (_x) => + _x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription, + ) || []; + CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`); + for (const element of baseStacks) { + const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime()); + + CloudRunnerLogger.log( + `Task Stack ${element.StackName} - Age D${Math.floor( + ageDate.getHours() / 24, + )} H${ageDate.getHours()} M${ageDate.getMinutes()}`, + ); + if (perResultCallback) await perResultCallback(element); + } + if (stacks === undefined) { + return; + } + } + public static async awsListTasks(perResultCallback: any = false) { + CloudRunnerLogger.log(`List Tasks`); + process.env.AWS_REGION = Input.region; + const ecs = new AWS.ECS(); + const clusters = (await ecs.listClusters().promise()).clusterArns || []; + CloudRunnerLogger.log(`Clusters ${clusters.length}`); + for (const element of clusters) { + const input: AWS.ECS.ListTasksRequest = { + cluster: element, + }; + + const list = (await ecs.listTasks(input).promise()).taskArns || []; + if (list.length > 0) { + const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element }; + const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || []; + if (describeList.length === 0) { + continue; + } + CloudRunnerLogger.log(`Tasks ${describeList.length}`); + for (const taskElement of describeList) { + if (taskElement === undefined) { + continue; + } + taskElement.overrides = {}; + taskElement.attachments = []; + if (taskElement.createdAt === undefined) { + CloudRunnerLogger.log(`Skipping ${taskElement.taskDefinitionArn} no createdAt date`); + continue; + } + if (perResultCallback) await perResultCallback(taskElement, element); + } + } + } + } + public static async awsListJobs(perResultCallback: any = false) { + CloudRunnerLogger.log(`List Jobs`); + process.env.AWS_REGION = Input.region; + const CF = new AWS.CloudFormation(); + const stacks = + (await CF.listStacks().promise()).StackSummaries?.filter( + (_x) => + _x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription !== BaseStackFormation.baseStackDecription, + ) || []; + CloudRunnerLogger.log(`Stacks ${stacks.length}`); + for (const element of stacks) { + const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime()); + + CloudRunnerLogger.log( + `Task Stack ${element.StackName} - Age D${Math.floor( + ageDate.getHours() / 24, + )} H${ageDate.getHours()} M${ageDate.getMinutes()}`, + ); + if (perResultCallback) await perResultCallback(element); + } + } + public static async awsDescribeJob(job: string) { + process.env.AWS_REGION = Input.region; + const CF = new AWS.CloudFormation(); + const stack = (await CF.listStacks().promise()).StackSummaries?.find((_x) => _x.StackName === job) || undefined; + const stackInfo = (await CF.describeStackResources({ StackName: job }).promise()) || undefined; + const stackInfo2 = (await CF.describeStacks({ StackName: job }).promise()) || undefined; + if (stack === undefined) { + throw new Error('stack not defined'); + } + const ageDate: Date = new Date(Date.now() - stack.CreationTime.getTime()); + const message = ` + Task Stack ${stack.StackName} + Age D${Math.floor(ageDate.getHours() / 24)} H${ageDate.getHours()} M${ageDate.getMinutes()} + ${JSON.stringify(stack, undefined, 4)} + ${JSON.stringify(stackInfo, undefined, 4)} + ${JSON.stringify(stackInfo2, undefined, 4)} + `; + CloudRunnerLogger.log(message); + + return message; + } +} diff --git a/src/model/cloud-runner/providers/aws/services/tertiary-resources-service.ts b/src/model/cloud-runner/providers/aws/services/tertiary-resources-service.ts new file mode 100644 index 0000000..2230026 --- /dev/null +++ b/src/model/cloud-runner/providers/aws/services/tertiary-resources-service.ts @@ -0,0 +1,36 @@ +import AWS from 'aws-sdk'; +import Input from '../../../../input'; +import CloudRunnerLogger from '../../../services/cloud-runner-logger'; + +export class TertiaryResourcesService { + public static async awsListLogGroups(perResultCallback: any = false) { + process.env.AWS_REGION = Input.region; + const ecs = new AWS.CloudWatchLogs(); + let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = { + /* logGroupNamePrefix: 'game-ci' */ + }; + let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise(); + const logGroups = logGroupsDescribe.logGroups || []; + while (logGroupsDescribe.nextToken) { + logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken }; + logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise(); + logGroups.push(...(logGroupsDescribe?.logGroups || [])); + } + + CloudRunnerLogger.log(`Log Groups ${logGroups.length}`); + for (const element of logGroups) { + if (element.creationTime === undefined) { + CloudRunnerLogger.log(`Skipping ${element.logGroupName} no createdAt date`); + continue; + } + const ageDate: Date = new Date(Date.now() - element.creationTime); + + CloudRunnerLogger.log( + `Task Stack ${element.logGroupName} - Age D${Math.floor( + ageDate.getHours() / 24, + )} H${ageDate.getHours()} M${ageDate.getMinutes()}`, + ); + if (perResultCallback) await perResultCallback(element, element); + } + } +} diff --git a/src/model/cloud-runner/providers/docker/index.ts b/src/model/cloud-runner/providers/docker/index.ts new file mode 100644 index 0000000..8de6cce --- /dev/null +++ b/src/model/cloud-runner/providers/docker/index.ts @@ -0,0 +1,148 @@ +import BuildParameters from '../../../build-parameters'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { ProviderInterface } from '../provider-interface'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import Docker from '../../../docker'; +import { Action } from '../../..'; +import { writeFileSync } from 'fs'; +import CloudRunner from '../../cloud-runner'; +import { ProviderResource } from '../provider-resource'; +import { ProviderWorkflow } from '../provider-workflow'; +import { CloudRunnerSystem } from '../../services/cloud-runner-system'; +import * as fs from 'fs'; + +class LocalDockerCloudRunner implements ProviderInterface { + public buildParameters: BuildParameters | undefined; + + listResources(): Promise { + return new Promise((resolve) => resolve([])); + } + listWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + watchWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + garbageCollect( + // eslint-disable-next-line no-unused-vars + filter: string, + // eslint-disable-next-line no-unused-vars + previewOnly: boolean, + // eslint-disable-next-line no-unused-vars + olderThan: Number, + // eslint-disable-next-line no-unused-vars + fullCache: boolean, + // eslint-disable-next-line no-unused-vars + baseDependencies: boolean, + ): Promise { + return new Promise((result) => result(``)); + } + async cleanupWorkflow( + buildGuid: string, + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars + branchName: string, + // eslint-disable-next-line no-unused-vars + defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], + ) { + const { workspace } = Action; + if (fs.existsSync(`${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar.lz4`)) { + await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache/cache/build/`); + await CloudRunnerSystem.Run( + `rm -r ${workspace}/cloud-runner-cache/cache/build/build-${buildParameters.buildGuid}.tar.lz4`, + ); + } + } + setupWorkflow( + buildGuid: string, + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars + branchName: string, + // eslint-disable-next-line no-unused-vars + defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], + ) { + this.buildParameters = buildParameters; + } + + public async runTaskInWorkflow( + buildGuid: string, + image: string, + commands: string, + mountdir: string, + workingdir: string, + environment: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ): Promise { + CloudRunnerLogger.log(buildGuid); + CloudRunnerLogger.log(commands); + + const { workspace, actionFolder } = Action; + const content: any[] = []; + for (const x of secrets) { + content.push({ name: x.EnvironmentVariable, value: x.ParameterValue }); + } + for (const x of environment) { + content.push({ name: x.name, value: x.value }); + } + + // if (this.buildParameters?.cloudRunnerIntegrationTests) { + // core.info(JSON.stringify(content, undefined, 4)); + // core.info(JSON.stringify(secrets, undefined, 4)); + // core.info(JSON.stringify(environment, undefined, 4)); + // } + + // eslint-disable-next-line unicorn/no-for-loop + for (let index = 0; index < content.length; index++) { + if (content[index] === undefined) { + delete content[index]; + } + } + let myOutput = ''; + const sharedFolder = `/data/`; + + // core.info(JSON.stringify({ workspace, actionFolder, ...this.buildParameters, ...content }, undefined, 4)); + const entrypointFilePath = `start.sh`; + const fileContents = `#!/bin/bash +set -e + +mkdir -p /github/workspace/cloud-runner-cache +mkdir -p /data/cache +cp -a /github/workspace/cloud-runner-cache/. ${sharedFolder} +${commands} +cp -a ${sharedFolder}. /github/workspace/cloud-runner-cache/ +`; + writeFileSync(`${workspace}/${entrypointFilePath}`, fileContents, { + flag: 'w', + }); + + if (CloudRunner.buildParameters.cloudRunnerDebug) { + CloudRunnerLogger.log(`Running local-docker: \n ${fileContents}`); + } + + if (fs.existsSync(`${workspace}/cloud-runner-cache`)) { + await CloudRunnerSystem.Run(`ls ${workspace}/cloud-runner-cache && du -sh ${workspace}/cloud-runner-cache`); + } + await Docker.run( + image, + { workspace, actionFolder, ...this.buildParameters }, + false, + `chmod +x /github/workspace/${entrypointFilePath} && /github/workspace/${entrypointFilePath}`, + content, + { + listeners: { + stdout: (data: Buffer) => { + myOutput += data.toString(); + }, + stderr: (data: Buffer) => { + myOutput += `[LOCAL-DOCKER-ERROR]${data.toString()}`; + }, + }, + }, + true, + ); + + return myOutput; + } +} +export default LocalDockerCloudRunner; diff --git a/src/model/cloud-runner/providers/k8s/index.ts b/src/model/cloud-runner/providers/k8s/index.ts index 454a984..bc427e2 100644 --- a/src/model/cloud-runner/providers/k8s/index.ts +++ b/src/model/cloud-runner/providers/k8s/index.ts @@ -1,54 +1,103 @@ -import { Parameters, Output } from '../../../index.ts'; -import { k8sTypes, k8s, waitUntil } from '../../../../dependencies.ts'; -import { ProviderInterface } from '../provider-interface.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; -import KubernetesStorage from './kubernetes-storage.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import KubernetesTaskRunner from './kubernetes-task-runner.ts'; -import KubernetesSecret from './kubernetes-secret.ts'; -import KubernetesJobSpecFactory from './kubernetes-job-spec-factory.ts'; -import KubernetesServiceAccount from './kubernetes-service-account.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import DependencyOverrideService from '../../services/depdency-override-service.ts'; +import * as k8s from '@kubernetes/client-node'; +import { BuildParameters } from '../../..'; +import * as core from '@actions/core'; +import { ProviderInterface } from '../provider-interface'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import KubernetesStorage from './kubernetes-storage'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import KubernetesTaskRunner from './kubernetes-task-runner'; +import KubernetesSecret from './kubernetes-secret'; +import KubernetesJobSpecFactory from './kubernetes-job-spec-factory'; +import KubernetesServiceAccount from './kubernetes-service-account'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { CoreV1Api } from '@kubernetes/client-node'; +import CloudRunner from '../../cloud-runner'; +import { ProviderResource } from '../provider-resource'; +import { ProviderWorkflow } from '../provider-workflow'; +import KubernetesPods from './kubernetes-pods'; class Kubernetes implements ProviderInterface { - private kubeConfig: k8sTypes.KubeConfig; - private kubeClient: k8sTypes.CoreV1Api; - private kubeClientBatch: k8sTypes.BatchV1Api; - private buildGuid: string = ''; - private buildParameters: Parameters; - private pvcName: string = ''; - private secretName: string = ''; - private jobName: string = ''; - private namespace: string; - private podName: string = ''; - private containerName: string = ''; - private cleanupCronJobName: string = ''; - private serviceAccountName: string = ''; - - constructor(buildParameters: Parameters) { + public static Instance: Kubernetes; + public kubeConfig!: k8s.KubeConfig; + public kubeClient!: k8s.CoreV1Api; + public kubeClientBatch!: k8s.BatchV1Api; + public buildGuid: string = ''; + public buildParameters!: BuildParameters; + public pvcName: string = ''; + public secretName: string = ''; + public jobName: string = ''; + public namespace!: string; + public podName: string = ''; + public containerName: string = ''; + public cleanupCronJobName: string = ''; + public serviceAccountName: string = ''; + + // eslint-disable-next-line no-unused-vars + constructor(buildParameters: BuildParameters) { + Kubernetes.Instance = this; this.kubeConfig = new k8s.KubeConfig(); this.kubeConfig.loadFromDefault(); - this.kubeClient = this.kubeConfig.makeApiClient(k8sTypes.CoreV1Api); + this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api); this.kubeClientBatch = this.kubeConfig.makeApiClient(k8s.BatchV1Api); + this.namespace = 'default'; CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment'); + } - this.namespace = 'default'; - this.buildParameters = buildParameters; + async listResources(): Promise { + const pods = await this.kubeClient.listNamespacedPod(this.namespace); + const serviceAccounts = await this.kubeClient.listNamespacedServiceAccount(this.namespace); + const secrets = await this.kubeClient.listNamespacedSecret(this.namespace); + const jobs = await this.kubeClientBatch.listNamespacedJob(this.namespace); + + return [ + ...pods.body.items.map((x) => { + return { Name: x.metadata?.name || `` }; + }), + ...serviceAccounts.body.items.map((x) => { + return { Name: x.metadata?.name || `` }; + }), + ...secrets.body.items.map((x) => { + return { Name: x.metadata?.name || `` }; + }), + ...jobs.body.items.map((x) => { + return { Name: x.metadata?.name || `` }; + }), + ]; + } + listWorkflow(): Promise { + throw new Error('Method not implemented.'); } - public async setup( + watchWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + garbageCollect( + // eslint-disable-next-line no-unused-vars + filter: string, + // eslint-disable-next-line no-unused-vars + previewOnly: boolean, + // eslint-disable-next-line no-unused-vars + olderThan: Number, + // eslint-disable-next-line no-unused-vars + fullCache: boolean, + // eslint-disable-next-line no-unused-vars + baseDependencies: boolean, + ): Promise { + return new Promise((result) => result(``)); + } + public async setupWorkflow( buildGuid: string, - buildParameters: Parameters, + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) { try { - this.pvcName = `unity-builder-pvc-${buildGuid}`; - this.cleanupCronJobName = `unity-builder-cronjob-${buildGuid}`; - this.serviceAccountName = `service-account-${buildGuid}`; - if (await DependencyOverrideService.CheckHealth()) { - await DependencyOverrideService.TryStartDependencies(); - } + this.buildParameters = buildParameters; + const id = buildParameters.retainWorkspace ? CloudRunner.lockedWorkspace : buildParameters.buildGuid; + this.pvcName = `unity-builder-pvc-${id}`; + this.cleanupCronJobName = `unity-builder-cronjob-${id}`; + this.serviceAccountName = `service-account-${buildParameters.buildGuid}`; await KubernetesStorage.createPersistentVolumeClaim( buildParameters, this.pvcName, @@ -62,7 +111,7 @@ class Kubernetes implements ProviderInterface { } } - async runTask( + async runTaskInWorkflow( buildGuid: string, image: string, commands: string, @@ -72,40 +121,22 @@ class Kubernetes implements ProviderInterface { secrets: CloudRunnerSecret[], ): Promise { try { + CloudRunnerLogger.log('Cloud Runner K8s workflow!'); + // Setup this.buildGuid = buildGuid; - this.secretName = `build-credentials-${buildGuid}`; - this.jobName = `unity-builder-job-${buildGuid}`; + this.secretName = `build-credentials-${this.buildGuid}`; + this.jobName = `unity-builder-job-${this.buildGuid}`; this.containerName = `main`; await KubernetesSecret.createSecret(secrets, this.secretName, this.namespace, this.kubeClient); - const jobSpec = KubernetesJobSpecFactory.getJobSpec( - commands, - image, - mountdir, - workingdir, - environment, - secrets, - this.buildGuid, - this.buildParameters, - this.secretName, - this.pvcName, - this.jobName, - k8s, - ); - - // Run - const jobResult = await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec); - CloudRunnerLogger.log(`Creating build job ${JSON.stringify(jobResult.body.metadata, undefined, 4)}`); - - await new Promise((promise) => setTimeout(promise, 5000)); - CloudRunnerLogger.log('Job created'); + await this.createNamespacedJob(commands, image, mountdir, workingdir, environment, secrets); this.setPodNameAndContainerName(await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace)); CloudRunnerLogger.log('Watching pod until running'); + await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace); let output = ''; // eslint-disable-next-line no-constant-condition while (true) { try { - await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace); CloudRunnerLogger.log('Pod running, streaming logs'); output = await KubernetesTaskRunner.runTask( this.kubeConfig, @@ -115,9 +146,31 @@ class Kubernetes implements ProviderInterface { 'main', this.namespace, ); - break; + const running = await KubernetesPods.IsPodRunning(this.podName, this.namespace, this.kubeClient); + + if (!running) { + CloudRunnerLogger.log(`Pod not found, assumed ended!`); + break; + } else { + CloudRunnerLogger.log('Pod still running, recovering stream...'); + } } catch (error: any) { - if (!error.message.includes(`HTTP`)) { + let errorParsed; + try { + errorParsed = JSON.parse(error); + } catch { + errorParsed = error; + } + const reason = errorParsed.reason || errorParsed.response?.body?.reason || ``; + const errorMessage = errorParsed.message || ``; + + const continueStreaming = reason === `NotFound` || errorMessage.includes(`dial timeout, backstop`); + if (continueStreaming) { + CloudRunnerLogger.log('Log Stream Container Not Found'); + await new Promise((resolve) => resolve(5000)); + continue; + } else { + CloudRunnerLogger.log(`error running k8s workflow ${error}`); throw error; } } @@ -127,12 +180,50 @@ class Kubernetes implements ProviderInterface { return output; } catch (error) { CloudRunnerLogger.log('Running job failed'); - log.error(JSON.stringify(error, undefined, 4)); + core.error(JSON.stringify(error, undefined, 4)); await this.cleanupTaskResources(); throw error; } } + private async createNamespacedJob( + commands: string, + image: string, + mountdir: string, + workingdir: string, + environment: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ) { + for (let index = 0; index < 3; index++) { + try { + const jobSpec = KubernetesJobSpecFactory.getJobSpec( + commands, + image, + mountdir, + workingdir, + environment, + secrets, + this.buildGuid, + this.buildParameters, + this.secretName, + this.pvcName, + this.jobName, + k8s, + ); + await new Promise((promise) => setTimeout(promise, 15000)); + await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec); + CloudRunnerLogger.log(`Build job created`); + await new Promise((promise) => setTimeout(promise, 5000)); + CloudRunnerLogger.log('Job created'); + + return; + } catch (error) { + CloudRunnerLogger.log(`Error occured creating job: ${error}`); + throw error; + } + } + } + setPodNameAndContainerName(pod: k8s.V1Pod) { this.podName = pod.metadata?.name || ''; this.containerName = pod.status?.containerStatuses?.[0].name || ''; @@ -143,46 +234,47 @@ class Kubernetes implements ProviderInterface { try { await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace); await this.kubeClient.deleteNamespacedPod(this.podName, this.namespace); - await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace); - await new Promise((promise) => setTimeout(promise, 5000)); - } catch (error) { - CloudRunnerLogger.log('Failed to cleanup, error:'); - log.error(JSON.stringify(error, undefined, 4)); - CloudRunnerLogger.log('Abandoning cleanup, build error:'); - throw error; + } catch (error: any) { + CloudRunnerLogger.log(`Failed to cleanup`); + if (error.response.body.reason !== `NotFound`) { + CloudRunnerLogger.log(`Wasn't a not found error: ${error.response.body.reason}`); + throw error; + } } try { - await waitUntil( - async () => { - const { body: jobBody } = await this.kubeClientBatch.readNamespacedJob(this.jobName, this.namespace); - const { body: podBody } = await this.kubeClient.readNamespacedPod(this.podName, this.namespace); - - return (jobBody === null || jobBody.status?.active === 0) && podBody === null; - }, - { - timeout: 500_000, - intervalBetweenAttempts: 15_000, - }, - ); - } catch { - log.debug('Moved into empty catch block'); + await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace); + } catch (error: any) { + CloudRunnerLogger.log(`Failed to cleanup secret`); + CloudRunnerLogger.log(error.response.body.reason); } + CloudRunnerLogger.log('cleaned up Secret, Job and Pod'); + CloudRunnerLogger.log('cleaning up finished'); } - async cleanup( + async cleanupWorkflow( buildGuid: string, - buildParameters: Parameters, + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) { + if (buildParameters.retainWorkspace) { + return; + } CloudRunnerLogger.log(`deleting PVC`); - await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace); - await Output.setBuildVersion(buildParameters.buildVersion); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(); + + try { + await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace); + await this.kubeClient.deleteNamespacedServiceAccount(this.serviceAccountName, this.namespace); + CloudRunnerLogger.log('cleaned up PVC and Service Account'); + } catch (error: any) { + CloudRunnerLogger.log(`Cleanup failed ${JSON.stringify(error, undefined, 4)}`); + throw error; + } } - static async findPodFromJob(kubeClient: k8sTypes.CoreV1Api, jobName: string, namespace: string) { + static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) { const namespacedPods = await kubeClient.listNamespacedPod(namespace); const pod = namespacedPods.body.items.find((x) => x.metadata?.labels?.['job-name'] === jobName); if (pod === undefined) { diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts b/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts index dc1e90a..8d39b7d 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-job-spec-factory.ts @@ -1,9 +1,9 @@ -import { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } from '../../../../dependencies.ts'; -import Parameters from '../../../parameters.ts'; -import { CloudRunnerBuildCommandProcessor } from '../../services/cloud-runner-build-command-process.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; -import CloudRunner from '../../cloud-runner.ts'; +import { V1EnvVar, V1EnvVarSource, V1SecretKeySelector } from '@kubernetes/client-node'; +import BuildParameters from '../../../build-parameters'; +import { CloudRunnerCustomHooks } from '../../services/cloud-runner-custom-hooks'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import CloudRunner from '../../cloud-runner'; class KubernetesJobSpecFactory { static getJobSpec( @@ -14,65 +14,67 @@ class KubernetesJobSpecFactory { environment: CloudRunnerEnvironmentVariable[], secrets: CloudRunnerSecret[], buildGuid: string, - buildParameters: Parameters, + buildParameters: BuildParameters, secretName, pvcName, jobName, k8s, ) { environment.push( - { - name: 'GITHUB_SHA', - value: buildGuid, - }, - { - name: 'GITHUB_WORKSPACE', - value: '/data/repo', - }, - { - name: 'PROJECT_PATH', - value: buildParameters.projectPath, - }, - { - name: 'BUILD_PATH', - value: buildParameters.buildPath, - }, - { - name: 'BUILD_FILE', - value: buildParameters.buildFile, - }, - { - name: 'BUILD_NAME', - value: buildParameters.buildName, - }, - { - name: 'BUILD_METHOD', - value: buildParameters.buildMethod, - }, - { - name: 'CUSTOM_PARAMETERS', - value: buildParameters.customParameters, - }, - { - name: 'CHOWN_FILES_TO', - value: buildParameters.chownFilesTo, - }, - { - name: 'BUILD_TARGET', - value: buildParameters.targetPlatform, - }, - { - name: 'ANDROID_VERSION_CODE', - value: buildParameters.androidVersionCode.toString(), - }, - { - name: 'ANDROID_KEYSTORE_NAME', - value: buildParameters.androidKeystoreName, - }, - { - name: 'ANDROID_KEYALIAS_NAME', - value: buildParameters.androidKeyaliasName, - }, + ...[ + { + name: 'GITHUB_SHA', + value: buildGuid, + }, + { + name: 'GITHUB_WORKSPACE', + value: '/data/repo', + }, + { + name: 'PROJECT_PATH', + value: buildParameters.projectPath, + }, + { + name: 'BUILD_PATH', + value: buildParameters.buildPath, + }, + { + name: 'BUILD_FILE', + value: buildParameters.buildFile, + }, + { + name: 'BUILD_NAME', + value: buildParameters.buildName, + }, + { + name: 'BUILD_METHOD', + value: buildParameters.buildMethod, + }, + { + name: 'CUSTOM_PARAMETERS', + value: buildParameters.customParameters, + }, + { + name: 'CHOWN_FILES_TO', + value: buildParameters.chownFilesTo, + }, + { + name: 'BUILD_TARGET', + value: buildParameters.targetPlatform, + }, + { + name: 'ANDROID_VERSION_CODE', + value: buildParameters.androidVersionCode.toString(), + }, + { + name: 'ANDROID_KEYSTORE_NAME', + value: buildParameters.androidKeystoreName, + }, + { + name: 'ANDROID_KEYALIAS_NAME', + value: buildParameters.androidKeyaliasName, + }, + ], ); const job = new k8s.V1Job(); job.apiVersion = 'batch/v1'; @@ -101,7 +103,7 @@ class KubernetesJobSpecFactory { name: 'main', image, command: ['/bin/sh'], - args: ['-c', CloudRunnerBuildCommandProcessor.ProcessCommands(command, CloudRunner.buildParameters)], + args: ['-c', CloudRunnerCustomHooks.ApplyHooksToCommands(command, CloudRunner.buildParameters)], workingDir: `${workingDirectory}`, resources: { @@ -112,7 +114,7 @@ class KubernetesJobSpecFactory { }, env: [ ...environment.map((x) => { - const environmentVariable = new k8s.V1EnvVar(); + const environmentVariable = new V1EnvVar(); environmentVariable.name = x.name; environmentVariable.value = x.value; diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts b/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts new file mode 100644 index 0000000..d9b24fc --- /dev/null +++ b/src/model/cloud-runner/providers/k8s/kubernetes-pods.ts @@ -0,0 +1,14 @@ +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { CoreV1Api } from '@kubernetes/client-node'; +class KubernetesPods { + public static async IsPodRunning(podName: string, namespace: string, kubeClient: CoreV1Api) { + const pods = (await kubeClient.listNamespacedPod(namespace)).body.items.filter((x) => podName === x.metadata?.name); + const running = pods.length > 0 && (pods[0].status?.phase === `Running` || pods[0].status?.phase === `Pending`); + const phase = pods[0].status?.phase || 'undefined status'; + CloudRunnerLogger.log(`Getting pod status: ${phase}`); + + return running; + } +} + +export default KubernetesPods; diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts b/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts index f439796..d17301d 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-secret.ts @@ -1,26 +1,44 @@ -import { k8sTypes, k8s, base64 } from '../../../../dependencies.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; +import { CoreV1Api } from '@kubernetes/client-node'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import * as k8s from '@kubernetes/client-node'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +const base64 = require('base-64'); class KubernetesSecret { static async createSecret( secrets: CloudRunnerSecret[], secretName: string, namespace: string, - kubeClient: k8sTypes.CoreV1Api, + kubeClient: CoreV1Api, ) { - const secret = new k8s.V1Secret(); - secret.apiVersion = 'v1'; - secret.kind = 'Secret'; - secret.type = 'Opaque'; - secret.metadata = { - name: secretName, - }; - secret.data = {}; - for (const buildSecret of secrets) { - secret.data[buildSecret.ParameterKey] = base64.encode(buildSecret.ParameterValue); - } + try { + const secret = new k8s.V1Secret(); + secret.apiVersion = 'v1'; + secret.kind = 'Secret'; + secret.type = 'Opaque'; + secret.metadata = { + name: secretName, + }; + secret.data = {}; + for (const buildSecret of secrets) { + secret.data[buildSecret.ParameterKey] = base64.encode(buildSecret.ParameterValue); + } + CloudRunnerLogger.log(`Creating secret: ${secretName}`); + const existingSecrets = await kubeClient.listNamespacedSecret(namespace); + const mappedSecrets = existingSecrets.body.items.map((x) => { + return x.metadata?.name || `no name`; + }); - return kubeClient.createNamespacedSecret(namespace, secret); + CloudRunnerLogger.log( + `ExistsAlready: ${mappedSecrets.includes(secretName)} SecretsCount: ${mappedSecrets.length}`, + ); + await new Promise((promise) => setTimeout(promise, 15000)); + await kubeClient.createNamespacedSecret(namespace, secret); + CloudRunnerLogger.log('Created secret'); + } catch (error) { + CloudRunnerLogger.log(`Created secret failed ${error}`); + throw new Error(`Failed to create kubernetes secret`); + } } } diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-service-account.ts b/src/model/cloud-runner/providers/k8s/kubernetes-service-account.ts index 225577e..311027f 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-service-account.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-service-account.ts @@ -1,7 +1,8 @@ -import { k8s, k8sTypes } from '../../../../dependencies.ts'; +import { CoreV1Api } from '@kubernetes/client-node'; +import * as k8s from '@kubernetes/client-node'; class KubernetesServiceAccount { - static async createServiceAccount(serviceAccountName: string, namespace: string, kubeClient: k8sTypes.CoreV1Api) { + static async createServiceAccount(serviceAccountName: string, namespace: string, kubeClient: CoreV1Api) { const serviceAccount = new k8s.V1ServiceAccount(); serviceAccount.apiVersion = 'v1'; serviceAccount.kind = 'ServiceAccount'; diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts b/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts index c450a75..41533e8 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-storage.ts @@ -1,28 +1,31 @@ -import { k8sTypes, k8s, core, yaml, waitUntil, http } from '../../../../dependencies.ts'; -import Parameters from '../../../parameters.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; +import waitUntil from 'async-wait-until'; +import * as core from '@actions/core'; +import * as k8s from '@kubernetes/client-node'; +import BuildParameters from '../../../build-parameters'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { IncomingMessage } from 'http'; +import GitHub from '../../../github'; class KubernetesStorage { public static async createPersistentVolumeClaim( - buildParameters: Parameters, + buildParameters: BuildParameters, pvcName: string, - kubeClient: k8sTypes.CoreV1Api, + kubeClient: k8s.CoreV1Api, namespace: string, ) { - if (buildParameters.kubeVolume) { - CloudRunnerLogger.log(buildParameters.kubeVolume); + if (buildParameters.kubeVolume !== ``) { + CloudRunnerLogger.log(`Kube Volume was input was set ${buildParameters.kubeVolume} overriding ${pvcName}`); pvcName = buildParameters.kubeVolume; return; } - - const listedPvcs = await kubeClient.listNamespacedPersistentVolumeClaim(namespace); - const pvcList = listedPvcs.body.items.map((x) => x.metadata?.name); + const allPvc = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items; + const pvcList = allPvc.map((x) => x.metadata?.name); CloudRunnerLogger.log(`Current PVCs in namespace ${namespace}`); CloudRunnerLogger.log(JSON.stringify(pvcList, undefined, 4)); if (pvcList.includes(pvcName)) { CloudRunnerLogger.log(`pvc ${pvcName} already exists`); - if (!buildParameters.isCliMode) { + if (GitHub.githubInputEnabled) { core.setOutput('volume', pvcName); } @@ -33,19 +36,17 @@ class KubernetesStorage { await KubernetesStorage.handleResult(result, kubeClient, namespace, pvcName); } - public static async getPVCPhase(kubeClient: k8sTypes.CoreV1Api, name: string, namespace: string) { + public static async getPVCPhase(kubeClient: k8s.CoreV1Api, name: string, namespace: string) { try { - const pvc = await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace); - - return pvc.body.status?.phase; + return (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body.status?.phase; } catch (error) { - log.error('Failed to get PVC phase'); - log.error(JSON.stringify(error, undefined, 4)); + core.error('Failed to get PVC phase'); + core.error(JSON.stringify(error, undefined, 4)); throw error; } } - public static async watchUntilPVCNotPending(kubeClient: k8sTypes.CoreV1Api, name: string, namespace: string) { + public static async watchUntilPVCNotPending(kubeClient: k8s.CoreV1Api, name: string, namespace: string) { try { CloudRunnerLogger.log(`watch Until PVC Not Pending ${name} ${namespace}`); CloudRunnerLogger.log(`${await this.getPVCPhase(kubeClient, name, namespace)}`); @@ -54,23 +55,28 @@ class KubernetesStorage { return (await this.getPVCPhase(kubeClient, name, namespace)) === 'Pending'; }, { - timeout: 750_000, - intervalBetweenAttempts: 15_000, + timeout: 750000, + intervalBetweenAttempts: 15000, }, ); } catch (error: any) { - log.error('Failed to watch PVC'); - log.error(error.toString()); - const pvc = await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace); - log.error(`PVC Body: ${JSON.stringify(pvc.body, undefined, 4)}`); + core.error('Failed to watch PVC'); + core.error(error.toString()); + core.error( + `PVC Body: ${JSON.stringify( + (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body, + undefined, + 4, + )}`, + ); throw error; } } private static async createPVC( pvcName: string, - buildParameters: Parameters, - kubeClient: k8sTypes.CoreV1Api, + buildParameters: BuildParameters, + kubeClient: k8s.CoreV1Api, namespace: string, ) { const pvc = new k8s.V1PersistentVolumeClaim(); @@ -88,17 +94,14 @@ class KubernetesStorage { }, }, }; - if (Deno.env.get('K8s_STORAGE_PVC_SPEC')) { - yaml.parse(Deno.env.get('K8s_STORAGE_PVC_SPEC')); - } const result = await kubeClient.createNamespacedPersistentVolumeClaim(namespace, pvc); return result; } private static async handleResult( - result: { response: http.IncomingMessage; body: k8s.V1PersistentVolumeClaim }, - kubeClient: k8sTypes.CoreV1Api, + result: { response: IncomingMessage; body: k8s.V1PersistentVolumeClaim }, + kubeClient: k8s.CoreV1Api, namespace: string, pvcName: string, ) { diff --git a/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts b/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts index ccb2074..da70be1 100644 --- a/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts +++ b/src/model/cloud-runner/providers/k8s/kubernetes-task-runner.ts @@ -1,12 +1,15 @@ -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { k8sTypes, k8s, Writable, waitUntil } from '../../../../dependencies.ts'; -import { CloudRunnerStatics } from '../../cloud-runner-statics.ts'; -import { FollowLogStreamService } from '../../services/follow-log-stream-service.ts'; +import { CoreV1Api, KubeConfig, Log } from '@kubernetes/client-node'; +import { Writable } from 'stream'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import * as core from '@actions/core'; +import { CloudRunnerStatics } from '../../cloud-runner-statics'; +import waitUntil from 'async-wait-until'; +import { FollowLogStreamService } from '../../services/follow-log-stream-service'; class KubernetesTaskRunner { static async runTask( - kubeConfig: k8sTypes.KubeConfig, - kubeClient: k8sTypes.CoreV1Api, + kubeConfig: KubeConfig, + kubeClient: CoreV1Api, jobName: string, podName: string, containerName: string, @@ -37,19 +40,18 @@ class KubernetesTaskRunner { }; try { const resultError = await new Promise((resolve) => - new k8s.Log(kubeConfig).log(namespace, podName, containerName, stream, resolve, logOptions), + new Log(kubeConfig).log(namespace, podName, containerName, stream, resolve, logOptions), ); stream.destroy(); if (resultError) { throw resultError; } if (!didStreamAnyLogs) { - log.error('Failed to stream any logs, listing namespace events, check for an error with the container'); - const listedEvents = await kubeClient.listNamespacedEvent(namespace); - log.error( + core.error('Failed to stream any logs, listing namespace events, check for an error with the container'); + core.error( JSON.stringify( { - events: listedEvents.body.items + events: (await kubeClient.listNamespacedEvent(namespace)).body.items .filter((x) => { return x.involvedObject.name === podName || x.involvedObject.name === jobName; }) @@ -78,7 +80,7 @@ class KubernetesTaskRunner { return output; } - static async watchUntilPodRunning(kubeClient: k8sTypes.CoreV1Api, podName: string, namespace: string) { + static async watchUntilPodRunning(kubeClient: CoreV1Api, podName: string, namespace: string) { let success: boolean = false; CloudRunnerLogger.log(`Watching ${podName} ${namespace}`); await waitUntil( @@ -91,12 +93,13 @@ class KubernetesTaskRunner { status.body.status?.conditions?.[0].message || '' }`, ); + if (success || phase !== 'Pending') return true; - return success || phase !== 'Pending'; + return false; }, { - timeout: 2_000_000, - intervalBetweenAttempts: 15_000, + timeout: 2000000, + intervalBetweenAttempts: 15000, }, ); diff --git a/src/model/cloud-runner/providers/local-docker/index.ts b/src/model/cloud-runner/providers/local-docker/index.ts deleted file mode 100644 index 9e86695..0000000 --- a/src/model/cloud-runner/providers/local-docker/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Parameters from '../../../parameters.ts'; -import { CloudRunnerSystem } from '../../services/cloud-runner-system.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { ProviderInterface } from '../provider-interface.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; - -class LocalDockerCloudRunner implements ProviderInterface { - cleanup( - buildGuid: string, - buildParameters: Parameters, - branchName: string, - defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) {} - setup( - buildGuid: string, - buildParameters: Parameters, - branchName: string, - defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], - ) {} - public runTask( - commands: string, - buildGuid: string, - image: string, - mountdir: string, - workingdir: string, - environment: CloudRunnerEnvironmentVariable[], - secrets: CloudRunnerSecret[], - ): Promise { - CloudRunnerLogger.log(buildGuid); - CloudRunnerLogger.log(commands); - - return CloudRunnerSystem.Run(commands, false, false); - } -} -export default LocalDockerCloudRunner; diff --git a/src/model/cloud-runner/providers/local/index.ts b/src/model/cloud-runner/providers/local/index.ts index d958624..9effda2 100644 --- a/src/model/cloud-runner/providers/local/index.ts +++ b/src/model/cloud-runner/providers/local/index.ts @@ -1,30 +1,67 @@ -import Parameters from '../../../parameters.ts'; -import { CloudRunnerSystem } from '../../services/cloud-runner-system.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { ProviderInterface } from '../provider-interface.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; +import BuildParameters from '../../../build-parameters'; +import { CloudRunnerSystem } from '../../services/cloud-runner-system'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { ProviderInterface } from '../provider-interface'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import { ProviderResource } from '../provider-resource'; +import { ProviderWorkflow } from '../provider-workflow'; class LocalCloudRunner implements ProviderInterface { - cleanup( + listResources(): Promise { + throw new Error('Method not implemented.'); + } + listWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + watchWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + garbageCollect( + // eslint-disable-next-line no-unused-vars + filter: string, + // eslint-disable-next-line no-unused-vars + previewOnly: boolean, + // eslint-disable-next-line no-unused-vars + olderThan: Number, + // eslint-disable-next-line no-unused-vars + fullCache: boolean, + // eslint-disable-next-line no-unused-vars + baseDependencies: boolean, + ): Promise { + throw new Error('Method not implemented.'); + } + cleanupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) {} - public setup( + public setupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) {} - public async runTask( + public async runTaskInWorkflow( buildGuid: string, image: string, commands: string, + // eslint-disable-next-line no-unused-vars mountdir: string, + // eslint-disable-next-line no-unused-vars workingdir: string, + // eslint-disable-next-line no-unused-vars environment: CloudRunnerEnvironmentVariable[], + // eslint-disable-next-line no-unused-vars secrets: CloudRunnerSecret[], ): Promise { CloudRunnerLogger.log(image); diff --git a/src/model/cloud-runner/providers/provider-interface.ts b/src/model/cloud-runner/providers/provider-interface.ts index a82b1f9..057b8d2 100644 --- a/src/model/cloud-runner/providers/provider-interface.ts +++ b/src/model/cloud-runner/providers/provider-interface.ts @@ -1,27 +1,59 @@ -import Parameters from '../../parameters.ts'; -import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable.ts'; -import CloudRunnerSecret from '../services/cloud-runner-secret.ts'; +import BuildParameters from '../../build-parameters'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { ProviderResource } from './provider-resource'; +import { ProviderWorkflow } from './provider-workflow'; export interface ProviderInterface { - cleanup( + cleanupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ); - setup( + setupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ); - runTask( + runTaskInWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, + // eslint-disable-next-line no-unused-vars image: string, + // eslint-disable-next-line no-unused-vars commands: string, + // eslint-disable-next-line no-unused-vars mountdir: string, + // eslint-disable-next-line no-unused-vars workingdir: string, + // eslint-disable-next-line no-unused-vars environment: CloudRunnerEnvironmentVariable[], + // eslint-disable-next-line no-unused-vars secrets: CloudRunnerSecret[], ): Promise; + garbageCollect( + // eslint-disable-next-line no-unused-vars + filter: string, + // eslint-disable-next-line no-unused-vars + previewOnly: boolean, + // eslint-disable-next-line no-unused-vars + olderThan: Number, + // eslint-disable-next-line no-unused-vars + fullCache: boolean, + // eslint-disable-next-line no-unused-vars + baseDependencies: boolean, + ): Promise; + listResources(): Promise; + listWorkflow(): Promise; + watchWorkflow(): Promise; } diff --git a/src/model/cloud-runner/providers/provider-resource.ts b/src/model/cloud-runner/providers/provider-resource.ts new file mode 100644 index 0000000..3765cb5 --- /dev/null +++ b/src/model/cloud-runner/providers/provider-resource.ts @@ -0,0 +1,3 @@ +export class ProviderResource { + public Name!: string; +} diff --git a/src/model/cloud-runner/providers/provider-workflow.ts b/src/model/cloud-runner/providers/provider-workflow.ts new file mode 100644 index 0000000..0096be0 --- /dev/null +++ b/src/model/cloud-runner/providers/provider-workflow.ts @@ -0,0 +1,3 @@ +export class ProviderWorkflow { + public Name!: string; +} diff --git a/src/model/cloud-runner/providers/test/index.ts b/src/model/cloud-runner/providers/test/index.ts index c4325ff..ccabb83 100644 --- a/src/model/cloud-runner/providers/test/index.ts +++ b/src/model/cloud-runner/providers/test/index.ts @@ -1,29 +1,60 @@ -import Parameters from '../../../parameters.ts'; -import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable.ts'; -import CloudRunnerLogger from '../../services/cloud-runner-logger.ts'; -import { ProviderInterface } from '../provider-interface.ts'; -import CloudRunnerSecret from '../../services/cloud-runner-secret.ts'; +import BuildParameters from '../../../build-parameters'; +import CloudRunnerEnvironmentVariable from '../../services/cloud-runner-environment-variable'; +import CloudRunnerLogger from '../../services/cloud-runner-logger'; +import { ProviderInterface } from '../provider-interface'; +import CloudRunnerSecret from '../../services/cloud-runner-secret'; +import { ProviderResource } from '../provider-resource'; +import { ProviderWorkflow } from '../provider-workflow'; class TestCloudRunner implements ProviderInterface { - cleanup( + listResources(): Promise { + throw new Error('Method not implemented.'); + } + listWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + watchWorkflow(): Promise { + throw new Error('Method not implemented.'); + } + garbageCollect( + // eslint-disable-next-line no-unused-vars + filter: string, + // eslint-disable-next-line no-unused-vars + previewOnly: boolean, + ): Promise { + throw new Error('Method not implemented.'); + } + cleanupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) {} - setup( + setupWorkflow( + // eslint-disable-next-line no-unused-vars buildGuid: string, - buildParameters: Parameters, + // eslint-disable-next-line no-unused-vars + buildParameters: BuildParameters, + // eslint-disable-next-line no-unused-vars branchName: string, + // eslint-disable-next-line no-unused-vars defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], ) {} - public async runTask( + public async runTaskInWorkflow( commands: string, buildGuid: string, image: string, + // eslint-disable-next-line no-unused-vars mountdir: string, + // eslint-disable-next-line no-unused-vars workingdir: string, + // eslint-disable-next-line no-unused-vars environment: CloudRunnerEnvironmentVariable[], + // eslint-disable-next-line no-unused-vars secrets: CloudRunnerSecret[], ): Promise { CloudRunnerLogger.log(image); diff --git a/src/model/cloud-runner/remote-client/caching.ts b/src/model/cloud-runner/remote-client/caching.ts index 9ae5d7f..fa8314b 100644 --- a/src/model/cloud-runner/remote-client/caching.ts +++ b/src/model/cloud-runner/remote-client/caching.ts @@ -1,12 +1,14 @@ -import { fs, path, assert } from '../../../dependencies.ts'; -import CloudRunner from '../cloud-runner.ts'; -import CloudRunnerLogger from '../services/cloud-runner-logger.ts'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders.ts'; -import { CloudRunnerSystem } from '../services/cloud-runner-system.ts'; -import { LfsHashing } from '../services/lfs-hashing.ts'; -import { RemoteClientLogger } from './remote-client-logger.ts'; -import { Cli } from '../../cli/cli.ts'; -import { CliFunction } from '../../cli/cli-functions-repository.ts'; +import { assert } from 'console'; +import fs from 'fs'; +import path from 'path'; +import CloudRunner from '../cloud-runner'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import { LfsHashing } from '../services/lfs-hashing'; +import { RemoteClientLogger } from './remote-client-logger'; +import { Cli } from '../../cli/cli'; +import { CliFunction } from '../../cli/cli-functions-repository'; // eslint-disable-next-line github/no-then const fileExists = async (fpath) => !!(await fs.promises.stat(fpath).catch(() => false)); @@ -14,7 +16,7 @@ export class Caching { @CliFunction(`cache-push`, `push to cache`) static async cachePush() { try { - const buildParameter = JSON.parse(Deno.env.get('BUILD_PARAMETERS') || '{}'); + const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}'); CloudRunner.buildParameters = buildParameter; await Caching.PushToCache( Cli.options['cachePushTo'], @@ -29,7 +31,7 @@ export class Caching { @CliFunction(`cache-pull`, `pull from cache`) static async cachePull() { try { - const buildParameter = JSON.parse(Deno.env.get('BUILD_PARAMETERS') || '{}'); + const buildParameter = JSON.parse(process.env.BUILD_PARAMETERS || '{}'); CloudRunner.buildParameters = buildParameter; await Caching.PullFromCache( Cli.options['cachePushFrom'], @@ -44,40 +46,49 @@ export class Caching { public static async PushToCache(cacheFolder: string, sourceFolder: string, cacheArtifactName: string) { cacheArtifactName = cacheArtifactName.replace(' ', ''); const startPath = process.cwd(); + const compressionSuffix = CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''; try { if (!(await fileExists(cacheFolder))) { await CloudRunnerSystem.Run(`mkdir -p ${cacheFolder}`); } process.chdir(path.resolve(sourceFolder, '..')); - if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) { + if (CloudRunner.buildParameters.cloudRunnerDebug) { CloudRunnerLogger.log( `Hashed cache folder ${await LfsHashing.hashAllFiles(sourceFolder)} ${sourceFolder} ${path.basename( sourceFolder, )}`, ); } - // eslint-disable-next-line func-style - const formatFunction = function (format: string) { - const arguments_ = Array.prototype.slice.call( - [path.resolve(sourceFolder, '..'), cacheFolder, cacheArtifactName], - 1, + const contents = await fs.promises.readdir(path.basename(sourceFolder)); + CloudRunnerLogger.log( + `There is ${contents.length} files/dir in the source folder ${path.basename(sourceFolder)}`, + ); + + if (CloudRunner.buildParameters.cloudRunnerDebug) { + // await CloudRunnerSystem.Run(`tree -L 2 ./..`); + // await CloudRunnerSystem.Run(`tree -L 2`); + } + + if (contents.length === 0) { + CloudRunnerLogger.log( + `Did not push source folder to cache because it was empty ${path.basename(sourceFolder)}`, ); + process.chdir(`${startPath}`); - return format.replace(/{(\d+)}/g, function (match, number) { - return typeof arguments_[number] != 'undefined' ? arguments_[number] : match; - }); - }; - await CloudRunnerSystem.Run(`tar -cf ${cacheArtifactName}.tar ${path.basename(sourceFolder)}`); - assert(await fileExists(`${cacheArtifactName}.tar`), 'cache archive exists'); - assert(await fileExists(path.basename(sourceFolder)), 'source folder exists'); - if (CloudRunner.buildParameters.cachePushOverrideCommand) { - await CloudRunnerSystem.Run(formatFunction(CloudRunner.buildParameters.cachePushOverrideCommand)); + return; } - await CloudRunnerSystem.Run(`mv ${cacheArtifactName}.tar ${cacheFolder}`); + + await CloudRunnerSystem.Run( + `tar -cf ${cacheArtifactName}.tar${compressionSuffix} ${path.basename(sourceFolder)}`, + ); + await CloudRunnerSystem.Run(`du ${cacheArtifactName}.tar${compressionSuffix}`); + assert(await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`), 'cache archive exists'); + assert(await fileExists(path.basename(sourceFolder)), 'source folder exists'); + await CloudRunnerSystem.Run(`mv ${cacheArtifactName}.tar${compressionSuffix} ${cacheFolder}`); RemoteClientLogger.log(`moved cache entry ${cacheArtifactName} to ${cacheFolder}`); assert( - await fileExists(`${path.join(cacheFolder, cacheArtifactName)}.tar`), + await fileExists(`${path.join(cacheFolder, cacheArtifactName)}.tar${compressionSuffix}`), 'cache archive exists inside cache folder', ); } catch (error) { @@ -88,7 +99,7 @@ export class Caching { } public static async PullFromCache(cacheFolder: string, destinationFolder: string, cacheArtifactName: string = ``) { cacheArtifactName = cacheArtifactName.replace(' ', ''); - + const compressionSuffix = CloudRunner.buildParameters.useLz4Compression ? '.lz4' : ''; const startPath = process.cwd(); RemoteClientLogger.log(`Caching for ${path.basename(destinationFolder)}`); try { @@ -100,35 +111,26 @@ export class Caching { await fs.promises.mkdir(destinationFolder); } - const latestInBranchRaw = await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .tar$ | head -1`); - const latestInBranch = latestInBranchRaw.replace(/\n/g, ``).replace('.tar', ''); + const latestInBranch = await ( + await CloudRunnerSystem.Run(`ls -t "${cacheFolder}" | grep .tar${compressionSuffix}$ | head -1`) + ) + .replace(/\n/g, ``) + .replace(`.tar${compressionSuffix}`, ''); process.chdir(cacheFolder); + const cacheSelection = - cacheArtifactName !== `` && (await fileExists(`${cacheArtifactName}.tar`)) ? cacheArtifactName : latestInBranch; + cacheArtifactName !== `` && (await fileExists(`${cacheArtifactName}.tar${compressionSuffix}`)) + ? cacheArtifactName + : latestInBranch; await CloudRunnerLogger.log(`cache key ${cacheArtifactName} selection ${cacheSelection}`); - const formatFunction = (format: string) => { - const arguments_ = Array.prototype.slice.call( - [path.resolve(destinationFolder, '..'), cacheFolder, cacheArtifactName], - 1, - ); - - return format.replace(/{(\d+)}/g, function (match, number) { - return typeof arguments_[number] != 'undefined' ? arguments_[number] : match; - }); - }; - - if (CloudRunner.buildParameters.cachePullOverrideCommand) { - await CloudRunnerSystem.Run(formatFunction(CloudRunner.buildParameters.cachePullOverrideCommand)); - } - - if (await fileExists(`${cacheSelection}.tar`)) { + if (await fileExists(`${cacheSelection}.tar${compressionSuffix}`)) { const resultsFolder = `results${CloudRunner.buildParameters.buildGuid}`; await CloudRunnerSystem.Run(`mkdir -p ${resultsFolder}`); - RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.tar`); + RemoteClientLogger.log(`cache item exists ${cacheFolder}/${cacheSelection}.tar${compressionSuffix}`); const fullResultsFolder = path.join(cacheFolder, resultsFolder); - await CloudRunnerSystem.Run(`tar -xf ${cacheSelection}.tar -C ${fullResultsFolder}`); + await CloudRunnerSystem.Run(`tar -xf ${cacheSelection}.tar${compressionSuffix} -C ${fullResultsFolder}`); RemoteClientLogger.log(`cache item extracted to ${fullResultsFolder}`); assert(await fileExists(fullResultsFolder), `cache extraction results folder exists`); const destinationParentFolder = path.resolve(destinationFolder, '..'); @@ -148,19 +150,21 @@ export class Caching { } else { RemoteClientLogger.logWarning(`cache item ${cacheArtifactName} doesn't exist ${destinationFolder}`); if (cacheSelection !== ``) { - RemoteClientLogger.logWarning(`cache item ${cacheArtifactName}.tar doesn't exist ${destinationFolder}`); + RemoteClientLogger.logWarning( + `cache item ${cacheArtifactName}.tar${compressionSuffix} doesn't exist ${destinationFolder}`, + ); throw new Error(`Failed to get cache item, but cache hit was found: ${cacheSelection}`); } } } catch (error) { - process.chdir(`${startPath}`); + process.chdir(startPath); throw error; } - process.chdir(`${startPath}`); + process.chdir(startPath); } public static async handleCachePurging() { - if (Deno.env.get('PURGE_REMOTE_BUILDER_CACHE') !== undefined) { + if (process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined) { RemoteClientLogger.log(`purging ${CloudRunnerFolders.purgeRemoteCaching}`); fs.promises.rmdir(CloudRunnerFolders.cacheFolder, { recursive: true }); } diff --git a/src/model/cloud-runner/remote-client/index.ts b/src/model/cloud-runner/remote-client/index.ts index 2b971b7..bfe126f 100644 --- a/src/model/cloud-runner/remote-client/index.ts +++ b/src/model/cloud-runner/remote-client/index.ts @@ -1,40 +1,51 @@ -import { fsSync as fs, assert, path } from '../../../dependencies.ts'; -import CloudRunner from '../cloud-runner.ts'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders.ts'; -import { Caching } from './caching.ts'; -import { LfsHashing } from '../services/lfs-hashing.ts'; -import { RemoteClientLogger } from './remote-client-logger.ts'; -import CloudRunnerLogger from '../services/cloud-runner-logger.ts'; -import { CliFunction } from '../../cli/cli-functions-repository.ts'; -import { CloudRunnerSystem } from '../services/cloud-runner-system.ts'; +import fs from 'fs'; +import CloudRunner from '../cloud-runner'; +import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import { Caching } from './caching'; +import { LfsHashing } from '../services/lfs-hashing'; +import { RemoteClientLogger } from './remote-client-logger'; +import path from 'path'; +import { assert } from 'console'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { CliFunction } from '../../cli/cli-functions-repository'; +import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import YAML from 'yaml'; export class RemoteClient { public static async bootstrapRepository() { try { - await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); - await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.repoPathAbsolute}`); - await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.cacheFolderFull}`); - process.chdir(CloudRunnerFolders.repoPathAbsolute); + await CloudRunnerSystem.Run(`mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}`); + await CloudRunnerSystem.Run( + `mkdir -p ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.cacheFolderForCacheKeyFull)}`, + ); + process.chdir(CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)); await RemoteClient.cloneRepoWithoutLFSFiles(); - await RemoteClient.sizeOfFolder('repo before lfs cache pull', CloudRunnerFolders.repoPathAbsolute); + RemoteClient.replaceLargePackageReferencesWithSharedReferences(); + await RemoteClient.sizeOfFolder( + 'repo before lfs cache pull', + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute), + ); const lfsHashes = await LfsHashing.createLFSHashFiles(); if (fs.existsSync(CloudRunnerFolders.libraryFolderAbsolute)) { RemoteClientLogger.logWarning(`!Warning!: The Unity library was included in the git repository`); } await Caching.PullFromCache( - CloudRunnerFolders.lfsCacheFolderFull, - CloudRunnerFolders.lfsFolderAbsolute, + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull), + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute), `${lfsHashes.lfsGuidSum}`, ); await RemoteClient.sizeOfFolder('repo after lfs cache pull', CloudRunnerFolders.repoPathAbsolute); await RemoteClient.pullLatestLFS(); await RemoteClient.sizeOfFolder('repo before lfs git pull', CloudRunnerFolders.repoPathAbsolute); await Caching.PushToCache( - CloudRunnerFolders.lfsCacheFolderFull, - CloudRunnerFolders.lfsFolderAbsolute, + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsCacheFolderFull), + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.lfsFolderAbsolute), `${lfsHashes.lfsGuidSum}`, ); - await Caching.PullFromCache(CloudRunnerFolders.libraryCacheFolderFull, CloudRunnerFolders.libraryFolderAbsolute); + await Caching.PullFromCache( + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryCacheFolderFull), + CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.libraryFolderAbsolute), + ); await RemoteClient.sizeOfFolder('repo after library cache pull', CloudRunnerFolders.repoPathAbsolute); await Caching.handleCachePurging(); } catch (error) { @@ -43,37 +54,69 @@ export class RemoteClient { } private static async sizeOfFolder(message: string, folder: string) { - if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) { + if (CloudRunner.buildParameters.cloudRunnerDebug) { CloudRunnerLogger.log(`Size of ${message}`); await CloudRunnerSystem.Run(`du -sh ${folder}`); } } private static async cloneRepoWithoutLFSFiles() { + process.chdir(`${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); + + if ( + CloudRunner.buildParameters.retainWorkspace && + fs.existsSync(path.join(CloudRunnerFolders.repoPathAbsolute, `.git`)) + ) { + process.chdir(CloudRunnerFolders.repoPathAbsolute); + RemoteClientLogger.log( + `${CloudRunnerFolders.repoPathAbsolute} repo exists - skipping clone - retained workspace mode ${CloudRunner.buildParameters.retainWorkspace}`, + ); + await CloudRunnerSystem.Run(`git reset --hard ${CloudRunner.buildParameters.gitSha}`); + + return; + } + + if (fs.existsSync(CloudRunnerFolders.repoPathAbsolute)) { + RemoteClientLogger.log(`${CloudRunnerFolders.repoPathAbsolute} repo exists cleaning up`); + await CloudRunnerSystem.Run(`rm -r ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}`); + } + try { - process.chdir(`${CloudRunnerFolders.repoPathAbsolute}`); RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`); await CloudRunnerSystem.Run(`git config --global advice.detachedHead false`); RemoteClientLogger.log(`Cloning the repository being built:`); await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`); await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`); await CloudRunnerSystem.Run( - `git clone -q ${CloudRunnerFolders.targetBuildRepoUrl} ${path.resolve( - `..`, - path.basename(CloudRunnerFolders.repoPathAbsolute), - )}`, + `git clone -q ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(CloudRunnerFolders.repoPathAbsolute)}`, ); + process.chdir(CloudRunnerFolders.repoPathAbsolute); await CloudRunnerSystem.Run(`git lfs install`); assert(fs.existsSync(`.git`), 'git folder exists'); RemoteClientLogger.log(`${CloudRunner.buildParameters.branch}`); await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.branch}`); + await CloudRunnerSystem.Run(`git checkout ${CloudRunner.buildParameters.gitSha}`); assert(fs.existsSync(path.join(`.git`, `lfs`)), 'LFS folder should not exist before caching'); - RemoteClientLogger.log(`Checked out ${Deno.env.get('GITHUB_SHA')}`); + RemoteClientLogger.log(`Checked out ${CloudRunner.buildParameters.branch}`); } catch (error) { + await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); throw error; } } + static replaceLargePackageReferencesWithSharedReferences() { + const manifest = fs.readFileSync( + path.join(CloudRunnerFolders.projectPathAbsolute, `Packages/manifest.json`), + 'utf8', + ); + if (CloudRunner.buildParameters.cloudRunnerDebug) { + CloudRunnerLogger.log(manifest); + } + if (CloudRunner.buildParameters.useSharedLargePackages) { + manifest.replace(/LargePackages/g, '../../LargePackages'); + } + } + private static async pullLatestLFS() { process.chdir(CloudRunnerFolders.repoPathAbsolute); await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge -- %f"`); @@ -83,13 +126,36 @@ export class RemoteClient { assert(fs.existsSync(CloudRunnerFolders.lfsFolderAbsolute)); } - @CliFunction(`remote-cli`, `sets up a repository, usually before a game-ci build`) + @CliFunction(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`) static async runRemoteClientJob() { - const buildParameter = JSON.parse(Deno.env.get('BUILD_PARAMETERS') || '{}'); - RemoteClientLogger.log(`Build Params: - ${JSON.stringify(buildParameter, undefined, 4)} - `); - CloudRunner.buildParameters = buildParameter; + // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); + RemoteClient.handleRetainedWorkspace(); + + // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); await RemoteClient.bootstrapRepository(); + + // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); + await RemoteClient.runCustomHookFiles(`before-build`); + + // await CloudRunnerSystem.Run(`tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute}`); + } + static async runCustomHookFiles(hookLifecycle: string) { + RemoteClientLogger.log(`RunCustomHookFiles: ${hookLifecycle}`); + const gameCiCustomHooksPath = path.join(CloudRunnerFolders.repoPathAbsolute, `game-ci`, `hooks`); + const files = fs.readdirSync(gameCiCustomHooksPath); + for (const file of files) { + const fileContents = fs.readFileSync(path.join(gameCiCustomHooksPath, file), `utf8`); + const fileContentsObject = YAML.parse(fileContents.toString()); + if (fileContentsObject.hook === hookLifecycle) { + RemoteClientLogger.log(`Active Hook File ${file} \n \n file contents: \n ${fileContents}`); + await CloudRunnerSystem.Run(fileContentsObject.commands); + } + } + } + static handleRetainedWorkspace() { + if (!CloudRunner.buildParameters.retainWorkspace) { + return; + } + RemoteClientLogger.log(`Retained Workspace: ${CloudRunner.lockedWorkspace}`); } } diff --git a/src/model/cloud-runner/remote-client/remote-client-logger.ts b/src/model/cloud-runner/remote-client/remote-client-logger.ts index 2c29d7e..5581b8b 100644 --- a/src/model/cloud-runner/remote-client/remote-client-logger.ts +++ b/src/model/cloud-runner/remote-client/remote-client-logger.ts @@ -1,4 +1,4 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger.ts'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; export class RemoteClientLogger { public static log(message: string) { diff --git a/src/model/cloud-runner/services/cloud-runner-build-command-process.ts b/src/model/cloud-runner/services/cloud-runner-build-command-process.ts deleted file mode 100644 index 1358efa..0000000 --- a/src/model/cloud-runner/services/cloud-runner-build-command-process.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Parameters } from '../../index.ts'; -import { yaml } from '../../../dependencies.ts'; -import CloudRunnerSecret from './cloud-runner-secret.ts'; -import CloudRunner from '../cloud-runner.ts'; - -export class CloudRunnerBuildCommandProcessor { - public static ProcessCommands(commands: string, buildParameters: Parameters): string { - const hooks = CloudRunnerBuildCommandProcessor.getHooks(buildParameters.customJobHooks).filter((x) => - x.step.includes(`all`), - ); - - return `echo "---" - echo "start cloud runner init" - ${CloudRunner.buildParameters.cloudRunnerIntegrationTests ? '' : '#'} printenv - echo "start of cloud runner job" - ${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} - ${commands} - ${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} - echo "end of cloud runner job" - echo "---${buildParameters.logId}"`; - } - - public static getHooks(customJobHooks): Hook[] { - const experimentHooks = customJobHooks; - let output = new Array(); - if (experimentHooks && experimentHooks !== '') { - try { - output = yaml.parse(experimentHooks); - } catch (error) { - throw error; - } - } - - return output.filter((x) => x.step !== undefined && x.hook !== undefined && x.hook.length > 0); - } -} -export class Hook { - public commands; - public secrets: CloudRunnerSecret[] = new Array(); - public name; - public hook!: string[]; - public step!: string[]; -} diff --git a/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts b/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts new file mode 100644 index 0000000..0b41595 --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-custom-hooks.ts @@ -0,0 +1,117 @@ +import { BuildParameters, Input } from '../..'; +import YAML from 'yaml'; +import CloudRunnerSecret from './cloud-runner-secret'; +import { RemoteClientLogger } from '../remote-client/remote-client-logger'; +import path from 'path'; +import CloudRunnerOptions from '../cloud-runner-options'; +import * as fs from 'fs'; +import CloudRunnerLogger from './cloud-runner-logger'; + +export class CloudRunnerCustomHooks { + // TODO also accept hooks as yaml files in the repo + public static ApplyHooksToCommands(commands: string, buildParameters: BuildParameters): string { + const hooks = CloudRunnerCustomHooks.getHooks(buildParameters.customJobHooks).filter((x) => x.step.includes(`all`)); + + return `echo "---" + echo "start cloud runner init" + ${CloudRunnerOptions.cloudRunnerDebugEnv ? `printenv` : `#`} + echo "start of cloud runner job" + ${hooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} + ${commands} + ${hooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} + echo "end of cloud runner job" + echo "---${buildParameters.logId}"`; + } + + public static getHooks(customJobHooks): Hook[] { + const experimentHooks = customJobHooks; + let output = new Array(); + if (experimentHooks && experimentHooks !== '') { + try { + output = YAML.parse(experimentHooks); + } catch (error) { + throw error; + } + } + + return output.filter((x) => x.step !== undefined && x.hook !== undefined && x.hook.length > 0); + } + + static GetCustomHooksFromFiles(hookLifecycle: string): Hook[] { + const results: Hook[] = []; + RemoteClientLogger.log(`GetCustomStepFiles: ${hookLifecycle}`); + try { + const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `hooks`); + const files = fs.readdirSync(gameCiCustomStepsPath); + for (const file of files) { + if (!CloudRunnerOptions.customHookFiles.includes(file.replace(`.yaml`, ``))) { + continue; + } + const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`); + const fileContentsObject = CloudRunnerCustomHooks.ParseHooks(fileContents)[0]; + if (fileContentsObject.hook.includes(hookLifecycle)) { + results.push(fileContentsObject); + } + } + } catch (error) { + RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`); + } + RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`); + + return results; + } + + private static ConvertYamlSecrets(object) { + if (object.secrets === undefined) { + object.secrets = []; + + return; + } + object.secrets = object.secrets.map((x) => { + return { + ParameterKey: x.name, + EnvironmentVariable: Input.ToEnvVarFormat(x.name), + ParameterValue: x.value, + }; + }); + } + + public static ParseHooks(steps: string): Hook[] { + if (steps === '') { + return []; + } + + // if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) { + + CloudRunnerLogger.log(`Parsing build hooks: ${steps}`); + + // } + const isArray = steps.replace(/\s/g, ``)[0] === `-`; + const object: Hook[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)]; + for (const hook of object) { + CloudRunnerCustomHooks.ConvertYamlSecrets(hook); + if (hook.secrets === undefined) { + hook.secrets = []; + } + } + if (object === undefined) { + throw new Error(`Failed to parse ${steps}`); + } + + return object; + } + + public static getSecrets(hooks) { + const secrets = hooks.map((x) => x.secrets).filter((x) => x !== undefined && x.length > 0); + + // eslint-disable-next-line unicorn/no-array-reduce + return secrets.length > 0 ? secrets.reduce((x, y) => [...x, ...y]) : []; + } +} +export class Hook { + public commands; + public secrets: CloudRunnerSecret[] = new Array(); + public name; + public hook!: string[]; + public step!: string[]; +} diff --git a/src/model/cloud-runner/services/cloud-runner-custom-steps.ts b/src/model/cloud-runner/services/cloud-runner-custom-steps.ts new file mode 100644 index 0000000..8e2737f --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-custom-steps.ts @@ -0,0 +1,206 @@ +import YAML from 'yaml'; +import CloudRunnerSecret from './cloud-runner-secret'; +import CloudRunner from '../cloud-runner'; +import * as core from '@actions/core'; +import { CustomWorkflow } from '../workflows/custom-workflow'; +import { RemoteClientLogger } from '../remote-client/remote-client-logger'; +import path from 'path'; +import * as fs from 'fs'; +import Input from '../../input'; +import CloudRunnerOptions from '../cloud-runner-options'; + +export class CloudRunnerCustomSteps { + static GetCustomStepsFromFiles(hookLifecycle: string): CustomStep[] { + const results: CustomStep[] = []; + RemoteClientLogger.log( + `GetCustomStepFiles: ${hookLifecycle} CustomStepFiles: ${CloudRunnerOptions.customStepFiles}`, + ); + try { + const gameCiCustomStepsPath = path.join(process.cwd(), `game-ci`, `steps`); + const files = fs.readdirSync(gameCiCustomStepsPath); + for (const file of files) { + if (!CloudRunnerOptions.customStepFiles.includes(file.replace(`.yaml`, ``))) { + RemoteClientLogger.log(`Skipping CustomStepFile: ${file}`); + continue; + } + const fileContents = fs.readFileSync(path.join(gameCiCustomStepsPath, file), `utf8`); + const fileContentsObject = CloudRunnerCustomSteps.ParseSteps(fileContents)[0]; + if (fileContentsObject.hook === hookLifecycle) { + results.push(fileContentsObject); + } + } + } catch (error) { + RemoteClientLogger.log(`Failed Getting: ${hookLifecycle} \n ${JSON.stringify(error, undefined, 4)}`); + } + RemoteClientLogger.log(`Active Steps From Files: \n ${JSON.stringify(results, undefined, 4)}`); + + const builtInCustomSteps: CustomStep[] = CloudRunnerCustomSteps.ParseSteps( + `- name: aws-s3-upload-build + image: amazon/aws-cli + hook: after + commands: | + aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default + aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default + aws configure set region $AWS_DEFAULT_REGION --profile default + aws s3 cp /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 + rm /data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4 + secrets: + - name: awsAccessKeyId + value: ${process.env.AWS_ACCESS_KEY_ID || ``} + - name: awsSecretAccessKey + value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} + - name: awsDefaultRegion + value: ${process.env.AWS_REGION || ``} +- name: aws-s3-upload-cache + image: amazon/aws-cli + hook: after + commands: | + aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default + aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default + aws configure set region $AWS_DEFAULT_REGION --profile default + aws s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/lfs + rm -r /data/cache/$CACHE_KEY/lfs + aws s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/Library + rm -r /data/cache/$CACHE_KEY/Library + secrets: + - name: awsAccessKeyId + value: ${process.env.AWS_ACCESS_KEY_ID || ``} + - name: awsSecretAccessKey + value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} + - name: awsDefaultRegion + value: ${process.env.AWS_REGION || ``} +- name: aws-s3-pull-cache + image: amazon/aws-cli + hook: before + commands: | + aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default + aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default + aws configure set region $AWS_DEFAULT_REGION --profile default + aws s3 ls game-ci-test-storage/cloud-runner-cache/ || true + aws s3 ls game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/ || true + BUCKET1="game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/Library/" + aws s3 ls $BUCKET1 || true + OBJECT1="$(aws s3 ls $BUCKET1 | sort | tail -n 1 | awk '{print $4}' || '')" + aws s3 cp s3://$BUCKET1$OBJECT1 /data/cache/$CACHE_KEY/Library/ || true + BUCKET2="game-ci-test-storage/cloud-runner-cache/$CACHE_KEY/lfs/" + aws s3 ls $BUCKET2 || true + OBJECT2="$(aws s3 ls $BUCKET2 | sort | tail -n 1 | awk '{print $4}' || '')" + aws s3 cp s3://$BUCKET2$OBJECT2 /data/cache/$CACHE_KEY/lfs/ || true + secrets: + - name: awsAccessKeyId + value: ${process.env.AWS_ACCESS_KEY_ID || ``} + - name: awsSecretAccessKey + value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} + - name: awsDefaultRegion + value: ${process.env.AWS_REGION || ``} +- name: debug-cache + image: ubuntu + hook: after + commands: | + apt-get update > /dev/null + ${CloudRunnerOptions.cloudRunnerDebugTree ? `apt-get install -y tree > /dev/null` : `#`} + ${CloudRunnerOptions.cloudRunnerDebugTree ? `tree -L 3 /data/cache` : `#`} + secrets: + - name: awsAccessKeyId + value: ${process.env.AWS_ACCESS_KEY_ID || ``} + - name: awsSecretAccessKey + value: ${process.env.AWS_SECRET_ACCESS_KEY || ``} + - name: awsDefaultRegion + value: ${process.env.AWS_REGION || ``}`, + ).filter((x) => CloudRunnerOptions.customStepFiles.includes(x.name) && x.hook === hookLifecycle); + if (builtInCustomSteps.length > 0) { + results.push(...builtInCustomSteps); + } + + return results; + } + + private static ConvertYamlSecrets(object) { + if (object.secrets === undefined) { + object.secrets = []; + + return; + } + object.secrets = object.secrets.map((x) => { + return { + ParameterKey: x.name, + EnvironmentVariable: Input.ToEnvVarFormat(x.name), + ParameterValue: x.value, + }; + }); + } + + public static ParseSteps(steps: string): CustomStep[] { + if (steps === '') { + return []; + } + + // if (CloudRunner.buildParameters?.cloudRunnerIntegrationTests) { + + // CloudRunnerLogger.log(`Parsing build steps: ${steps}`); + + // } + const isArray = steps.replace(/\s/g, ``)[0] === `-`; + const object: CustomStep[] = isArray ? YAML.parse(steps) : [YAML.parse(steps)]; + for (const step of object) { + CloudRunnerCustomSteps.ConvertYamlSecrets(step); + if (step.secrets === undefined) { + step.secrets = []; + } + if (step.image === undefined) { + step.image = `ubuntu`; + } + } + if (object === undefined) { + throw new Error(`Failed to parse ${steps}`); + } + + return object; + } + + static async RunPostBuildSteps(cloudRunnerStepState) { + let output = ``; + const steps: CustomStep[] = [ + ...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.postBuildSteps), + ...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`after`), + ]; + + if (steps.length > 0) { + if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps'); + output += await CustomWorkflow.runCustomJob( + steps, + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, + ); + if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + } + + return output; + } + static async RunPreBuildSteps(cloudRunnerStepState) { + let output = ``; + const steps: CustomStep[] = [ + ...CloudRunnerCustomSteps.ParseSteps(CloudRunner.buildParameters.preBuildSteps), + ...CloudRunnerCustomSteps.GetCustomStepsFromFiles(`before`), + ]; + + if (steps.length > 0) { + if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps'); + output += await CustomWorkflow.runCustomJob( + steps, + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, + ); + if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + } + + return output; + } +} +export class CustomStep { + public commands; + public secrets: CloudRunnerSecret[] = new Array(); + public name; + public image: string = `ubuntu`; + public hook!: string; +} diff --git a/src/model/cloud-runner/services/cloud-runner-folders.ts b/src/model/cloud-runner/services/cloud-runner-folders.ts index de4bef6..d544de0 100644 --- a/src/model/cloud-runner/services/cloud-runner-folders.ts +++ b/src/model/cloud-runner/services/cloud-runner-folders.ts @@ -1,16 +1,27 @@ -import { path } from '../../../dependencies.ts'; -import { CloudRunner } from '../../index.ts'; +import path from 'path'; +import CloudRunnerOptions from '../cloud-runner-options'; +import CloudRunner from './../cloud-runner'; export class CloudRunnerFolders { public static readonly repositoryFolder = 'repo'; + public static ToLinuxFolder(folder: string) { + return folder.replace(/\\/g, `/`); + } + // Only the following paths that do not start a path.join with another "Full" suffixed property need to start with an absolute / public static get uniqueCloudRunnerJobFolderAbsolute(): string { - return path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid); + return CloudRunner.buildParameters && CloudRunner.buildParameters.retainWorkspace && CloudRunner.lockedWorkspace + ? path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.lockedWorkspace) + : path.join(`/`, CloudRunnerFolders.buildVolumeFolder, CloudRunner.buildParameters.buildGuid); + } + + public static get cacheFolderForAllFull(): string { + return path.join('/', CloudRunnerFolders.buildVolumeFolder, CloudRunnerFolders.cacheFolder); } - public static get cacheFolderFull(): string { + public static get cacheFolderForCacheKeyFull(): string { return path.join( '/', CloudRunnerFolders.buildVolumeFolder, @@ -20,7 +31,12 @@ export class CloudRunnerFolders { } public static get builderPathAbsolute(): string { - return path.join(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, `builder`); + return path.join( + CloudRunnerOptions.useSharedBuilder + ? `/${CloudRunnerFolders.buildVolumeFolder}` + : CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, + `builder`, + ); } public static get repoPathAbsolute(): string { @@ -44,15 +60,15 @@ export class CloudRunnerFolders { } public static get purgeRemoteCaching(): boolean { - return Deno.env.get('PURGE_REMOTE_BUILDER_CACHE') !== undefined; + return process.env.PURGE_REMOTE_BUILDER_CACHE !== undefined; } public static get lfsCacheFolderFull() { - return path.join(CloudRunnerFolders.cacheFolderFull, `lfs`); + return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `lfs`); } public static get libraryCacheFolderFull() { - return path.join(CloudRunnerFolders.cacheFolderFull, `Library`); + return path.join(CloudRunnerFolders.cacheFolderForCacheKeyFull, `Library`); } public static get unityBuilderRepoUrl(): string { diff --git a/src/model/cloud-runner/services/cloud-runner-guid.ts b/src/model/cloud-runner/services/cloud-runner-guid.ts index a24a769..1ee7601 100644 --- a/src/model/cloud-runner/services/cloud-runner-guid.ts +++ b/src/model/cloud-runner/services/cloud-runner-guid.ts @@ -1,9 +1,9 @@ -import { nanoid } from '../../../dependencies.ts'; -import CloudRunnerConstants from './cloud-runner-constants.ts'; +import { customAlphabet } from 'nanoid'; +import CloudRunnerConstants from './cloud-runner-constants'; class CloudRunnerNamespace { static generateGuid(runNumber: string | number, platform: string) { - const nanoid = nanoid.customAlphabet(CloudRunnerConstants.alphabet, 4); + const nanoid = customAlphabet(CloudRunnerConstants.alphabet, 4); return `${runNumber}-${platform.toLowerCase().replace('standalone', '')}-${nanoid()}`; } diff --git a/src/model/cloud-runner/services/cloud-runner-logger.ts b/src/model/cloud-runner/services/cloud-runner-logger.ts index 92c1382..5315884 100644 --- a/src/model/cloud-runner/services/cloud-runner-logger.ts +++ b/src/model/cloud-runner/services/cloud-runner-logger.ts @@ -1,4 +1,4 @@ -import { core } from '../../../dependencies.ts'; +import * as core from '@actions/core'; class CloudRunnerLogger { private static timestamp: number; @@ -10,24 +10,24 @@ class CloudRunnerLogger { } public static log(message: string) { - log.info(message); + core.info(message); } public static logWarning(message: string) { - log.warning(message); + core.warning(message); } public static logLine(message: string) { - log.info(`${message}\n`); + core.info(`${message}\n`); } public static error(message: string) { - log.error(message); + core.error(message); } public static logWithTime(message: string) { const newTimestamp = this.createTimestamp(); - log.info( + core.info( `${message} (Since previous: ${this.calculateTimeDiff( newTimestamp, this.timestamp, diff --git a/src/model/cloud-runner/services/cloud-runner-options-reader.ts b/src/model/cloud-runner/services/cloud-runner-options-reader.ts new file mode 100644 index 0000000..d8f40db --- /dev/null +++ b/src/model/cloud-runner/services/cloud-runner-options-reader.ts @@ -0,0 +1,10 @@ +import Input from '../../input'; +import CloudRunnerOptions from '../cloud-runner-options'; + +class CloudRunnerOptionsReader { + static GetProperties() { + return [...Object.getOwnPropertyNames(Input), ...Object.getOwnPropertyNames(CloudRunnerOptions)]; + } +} + +export default CloudRunnerOptionsReader; diff --git a/src/model/cloud-runner/services/cloud-runner-query-override.ts b/src/model/cloud-runner/services/cloud-runner-query-override.ts index 28fb3a2..8b364e5 100644 --- a/src/model/cloud-runner/services/cloud-runner-query-override.ts +++ b/src/model/cloud-runner/services/cloud-runner-query-override.ts @@ -1,5 +1,6 @@ -import Input from '../../input.ts'; -import { GenericInputReader } from '../../input-readers/generic-input-reader.ts'; +import Input from '../../input'; +import { GenericInputReader } from '../../input-readers/generic-input-reader'; +import CloudRunnerOptions from '../cloud-runner-options'; const formatFunction = (value, arguments_) => { for (const element of arguments_) { @@ -12,6 +13,8 @@ const formatFunction = (value, arguments_) => { class CloudRunnerQueryOverride { static queryOverrides: any; + // TODO accept premade secret sources or custom secret source definition yamls + public static query(key, alternativeKey) { if (CloudRunnerQueryOverride.queryOverrides && CloudRunnerQueryOverride.queryOverrides[key] !== undefined) { return CloudRunnerQueryOverride.queryOverrides[key]; @@ -28,11 +31,11 @@ class CloudRunnerQueryOverride { } private static shouldUseOverride(query) { - if (Input.readInputOverrideCommand() !== '') { - if (Input.readInputFromOverrideList() !== '') { + if (CloudRunnerOptions.readInputOverrideCommand() !== '') { + if (CloudRunnerOptions.readInputFromOverrideList() !== '') { const doesInclude = - Input.readInputFromOverrideList().split(',').includes(query) || - Input.readInputFromOverrideList().split(',').includes(Input.toEnvVarFormat(query)); + CloudRunnerOptions.readInputFromOverrideList().split(',').includes(query) || + CloudRunnerOptions.readInputFromOverrideList().split(',').includes(Input.ToEnvVarFormat(query)); return doesInclude ? true : false; } else { @@ -46,11 +49,13 @@ class CloudRunnerQueryOverride { throw new Error(`Should not be trying to run override query on ${query}`); } - return await GenericInputReader.Run(formatFunction(Input.readInputOverrideCommand(), [{ key: 0, value: query }])); + return await GenericInputReader.Run( + formatFunction(CloudRunnerOptions.readInputOverrideCommand(), [{ key: 0, value: query }]), + ); } public static async PopulateQueryOverrideInput() { - const queries = Input.readInputFromOverrideList().split(','); + const queries = CloudRunnerOptions.readInputFromOverrideList().split(','); CloudRunnerQueryOverride.queryOverrides = new Array(); for (const element of queries) { if (CloudRunnerQueryOverride.shouldUseOverride(element)) { diff --git a/src/model/cloud-runner/services/cloud-runner-system.ts b/src/model/cloud-runner/services/cloud-runner-system.ts index 1ced6c9..02798a7 100644 --- a/src/model/cloud-runner/services/cloud-runner-system.ts +++ b/src/model/cloud-runner/services/cloud-runner-system.ts @@ -1,7 +1,21 @@ -import { RemoteClientLogger } from '../remote-client/remote-client-logger.ts'; -import { spawn } from 'https://deno.land/std@0.152.0/node/child_process.ts'; +import { exec } from 'child_process'; +import { RemoteClientLogger } from '../remote-client/remote-client-logger'; export class CloudRunnerSystem { + public static async RunAndReadLines(command: string): Promise { + const result = await CloudRunnerSystem.Run(command, false, true); + + return result + .split(`\n`) + .map((x) => x.replace(`\r`, ``)) + .filter((x) => x !== ``) + .map((x) => { + const lineValues = x.split(` `); + + return lineValues[lineValues.length - 1]; + }); + } + public static async Run(command: string, suppressError = false, suppressLogs = false) { for (const element of command.split(`\n`)) { if (!suppressLogs) { @@ -11,9 +25,7 @@ export class CloudRunnerSystem { return await new Promise((promise, throwError) => { let output = ''; - - // Todo - find Deno variant of child_process or rewrite this method to use execSync - const child = spawn(command, (error, stdout, stderr) => { + const child = exec(command, (error, stdout, stderr) => { if (!suppressError && error) { RemoteClientLogger.log(error.toString()); throwError(error); diff --git a/src/model/cloud-runner/services/depdency-override-service.ts b/src/model/cloud-runner/services/depdency-override-service.ts deleted file mode 100644 index 35fbfc0..0000000 --- a/src/model/cloud-runner/services/depdency-override-service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Input from '../../input.ts'; -import { CloudRunnerSystem } from './cloud-runner-system.ts'; - -class DependencyOverrideService { - public static async CheckHealth() { - if (Input.checkDependencyHealthOverride) { - try { - await CloudRunnerSystem.Run(Input.checkDependencyHealthOverride); - } catch { - return false; - } - } - - return true; - } - public static async TryStartDependencies() { - if (Input.startDependenciesOverride) { - await CloudRunnerSystem.Run(Input.startDependenciesOverride); - } - } -} -export default DependencyOverrideService; diff --git a/src/model/cloud-runner/services/follow-log-stream-service.ts b/src/model/cloud-runner/services/follow-log-stream-service.ts index 357590f..c25ef6d 100644 --- a/src/model/cloud-runner/services/follow-log-stream-service.ts +++ b/src/model/cloud-runner/services/follow-log-stream-service.ts @@ -1,7 +1,7 @@ -import CloudRunnerLogger from './cloud-runner-logger.ts'; -import { core } from '../../../dependencies.ts'; -import CloudRunner from '../cloud-runner.ts'; -import { CloudRunnerStatics } from '../cloud-runner-statics.ts'; +import CloudRunnerLogger from './cloud-runner-logger'; +import * as core from '@actions/core'; +import CloudRunner from '../cloud-runner'; +import { CloudRunnerStatics } from '../cloud-runner-statics'; export class FollowLogStreamService { public static handleIteration(message, shouldReadLogs, shouldCleanup, output) { @@ -9,25 +9,24 @@ export class FollowLogStreamService { CloudRunnerLogger.log('End of log transmission received'); shouldReadLogs = false; } else if (message.includes('Rebuilding Library because the asset database could not be found!')) { - log.warning('LIBRARY NOT FOUND!'); + core.warning('LIBRARY NOT FOUND!'); core.setOutput('library-found', 'false'); } else if (message.includes('Build succeeded')) { core.setOutput('build-result', 'success'); } else if (message.includes('Build fail')) { core.setOutput('build-result', 'failed'); - log.error('build-result: failed'); - Deno.exit(1); - } else if (CloudRunner.buildParameters.cloudRunnerIntegrationTests && message.includes(': Listening for Jobs')) { + core.setFailed('unity build failed'); + core.error('BUILD FAILED!'); + } else if (CloudRunner.buildParameters.cloudRunnerDebug && message.includes(': Listening for Jobs')) { core.setOutput('cloud runner stop watching', 'true'); shouldReadLogs = false; shouldCleanup = false; - log.warning('cloud runner stop watching'); + core.warning('cloud runner stop watching'); } - message = `[${CloudRunnerStatics.logPrefix}] ${message}`; - if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) { - output += message; + if (CloudRunner.buildParameters.cloudRunnerDebug) { + output += `${message}\n`; } - CloudRunnerLogger.log(message); + CloudRunnerLogger.log(`[${CloudRunnerStatics.logPrefix}] ${message}`); return { shouldReadLogs, shouldCleanup, output }; } diff --git a/src/model/cloud-runner/services/lfs-hashing.ts b/src/model/cloud-runner/services/lfs-hashing.ts index 8564276..9d43130 100644 --- a/src/model/cloud-runner/services/lfs-hashing.ts +++ b/src/model/cloud-runner/services/lfs-hashing.ts @@ -1,8 +1,10 @@ -import { CloudRunnerFolders } from './cloud-runner-folders.ts'; -import { CloudRunnerSystem } from './cloud-runner-system.ts'; -import { fsSync as fs, assert, path } from '../../../dependencies.ts'; -import { Cli } from '../../cli/cli.ts'; -import { CliFunction } from '../../cli/cli-functions-repository.ts'; +import path from 'path'; +import { CloudRunnerFolders } from './cloud-runner-folders'; +import { CloudRunnerSystem } from './cloud-runner-system'; +import fs from 'fs'; +import { assert } from 'console'; +import { Cli } from '../../cli/cli'; +import { CliFunction } from '../../cli/cli-functions-repository'; export class LfsHashing { public static async createLFSHashFiles() { @@ -28,11 +30,10 @@ export class LfsHashing { } public static async hashAllFiles(folder: string) { const startPath = process.cwd(); - process.chdir(folder); - const checksums = await CloudRunnerSystem.Run(`find -type f -exec md5sum "{}" + | sort | md5sum`); - const result = checksums.replace(/\n/g, '').split(` `)[0]; - + const result = await (await CloudRunnerSystem.Run(`find -type f -exec md5sum "{}" + | sort | md5sum`)) + .replace(/\n/g, '') + .split(` `)[0]; process.chdir(startPath); return result; diff --git a/src/model/cloud-runner/services/shared-workspace-locking.ts b/src/model/cloud-runner/services/shared-workspace-locking.ts new file mode 100644 index 0000000..dd4b393 --- /dev/null +++ b/src/model/cloud-runner/services/shared-workspace-locking.ts @@ -0,0 +1,279 @@ +import { CloudRunnerSystem } from './cloud-runner-system'; +import * as fs from 'fs'; +import CloudRunnerLogger from './cloud-runner-logger'; +import CloudRunnerOptions from '../cloud-runner-options'; +import BuildParameters from '../../build-parameters'; +import CloudRunner from '../cloud-runner'; +export class SharedWorkspaceLocking { + private static readonly workspaceBucketRoot = `s3://game-ci-test-storage/`; + private static readonly workspaceRoot = `${SharedWorkspaceLocking.workspaceBucketRoot}locks/`; + public static async GetAllWorkspaces(buildParametersContext: BuildParameters): Promise { + if (!(await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext))) { + return []; + } + + return ( + await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, + ) + ).map((x) => x.replace(`/`, ``)); + } + public static async DoesWorkspaceTopLevelExist(buildParametersContext: BuildParameters) { + return ( + (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceBucketRoot}`)) + .map((x) => x.replace(`/`, ``)) + .includes(`locks`) && + (await SharedWorkspaceLocking.ReadLines(`aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}`)) + .map((x) => x.replace(`/`, ``)) + .includes(buildParametersContext.cacheKey) + ); + } + public static async GetAllLocks(workspace: string, buildParametersContext: BuildParameters): Promise { + if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { + return []; + } + + return ( + await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`, + ) + ) + .map((x) => x.replace(`/`, ``)) + .filter((x) => x.includes(`_lock`)); + } + public static async GetOrCreateLockedWorkspace( + workspace: string, + runId: string, + buildParametersContext: BuildParameters, + ) { + if (!CloudRunnerOptions.retainWorkspaces) { + return; + } + if (await SharedWorkspaceLocking.DoesWorkspaceTopLevelExist(buildParametersContext)) { + const workspaces = await SharedWorkspaceLocking.GetFreeWorkspaces(buildParametersContext); + CloudRunnerLogger.log(`run agent ${runId} is trying to access a workspace, free: ${JSON.stringify(workspaces)}`); + for (const element of workspaces) { + await new Promise((promise) => setTimeout(promise, 1000)); + const lockResult = await SharedWorkspaceLocking.LockWorkspace(element, runId, buildParametersContext); + CloudRunnerLogger.log(`run agent: ${runId} try lock workspace: ${element} result: ${lockResult}`); + + if (lockResult) { + CloudRunner.lockedWorkspace = element; + + return true; + } + } + } + + const createResult = await SharedWorkspaceLocking.CreateWorkspace(workspace, buildParametersContext, runId); + CloudRunnerLogger.log( + `run agent ${runId} didn't find a free workspace so created: ${workspace} createWorkspaceSuccess: ${createResult}`, + ); + + return createResult; + } + + public static async DoesWorkspaceExist(workspace: string, buildParametersContext: BuildParameters) { + return (await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext)).includes(workspace); + } + public static async HasWorkspaceLock( + workspace: string, + runId: string, + buildParametersContext: BuildParameters, + ): Promise { + if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { + return false; + } + const locks = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext)) + .map((x) => { + return { + name: x, + timestamp: Number(x.split(`_`)[0]), + }; + }) + .sort((x) => x.timestamp); + const lockMatches = locks.filter((x) => x.name.includes(runId)); + const includesRunLock = lockMatches.length > 0 && locks.indexOf(lockMatches[0]) === 0; + CloudRunnerLogger.log( + `Checking has workspace lock, runId: ${runId}, workspace: ${workspace}, success: ${includesRunLock} \n- Num of locks created by Run Agent: ${ + lockMatches.length + } Num of Locks: ${locks.length}, Time ordered index for Run Agent: ${locks.indexOf(lockMatches[0])} \n \n`, + ); + + return includesRunLock; + } + + public static async GetFreeWorkspaces(buildParametersContext: BuildParameters): Promise { + const result: string[] = []; + const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext); + for (const element of workspaces) { + await new Promise((promise) => setTimeout(promise, 1500)); + const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(element, buildParametersContext); + const isBelowMax = await SharedWorkspaceLocking.IsWorkspaceBelowMax(element, buildParametersContext); + if (!isLocked && isBelowMax) { + result.push(element); + CloudRunnerLogger.log(`workspace ${element} is free`); + } else { + CloudRunnerLogger.log(`workspace ${element} is NOT free ${!isLocked} ${isBelowMax}`); + } + } + + return result; + } + + public static async IsWorkspaceBelowMax( + workspace: string, + buildParametersContext: BuildParameters, + ): Promise { + const workspaces = await SharedWorkspaceLocking.GetAllWorkspaces(buildParametersContext); + if (workspace === ``) { + return ( + workspaces.length < buildParametersContext.maxRetainedWorkspaces || + buildParametersContext.maxRetainedWorkspaces === 0 + ); + } + const ordered: any[] = []; + for (const ws of workspaces) { + ordered.push({ + name: ws, + timestamp: await SharedWorkspaceLocking.GetWorkspaceTimestamp(ws, buildParametersContext), + }); + } + ordered.sort((x) => x.timestamp); + const matches = ordered.filter((x) => x.name.includes(workspace)); + const isWorkspaceBelowMax = + matches.length > 0 && + (ordered.indexOf(matches[0]) < buildParametersContext.maxRetainedWorkspaces || + buildParametersContext.maxRetainedWorkspaces === 0); + + return isWorkspaceBelowMax; + } + + public static async GetWorkspaceTimestamp( + workspace: string, + buildParametersContext: BuildParameters, + ): Promise { + if (workspace.split(`_`).length > 0) { + return Number(workspace.split(`_`)[1]); + } + + if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { + throw new Error("Workspace doesn't exist, can't call get all locks"); + } + + return ( + await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`, + ) + ) + .map((x) => x.replace(`/`, ``)) + .filter((x) => x.includes(`_workspace`)) + .map((x) => Number(x))[0]; + } + + public static async IsWorkspaceLocked(workspace: string, buildParametersContext: BuildParameters): Promise { + if (!(await SharedWorkspaceLocking.DoesWorkspaceExist(workspace, buildParametersContext))) { + return false; + } + const files = await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/`, + ); + + const workspaceFileDoesNotExists = + files.filter((x) => { + return x.includes(`_workspace`); + }).length === 0; + + const lockFilesExist = + files.filter((x) => { + return x.includes(`_lock`); + }).length > 0; + + return workspaceFileDoesNotExists || lockFilesExist; + } + + public static async CreateWorkspace( + workspace: string, + buildParametersContext: BuildParameters, + lockId: string = ``, + ): Promise { + if (lockId !== ``) { + await SharedWorkspaceLocking.LockWorkspace(workspace, lockId, buildParametersContext); + } + const timestamp = Date.now(); + const file = `${timestamp}_workspace`; + fs.writeFileSync(file, ''); + await CloudRunnerSystem.Run( + `aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + false, + true, + ); + fs.rmSync(file); + + const workspaces = await SharedWorkspaceLocking.ReadLines( + `aws s3 ls ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/`, + ); + + CloudRunnerLogger.log(`All workspaces ${workspaces}`); + if (!(await SharedWorkspaceLocking.IsWorkspaceBelowMax(workspace, buildParametersContext))) { + CloudRunnerLogger.log(`Workspace is below max ${workspaces} ${buildParametersContext.maxRetainedWorkspaces}`); + await SharedWorkspaceLocking.CleanupWorkspace(workspace, buildParametersContext); + + return false; + } + + return true; + } + + public static async LockWorkspace( + workspace: string, + runId: string, + buildParametersContext: BuildParameters, + ): Promise { + const file = `${Date.now()}_${runId}_lock`; + fs.writeFileSync(file, ''); + await CloudRunnerSystem.Run( + `aws s3 cp ./${file} ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + false, + true, + ); + fs.rmSync(file); + + return SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext); + } + + public static async ReleaseWorkspace( + workspace: string, + runId: string, + buildParametersContext: BuildParameters, + ): Promise { + const file = (await SharedWorkspaceLocking.GetAllLocks(workspace, buildParametersContext)).filter((x) => + x.includes(`_${runId}_lock`), + ); + CloudRunnerLogger.log(`Deleting lock ${workspace}/${file}`); + CloudRunnerLogger.log( + `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + ); + await CloudRunnerSystem.Run( + `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace}/${file}`, + false, + true, + ); + + return !SharedWorkspaceLocking.HasWorkspaceLock(workspace, runId, buildParametersContext); + } + + public static async CleanupWorkspace(workspace: string, buildParametersContext: BuildParameters) { + await CloudRunnerSystem.Run( + `aws s3 rm ${SharedWorkspaceLocking.workspaceRoot}${buildParametersContext.cacheKey}/${workspace} --recursive`, + false, + true, + ); + } + + private static async ReadLines(command: string): Promise { + return CloudRunnerSystem.RunAndReadLines(command); + } +} + +export default SharedWorkspaceLocking; diff --git a/src/model/cloud-runner/services/task-parameter-serializer.ts b/src/model/cloud-runner/services/task-parameter-serializer.ts index 55fb0a7..ddc7d28 100644 --- a/src/model/cloud-runner/services/task-parameter-serializer.ts +++ b/src/model/cloud-runner/services/task-parameter-serializer.ts @@ -1,72 +1,137 @@ -import { CloudRunner, Input } from '../../index.ts'; -import ImageEnvironmentFactory from '../../image-environment-factory.ts'; -import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable.ts'; -import { CloudRunnerBuildCommandProcessor } from './cloud-runner-build-command-process.ts'; -import CloudRunnerSecret from './cloud-runner-secret.ts'; -import CloudRunnerQueryOverride from './cloud-runner-query-override.ts'; +import { Input } from '../..'; +import CloudRunnerEnvironmentVariable from './cloud-runner-environment-variable'; +import { CloudRunnerCustomHooks } from './cloud-runner-custom-hooks'; +import CloudRunnerSecret from './cloud-runner-secret'; +import CloudRunnerQueryOverride from './cloud-runner-query-override'; +import CloudRunnerOptionsReader from './cloud-runner-options-reader'; +import BuildParameters from '../../build-parameters'; +import CloudRunnerOptions from '../cloud-runner-options'; +import * as core from '@actions/core'; export class TaskParameterSerializer { - public static readBuildEnvironmentVariables(): CloudRunnerEnvironmentVariable[] { - return [ - { - name: 'ContainerMemory', - value: CloudRunner.buildParameters.cloudRunnerMemory, - }, - { - name: 'ContainerCpu', - value: CloudRunner.buildParameters.cloudRunnerCpu, - }, - { - name: 'BUILD_TARGET', - value: CloudRunner.buildParameters.targetPlatform, - }, - ...TaskParameterSerializer.serializeBuildParamsAndInput, - ]; - } - private static get serializeBuildParamsAndInput() { - let array = new Array(); - array = TaskParameterSerializer.readBuildParameters(array); - array = TaskParameterSerializer.readInput(array); - const configurableHooks = CloudRunnerBuildCommandProcessor.getHooks(CloudRunner.buildParameters.customJobHooks); - const secrets = configurableHooks.map((x) => x.secrets).filter((x) => x !== undefined && x.length > 0); - if (secrets.length > 0) { - // eslint-disable-next-line unicorn/no-array-reduce - array.push(secrets.reduce((x, y) => [...x, ...y])); - } + static readonly blocked = new Set(['0', 'length', 'prototype', '', 'unityVersion']); + public static createCloudRunnerEnvironmentVariables( + buildParameters: BuildParameters, + ): CloudRunnerEnvironmentVariable[] { + const result = this.uniqBy( + [ + { + name: 'ContainerMemory', + value: buildParameters.cloudRunnerMemory, + }, + { + name: 'ContainerCpu', + value: buildParameters.cloudRunnerCpu, + }, + { + name: 'BUILD_TARGET', + value: buildParameters.targetPlatform, + }, + ...TaskParameterSerializer.serializeFromObject(buildParameters), + ...TaskParameterSerializer.readInput(), + ...CloudRunnerCustomHooks.getSecrets(CloudRunnerCustomHooks.getHooks(buildParameters.customJobHooks)), + ] + .filter( + (x) => + !TaskParameterSerializer.blocked.has(x.name) && + x.value !== '' && + x.value !== undefined && + x.name !== `CUSTOM_JOB` && + x.name !== `GAMECI_CUSTOM_JOB` && + x.value !== `undefined`, + ) + .map((x) => { + x.name = TaskParameterSerializer.ToEnvVarFormat(x.name); + x.value = `${x.value}`; - array = array.filter( - (x) => x.value !== undefined && x.name !== '0' && x.value !== '' && x.name !== 'prototype' && x.name !== 'length', + if (buildParameters.cloudRunnerDebug && Number(x.name) === Number.NaN) { + core.info(`[ERROR] found a number in task param serializer ${JSON.stringify(x)}`); + } + + return x; + }), + (item) => item.name, ); - array = array.map((x) => { - x.name = Input.toEnvVarFormat(x.name); - x.value = `${x.value}`; - return x; + return result; + } + + static uniqBy(a, key) { + const seen = {}; + + return a.filter(function (item) { + const k = key(item); + + return seen.hasOwnProperty(k) ? false : (seen[k] = true); }); + } - return array; + public static readBuildParameterFromEnvironment(): BuildParameters { + const buildParameters = new BuildParameters(); + const keys = [ + ...new Set( + Object.getOwnPropertyNames(process.env) + .filter((x) => !this.blocked.has(x) && x.startsWith('GAMECI_')) + .map((x) => TaskParameterSerializer.UndoEnvVarFormat(x)), + ), + ]; + + for (const element of keys) { + if (element !== `customJob`) { + buildParameters[element] = process.env[`GAMECI_${TaskParameterSerializer.ToEnvVarFormat(element)}`]; + } + } + + return buildParameters; + } + + private static readInput() { + return TaskParameterSerializer.serializeFromType(Input); + } + + public static ToEnvVarFormat(input): string { + return CloudRunnerOptions.ToEnvVarFormat(input); + } + + public static UndoEnvVarFormat(element): string { + return this.camelize(element.replace('GAMECI_', '').toLowerCase().replace(/_+/g, ' ')); } - private static readBuildParameters(array: any[]) { - const keys = Object.keys(CloudRunner.buildParameters); + private static camelize(string) { + return string + .replace(/^\w|[A-Z]|\b\w/g, function (word, index) { + return index === 0 ? word.toLowerCase() : word.toUpperCase(); + }) + .replace(/\s+/g, ''); + } + + private static serializeFromObject(buildParameters) { + const array: any[] = []; + const keys = Object.getOwnPropertyNames(buildParameters).filter((x) => !this.blocked.has(x)); for (const element of keys) { - array.push({ - name: element, - value: CloudRunner.buildParameters[element], - }); + array.push( + { + name: `GAMECI_${TaskParameterSerializer.ToEnvVarFormat(element)}`, + value: buildParameters[element], + }, + { + name: element, + value: buildParameters[element], + }, + ); } - array.push({ name: 'buildParameters', value: JSON.stringify(CloudRunner.buildParameters) }); return array; } - private static readInput(array: any[]) { - const input = Object.getOwnPropertyNames(Input); + private static serializeFromType(type) { + const array: any[] = []; + const input = CloudRunnerOptionsReader.GetProperties(); for (const element of input) { - if (typeof Input[element] !== 'function' && array.filter((x) => x.name === element).length === 0) { + if (typeof type[element] !== 'function' && array.filter((x) => x.name === element).length === 0) { array.push({ name: element, - value: `${Input[element]}`, + value: `${type[element]}`, }); } } @@ -79,17 +144,7 @@ export class TaskParameterSerializer { array = TaskParameterSerializer.tryAddInput(array, 'UNITY_SERIAL'); array = TaskParameterSerializer.tryAddInput(array, 'UNITY_EMAIL'); array = TaskParameterSerializer.tryAddInput(array, 'UNITY_PASSWORD'); - array.push( - ...ImageEnvironmentFactory.getEnvironmentVariables(CloudRunner.buildParameters) - .filter((x) => array.every((y) => y.ParameterKey !== x.name)) - .map((x) => { - return { - ParameterKey: x.name, - EnvironmentVariable: x.name, - ParameterValue: x.value, - }; - }), - ); + array = TaskParameterSerializer.tryAddInput(array, 'UNITY_LICENSE'); return array; } @@ -102,7 +157,7 @@ export class TaskParameterSerializer { s; private static tryAddInput(array, key): CloudRunnerSecret[] { const value = TaskParameterSerializer.getValue(key); - if (value !== undefined && value !== '') { + if (value !== undefined && value !== '' && value !== 'null') { array.push({ ParameterKey: key, EnvironmentVariable: key, diff --git a/src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts b/src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts new file mode 100644 index 0000000..667ca43 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-environment-serializer.test.ts @@ -0,0 +1,45 @@ +import { BuildParameters } from '../..'; +import { TaskParameterSerializer } from '../services/task-parameter-serializer'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import GitHub from '../../github'; +import setups from './cloud-runner-suite.test'; + +async function CreateParameters(overrides) { + if (overrides) { + Cli.options = overrides; + } + const originalValue = GitHub.githubInputEnabled; + GitHub.githubInputEnabled = false; + const results = await BuildParameters.create(); + GitHub.githubInputEnabled = originalValue; + delete Cli.options; + + return results; +} +describe('Cloud Runner Environment Serializer', () => { + setups(); + const testSecretName = 'testSecretName'; + const testSecretValue = 'testSecretValue'; + it('Cloud Runner Parameter Serialization', async () => { + // Setup parameters + const buildParameter = await CreateParameters({ + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + customJob: ` + - name: 'step 1' + image: 'alpine' + commands: 'printenv' + secrets: + - name: '${testSecretName}' + value: '${testSecretValue}' + `, + }); + + const result = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); + expect(result.find((x) => Number.parseInt(x.name)) !== undefined).toBeFalsy(); + const result2 = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); + expect(result2.find((x) => Number.parseInt(x.name)) !== undefined).toBeFalsy(); + }); +}); diff --git a/src/model/cloud-runner/remote-client/caching.test.ts b/src/model/cloud-runner/tests/cloud-runner-remote-client-caching.test.ts similarity index 56% rename from src/model/cloud-runner/remote-client/caching.test.ts rename to src/model/cloud-runner/tests/cloud-runner-remote-client-caching.test.ts index 7f7d86c..906ccb6 100644 --- a/src/model/cloud-runner/remote-client/caching.test.ts +++ b/src/model/cloud-runner/tests/cloud-runner-remote-client-caching.test.ts @@ -1,27 +1,26 @@ -import { fs, uuid, path, __dirname } from '../../../dependencies.ts'; -import Parameters from '../../parameters.ts'; -import { Cli } from '../../cli/cli.ts'; -import Input from '../../input.ts'; -import UnityVersionDetector from '../../../middleware/engine-detection/unity-version-detector.ts'; -import CloudRunner from '../cloud-runner.ts'; -import { CloudRunnerSystem } from '../services/cloud-runner-system/index.ts'; -import { Caching } from './caching.ts'; - -describe('Cloud Runner Caching', () => { +import fs from 'fs'; +import path from 'path'; +import BuildParameters from '../../build-parameters'; +import { Cli } from '../../cli/cli'; +import UnityVersioning from '../../unity-versioning'; +import CloudRunner from '../cloud-runner'; +import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import { Caching } from '../remote-client/caching'; +import { v4 as uuidv4 } from 'uuid'; +import GitHub from '../../github'; +describe('Cloud Runner (Remote Client) Caching', () => { it('responds', () => {}); -}); -describe('Cloud Runner Caching', () => { if (process.platform === 'linux') { - it('Simple caching works', async () => { + it.skip('Simple caching works', async () => { Cli.options = { versioning: 'None', projectPath: 'test-project', - engineVersion: UnityVersionDetector.read('test-project'), + unityVersion: UnityVersioning.read('test-project'), targetPlatform: 'StandaloneLinux64', - cacheKey: `test-case-${uuid()}`, + cacheKey: `test-case-${uuidv4()}`, }; - Input.githubInputEnabled = false; - const buildParameter = await Parameters.create(); + GitHub.githubInputEnabled = false; + const buildParameter = await BuildParameters.create(); CloudRunner.buildParameters = buildParameter; // Create test folder @@ -44,18 +43,16 @@ describe('Cloud Runner Caching', () => { `${Cli.options.cacheKey}`, ); await CloudRunnerSystem.Run(`du -h ${__dirname}`); - await CloudRunnerSystem.Run(`tree ${testFolder}`); - await CloudRunnerSystem.Run(`tree ${cacheFolder}`); // Compare validity to original hash - expect(Deno.readTextFileSync(path.resolve(testFolder, 'test.txt'), { encoding: 'utf8' }).toString()).toContain( + expect(fs.readFileSync(path.resolve(testFolder, 'test.txt'), { encoding: 'utf8' }).toString()).toContain( Cli.options.cacheKey, ); fs.rmdirSync(testFolder, { recursive: true }); fs.rmdirSync(cacheFolder, { recursive: true }); - Input.githubInputEnabled = true; + GitHub.githubInputEnabled = true; delete Cli.options; - }, 1_000_000); + }, 1000000); } }); diff --git a/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts b/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts new file mode 100644 index 0000000..4ee010a --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-run-once-custom-hooks.test.ts @@ -0,0 +1,70 @@ +import CloudRunner from '../cloud-runner'; +import { BuildParameters, ImageTag } from '../..'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../cloud-runner-options'; +import setups from './cloud-runner-suite.test'; +import { CloudRunnerCustomSteps } from '../services/cloud-runner-custom-steps'; + +async function CreateParameters(overrides) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Custom Hooks And Steps', () => { + it('Responds', () => {}); + setups(); + it('Check parsing and reading of steps', async () => { + const yamlString = `hook: before +commands: echo "test"`; + const yamlString2 = `- hook: before + commands: echo "test"`; + const stringObject = CloudRunnerCustomSteps.ParseSteps(yamlString); + const stringObject2 = CloudRunnerCustomSteps.ParseSteps(yamlString2); + + CloudRunnerLogger.log(yamlString); + CloudRunnerLogger.log(JSON.stringify(stringObject, undefined, 4)); + + expect(stringObject.length).toBe(1); + expect(stringObject[0].hook).toBe(`before`); + expect(stringObject2.length).toBe(1); + expect(stringObject2[0].hook).toBe(`before`); + + const getCustomStepsFromFiles = CloudRunnerCustomSteps.GetCustomStepsFromFiles(`before`); + CloudRunnerLogger.log(JSON.stringify(getCustomStepsFromFiles, undefined, 4)); + }); + if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `k8s`) { + it('Run build once - check for pre and post custom hooks run contents', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + customStepFiles: `my-test-step-pre-build,my-test-step-post-build`, + }; + const buildParameter2 = await CreateParameters(overrides); + const baseImage2 = new ImageTag(buildParameter2); + const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); + CloudRunnerLogger.log(`run 2 succeeded`); + + const build2ContainsBuildSucceeded = results2.includes('Build succeeded'); + const build2ContainsPreBuildHookRunMessage = results2.includes('before-build hook test!'); + const build2ContainsPostBuildHookRunMessage = results2.includes('after-build hook test!'); + + const build2ContainsPreBuildStepMessage = results2.includes('before-build step test!'); + const build2ContainsPostBuildStepMessage = results2.includes('after-build step test!'); + + expect(build2ContainsBuildSucceeded).toBeTruthy(); + expect(build2ContainsPreBuildHookRunMessage).toBeTruthy(); + expect(build2ContainsPostBuildHookRunMessage).toBeTruthy(); + expect(build2ContainsPreBuildStepMessage).toBeTruthy(); + expect(build2ContainsPostBuildStepMessage).toBeTruthy(); + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts b/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts new file mode 100644 index 0000000..f5e730c --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-run-twice-caching.test.ts @@ -0,0 +1,72 @@ +import CloudRunner from '../cloud-runner'; +import { BuildParameters, ImageTag } from '../..'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../cloud-runner-options'; +import setups from './cloud-runner-suite.test'; + +async function CreateParameters(overrides) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Caching', () => { + it('Responds', () => {}); + setups(); + if (CloudRunnerOptions.cloudRunnerDebug) { + it('Run one build it should not use cache, run subsequent build which should use cache', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + customStepFiles: `debug-cache`, + }; + if (CloudRunnerOptions.cloudRunnerCluster === `k8s`) { + overrides.customStepFiles += `,aws-s3-pull-cache,aws-s3-upload-cache`; + } + const buildParameter = await CreateParameters(overrides); + expect(buildParameter.projectPath).toEqual(overrides.projectPath); + + const baseImage = new ImageTag(buildParameter); + const results = await CloudRunner.run(buildParameter, baseImage.toString()); + const libraryString = 'Rebuilding Library because the asset database could not be found!'; + const cachePushFail = 'Did not push source folder to cache because it was empty Library'; + const buildSucceededString = 'Build succeeded'; + + expect(results).toContain(libraryString); + expect(results).toContain(buildSucceededString); + expect(results).not.toContain(cachePushFail); + + CloudRunnerLogger.log(`run 1 succeeded`); + const buildParameter2 = await CreateParameters(overrides); + + buildParameter2.cacheKey = buildParameter.cacheKey; + const baseImage2 = new ImageTag(buildParameter2); + const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); + CloudRunnerLogger.log(`run 2 succeeded`); + + const build2ContainsCacheKey = results2.includes(buildParameter.cacheKey); + const build2ContainsBuildSucceeded = results2.includes(buildSucceededString); + const build2NotContainsNoLibraryMessage = !results2.includes(libraryString); + const build2NotContainsZeroLibraryCacheFilesMessage = !results2.includes( + 'There is 0 files/dir in the cache pulled contents for Library', + ); + const build2NotContainsZeroLFSCacheFilesMessage = !results2.includes( + 'There is 0 files/dir in the cache pulled contents for LFS', + ); + + expect(build2ContainsCacheKey).toBeTruthy(); + expect(build2ContainsBuildSucceeded).toBeTruthy(); + expect(build2NotContainsZeroLibraryCacheFilesMessage).toBeTruthy(); + expect(build2NotContainsZeroLFSCacheFilesMessage).toBeTruthy(); + expect(build2NotContainsNoLibraryMessage).toBeTruthy(); + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts b/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts new file mode 100644 index 0000000..b357ac4 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-run-twice-retaining.test.ts @@ -0,0 +1,93 @@ +import CloudRunner from '../cloud-runner'; +import { BuildParameters, ImageTag } from '../..'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../cloud-runner-options'; +import setups from './cloud-runner-suite.test'; +import { CloudRunnerSystem } from '../services/cloud-runner-system'; +import * as fs from 'fs'; +import path from 'path'; +import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import SharedWorkspaceLocking from '../services/shared-workspace-locking'; + +async function CreateParameters(overrides) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Retain Workspace', () => { + it('Responds', () => {}); + setups(); + if (CloudRunnerOptions.cloudRunnerDebug) { + it('Run one build it should not already be retained, run subsequent build which should use retained workspace', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + retainWorkspaces: true, + }; + const buildParameter = await CreateParameters(overrides); + expect(buildParameter.projectPath).toEqual(overrides.projectPath); + + const baseImage = new ImageTag(buildParameter); + const results = await CloudRunner.run(buildParameter, baseImage.toString()); + const libraryString = 'Rebuilding Library because the asset database could not be found!'; + const cachePushFail = 'Did not push source folder to cache because it was empty Library'; + const buildSucceededString = 'Build succeeded'; + + expect(results).toContain(libraryString); + expect(results).toContain(buildSucceededString); + expect(results).not.toContain(cachePushFail); + + CloudRunnerLogger.log(`run 1 succeeded`); + const buildParameter2 = await CreateParameters(overrides); + + buildParameter2.cacheKey = buildParameter.cacheKey; + const baseImage2 = new ImageTag(buildParameter2); + const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); + CloudRunnerLogger.log(`run 2 succeeded`); + + const build2ContainsCacheKey = results2.includes(buildParameter.cacheKey); + const build2ContainsBuildGuid1FromRetainedWorkspace = results2.includes(buildParameter.buildGuid); + const build2ContainsRetainedWorkspacePhrase = results2.includes(`Retained Workspace:`); + const build2ContainsWorkspaceExistsAlreadyPhrase = results2.includes(`Retained Workspace Already Exists!`); + const build2ContainsBuildSucceeded = results2.includes(buildSucceededString); + const build2NotContainsNoLibraryMessage = !results2.includes(libraryString); + const build2NotContainsZeroLibraryCacheFilesMessage = !results2.includes( + 'There is 0 files/dir in the cache pulled contents for Library', + ); + const build2NotContainsZeroLFSCacheFilesMessage = !results2.includes( + 'There is 0 files/dir in the cache pulled contents for LFS', + ); + + expect(build2ContainsCacheKey).toBeTruthy(); + expect(build2ContainsRetainedWorkspacePhrase).toBeTruthy(); + expect(build2ContainsWorkspaceExistsAlreadyPhrase).toBeTruthy(); + expect(build2ContainsBuildGuid1FromRetainedWorkspace).toBeTruthy(); + expect(build2ContainsBuildSucceeded).toBeTruthy(); + expect(build2NotContainsZeroLibraryCacheFilesMessage).toBeTruthy(); + expect(build2NotContainsZeroLFSCacheFilesMessage).toBeTruthy(); + expect(build2NotContainsNoLibraryMessage).toBeTruthy(); + }, 1_000_000_000); + afterAll(async () => { + await SharedWorkspaceLocking.CleanupWorkspace(CloudRunner.lockedWorkspace || ``, CloudRunner.buildParameters); + if ( + fs.existsSync(`./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`) + ) { + CloudRunnerLogger.log( + `Cleaning up ./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`, + ); + await CloudRunnerSystem.Run( + `rm -r ./cloud-runner-cache/${path.basename(CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute)}`, + ); + } + }); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts b/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts new file mode 100644 index 0000000..6694cb7 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-s3-prebuilt-steps.test.ts @@ -0,0 +1,46 @@ +import CloudRunner from '../cloud-runner'; +import { BuildParameters, ImageTag } from '../..'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../cloud-runner-options'; +import setups from './cloud-runner-suite.test'; +import { CloudRunnerSystem } from '../services/cloud-runner-system'; + +async function CreateParameters(overrides) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner pre-built S3 steps', () => { + it('Responds', () => {}); + setups(); + if (CloudRunnerOptions.cloudRunnerDebug && CloudRunnerOptions.cloudRunnerCluster !== `local-docker`) { + it('Run build and prebuilt s3 cache pull, cache push and upload build', async () => { + const overrides = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + customStepFiles: `aws-s3-pull-cache,aws-s3-upload-cache,aws-s3-upload-build`, + }; + const buildParameter2 = await CreateParameters(overrides); + const baseImage2 = new ImageTag(buildParameter2); + const results2 = await CloudRunner.run(buildParameter2, baseImage2.toString()); + CloudRunnerLogger.log(`run 2 succeeded`); + + const build2ContainsBuildSucceeded = results2.includes('Build succeeded'); + expect(build2ContainsBuildSucceeded).toBeTruthy(); + + const results = await CloudRunnerSystem.RunAndReadLines( + `aws s3 ls s3://game-ci-test-storage/cloud-runner-cache/${buildParameter2.cacheKey}/`, + ); + CloudRunnerLogger.log(results.join(`,`)); + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/cloud-runner-suite.test.ts b/src/model/cloud-runner/tests/cloud-runner-suite.test.ts new file mode 100644 index 0000000..f362f78 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-suite.test.ts @@ -0,0 +1,25 @@ +import { Cli } from '../../cli/cli'; +import GitHub from '../../github'; + +describe('Cloud Runner', () => { + it('Responds', () => {}); +}); + +const setups = () => { + beforeAll(() => { + GitHub.githubInputEnabled = false; + }); + beforeEach(() => { + Cli.options = {}; + }); + afterEach(() => { + if (Cli.options !== undefined) { + delete Cli.options; + } + }); + afterAll(() => { + GitHub.githubInputEnabled = true; + }); +}; + +export default setups; diff --git a/src/model/cloud-runner/tests/cloud-runner-sync-environment.test.ts b/src/model/cloud-runner/tests/cloud-runner-sync-environment.test.ts new file mode 100644 index 0000000..2368d25 --- /dev/null +++ b/src/model/cloud-runner/tests/cloud-runner-sync-environment.test.ts @@ -0,0 +1,77 @@ +import { BuildParameters, ImageTag } from '../..'; +import CloudRunner from '../cloud-runner'; +import Input from '../../input'; +import { CloudRunnerStatics } from '../cloud-runner-statics'; +import { TaskParameterSerializer } from '../services/task-parameter-serializer'; +import UnityVersioning from '../../unity-versioning'; +import { Cli } from '../../cli/cli'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerOptions from '../cloud-runner-options'; +import setups from './cloud-runner-suite.test'; + +async function CreateParameters(overrides) { + if (overrides) Cli.options = overrides; + + return BuildParameters.create(); +} +describe('Cloud Runner Sync Environments', () => { + setups(); + const testSecretName = 'testSecretName'; + const testSecretValue = 'testSecretValue'; + it('Responds', () => {}); + + if (CloudRunnerOptions.cloudRunnerDebug) { + it('All build parameters sent to cloud runner as env vars', async () => { + // Setup parameters + const buildParameter = await CreateParameters({ + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.read('test-project'), + customJob: ` + - name: 'step 1' + image: 'ubuntu' + commands: 'printenv' + secrets: + - name: '${testSecretName}' + value: '${testSecretValue}' + `, + }); + const baseImage = new ImageTag(buildParameter); + + // Run the job + const file = await CloudRunner.run(buildParameter, baseImage.toString()); + + // Assert results + // expect(file).toContain(JSON.stringify(buildParameter)); + expect(file).toContain(`${Input.ToEnvVarFormat(testSecretName)}=${testSecretValue}`); + const environmentVariables = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameter); + const secrets = TaskParameterSerializer.readDefaultSecrets().map((x) => { + return { + name: x.EnvironmentVariable, + value: x.ParameterValue, + }; + }); + const combined = [...environmentVariables, ...secrets] + .filter((element) => element.value !== undefined && element.value !== '' && typeof element.value !== 'function') + .map((x) => { + if (typeof x.value === `string`) { + x.value = x.value.replace(/\s+/g, ''); + } + + return x; + }) + .filter((element) => { + return !['UNITY_LICENSE', 'CUSTOM_JOB'].includes(element.name); + }); + const newLinePurgedFile = file + .replace(/\s+/g, '') + .replace(new RegExp(`\\[${CloudRunnerStatics.logPrefix}\\]`, 'g'), ''); + for (const element of combined) { + expect(newLinePurgedFile).toContain(`${element.name}`); + CloudRunnerLogger.log(`Contains ${element.name}`); + const fullNameEqualValue = `${element.name}=${element.value}`; + expect(newLinePurgedFile).toContain(fullNameEqualValue); + } + }, 1_000_000_000); + } +}); diff --git a/src/model/cloud-runner/tests/shared-workspace-locking.test.ts b/src/model/cloud-runner/tests/shared-workspace-locking.test.ts new file mode 100644 index 0000000..21aadcc --- /dev/null +++ b/src/model/cloud-runner/tests/shared-workspace-locking.test.ts @@ -0,0 +1,99 @@ +import SharedWorkspaceLocking from '../services/shared-workspace-locking'; +import { Cli } from '../../cli/cli'; +import setups from './cloud-runner-suite.test'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { v4 as uuidv4 } from 'uuid'; +import CloudRunnerOptions from '../cloud-runner-options'; +import UnityVersioning from '../../unity-versioning'; +import BuildParameters from '../../build-parameters'; + +async function CreateParameters(overrides) { + if (overrides) { + Cli.options = overrides; + } + + return await BuildParameters.create(); +} + +describe('Cloud Runner Locking', () => { + setups(); + it('Responds', () => {}); + if (CloudRunnerOptions.cloudRunnerDebug) { + it(`Simple Locking Flow`, async () => { + Cli.options.retainWorkspaces = true; + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + }; + const buildParameters = await CreateParameters(overrides); + + const newWorkspaceName = `test-workspace-${uuidv4()}`; + const runId = uuidv4(); + await SharedWorkspaceLocking.CreateWorkspace(newWorkspaceName, buildParameters); + const isExpectedUnlockedBeforeLocking = + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; + expect(isExpectedUnlockedBeforeLocking).toBeTruthy(); + await SharedWorkspaceLocking.LockWorkspace(newWorkspaceName, runId, buildParameters); + const isExpectedLockedAfterLocking = + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === true; + expect(isExpectedLockedAfterLocking).toBeTruthy(); + const locksBeforeRelease = await SharedWorkspaceLocking.GetAllLocks(newWorkspaceName, buildParameters); + CloudRunnerLogger.log(JSON.stringify(locksBeforeRelease, undefined, 4)); + expect(locksBeforeRelease.length).toBe(1); + await SharedWorkspaceLocking.ReleaseWorkspace(newWorkspaceName, runId, buildParameters); + const locks = await SharedWorkspaceLocking.GetAllLocks(newWorkspaceName, buildParameters); + expect(locks.length).toBe(0); + const isExpectedLockedAfterReleasing = + (await SharedWorkspaceLocking.IsWorkspaceLocked(newWorkspaceName, buildParameters)) === false; + expect(isExpectedLockedAfterReleasing).toBeTruthy(); + }, 150000); + it.skip('All Locking Actions', async () => { + Cli.options.retainWorkspaces = true; + const overrides: any = { + versioning: 'None', + projectPath: 'test-project', + unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')), + targetPlatform: 'StandaloneLinux64', + cacheKey: `test-case-${uuidv4()}`, + }; + const buildParameters = await CreateParameters(overrides); + + CloudRunnerLogger.log( + `GetAllWorkspaces ${JSON.stringify(await SharedWorkspaceLocking.GetAllWorkspaces(buildParameters))}`, + ); + CloudRunnerLogger.log( + `GetFreeWorkspaces ${JSON.stringify(await SharedWorkspaceLocking.GetFreeWorkspaces(buildParameters))}`, + ); + CloudRunnerLogger.log( + `IsWorkspaceLocked ${JSON.stringify( + await SharedWorkspaceLocking.IsWorkspaceLocked(`test-workspace-${uuidv4()}`, buildParameters), + )}`, + ); + CloudRunnerLogger.log( + `GetFreeWorkspaces ${JSON.stringify(await SharedWorkspaceLocking.GetFreeWorkspaces(buildParameters))}`, + ); + CloudRunnerLogger.log( + `LockWorkspace ${JSON.stringify( + await SharedWorkspaceLocking.LockWorkspace(`test-workspace-${uuidv4()}`, uuidv4(), buildParameters), + )}`, + ); + CloudRunnerLogger.log( + `CreateLockableWorkspace ${JSON.stringify( + await SharedWorkspaceLocking.CreateWorkspace(`test-workspace-${uuidv4()}`, buildParameters), + )}`, + ); + CloudRunnerLogger.log( + `GetLockedWorkspace ${JSON.stringify( + await SharedWorkspaceLocking.GetOrCreateLockedWorkspace( + `test-workspace-${uuidv4()}`, + uuidv4(), + buildParameters, + ), + )}`, + ); + }, 3000000); + } +}); diff --git a/src/model/cloud-runner/workflows/build-automation-workflow.ts b/src/model/cloud-runner/workflows/build-automation-workflow.ts index 089997f..b880da8 100644 --- a/src/model/cloud-runner/workflows/build-automation-workflow.ts +++ b/src/model/cloud-runner/workflows/build-automation-workflow.ts @@ -1,31 +1,31 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger.ts'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders.ts'; -import { CloudRunnerStepState } from '../cloud-runner-step-state.ts'; -import { CustomWorkflow } from './custom-workflow.ts'; -import { WorkflowInterface } from './workflow-interface.ts'; -import { core, path } from '../../../dependencies.ts'; -import { CloudRunnerBuildCommandProcessor } from '../services/cloud-runner-build-command-process.ts'; -import CloudRunner from '../cloud-runner.ts'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import { CloudRunnerStepState } from '../cloud-runner-step-state'; +import { WorkflowInterface } from './workflow-interface'; +import * as core from '@actions/core'; +import { CloudRunnerCustomHooks } from '../services/cloud-runner-custom-hooks'; +import path from 'path'; +import CloudRunner from '../cloud-runner'; +import CloudRunnerOptions from '../cloud-runner-options'; +import { CloudRunnerCustomSteps } from '../services/cloud-runner-custom-steps'; export class BuildAutomationWorkflow implements WorkflowInterface { async run(cloudRunnerStepState: CloudRunnerStepState) { try { - return await BuildAutomationWorkflow.standardBuildAutomation(cloudRunnerStepState.image); + return await BuildAutomationWorkflow.standardBuildAutomation(cloudRunnerStepState.image, cloudRunnerStepState); } catch (error) { throw error; } } - private static async standardBuildAutomation(baseImage: any) { + private static async standardBuildAutomation(baseImage: any, cloudRunnerStepState: CloudRunnerStepState) { + // TODO accept post and pre build steps as yaml files in the repo try { CloudRunnerLogger.log(`Cloud Runner is running standard build automation`); - if (!CloudRunner.buildParameters.isCliMode) core.startGroup('pre build steps'); let output = ''; - if (CloudRunner.buildParameters.preBuildSteps !== '') { - output += await CustomWorkflow.runCustomJob(CloudRunner.buildParameters.preBuildSteps); - } - if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + + output += await CloudRunnerCustomSteps.RunPreBuildSteps(cloudRunnerStepState); CloudRunnerLogger.logWithTime('Configurable pre build step(s) time'); if (!CloudRunner.buildParameters.isCliMode) core.startGroup('build'); @@ -33,23 +33,19 @@ export class BuildAutomationWorkflow implements WorkflowInterface { CloudRunnerLogger.logLine(` `); CloudRunnerLogger.logLine('Starting build automation job'); - output += await CloudRunner.Provider.runTask( + output += await CloudRunner.Provider.runTaskInWorkflow( CloudRunner.buildParameters.buildGuid, baseImage.toString(), BuildAutomationWorkflow.BuildWorkflow, `/${CloudRunnerFolders.buildVolumeFolder}`, `/${CloudRunnerFolders.buildVolumeFolder}/`, - CloudRunner.cloudRunnerEnvironmentVariables, - CloudRunner.defaultSecrets, + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, ); if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); CloudRunnerLogger.logWithTime('Build time'); - if (!CloudRunner.buildParameters.isCliMode) core.startGroup('post build steps'); - if (CloudRunner.buildParameters.postBuildSteps !== '') { - output += await CustomWorkflow.runCustomJob(CloudRunner.buildParameters.postBuildSteps); - } - if (!CloudRunner.buildParameters.isCliMode) core.endGroup(); + output += await CloudRunnerCustomSteps.RunPostBuildSteps(cloudRunnerStepState); CloudRunnerLogger.logWithTime('Configurable post build step(s) time'); CloudRunnerLogger.log(`Cloud Runner finished running standard build automation`); @@ -61,62 +57,83 @@ export class BuildAutomationWorkflow implements WorkflowInterface { } private static get BuildWorkflow() { - const setupHooks = CloudRunnerBuildCommandProcessor.getHooks(CloudRunner.buildParameters.customJobHooks).filter( - (x) => x.step.includes(`setup`), + const setupHooks = CloudRunnerCustomHooks.getHooks(CloudRunner.buildParameters.customJobHooks).filter((x) => + x.step.includes(`setup`), ); - const buildHooks = CloudRunnerBuildCommandProcessor.getHooks(CloudRunner.buildParameters.customJobHooks).filter( - (x) => x.step.includes(`build`), + const buildHooks = CloudRunnerCustomHooks.getHooks(CloudRunner.buildParameters.customJobHooks).filter((x) => + x.step.includes(`build`), + ); + const builderPath = CloudRunnerFolders.ToLinuxFolder( + path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', `index.js`), ); - const builderPath = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', `index.js`).replace(/\\/g, `/`); return `apt-get update > /dev/null - apt-get install -y tar tree npm git-lfs jq git > /dev/null - npm install -g n > /dev/null - n stable > /dev/null + apt-get install -y curl tar tree npm git-lfs jq git > /dev/null + npm i -g n > /dev/null + n 16.15.1 > /dev/null + npm --version + node --version + ${BuildAutomationWorkflow.TreeCommand} ${setupHooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} - export GITHUB_WORKSPACE="${CloudRunnerFolders.repoPathAbsolute.replace(/\\/g, `/`)}" + export GITHUB_WORKSPACE="${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.repoPathAbsolute)}" ${BuildAutomationWorkflow.setupCommands(builderPath)} ${setupHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} + ${BuildAutomationWorkflow.TreeCommand} ${buildHooks.filter((x) => x.hook.includes(`before`)).map((x) => x.commands) || ' '} - ${BuildAutomationWorkflow.BuildCommands(builderPath, CloudRunner.buildParameters.buildGuid)} - ${buildHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '}`; + ${BuildAutomationWorkflow.BuildCommands(builderPath)} + ${buildHooks.filter((x) => x.hook.includes(`after`)).map((x) => x.commands) || ' '} + ${BuildAutomationWorkflow.TreeCommand}`; } private static setupCommands(builderPath) { - return `export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 - echo "game ci cloud runner clone" - mkdir -p ${CloudRunnerFolders.builderPathAbsolute.replace(/\\/g, `/`)} - git clone -q -b ${CloudRunner.buildParameters.cloudRunnerBranch} ${ + const commands = `mkdir -p ${CloudRunnerFolders.ToLinuxFolder( + CloudRunnerFolders.builderPathAbsolute, + )} && git clone -q -b ${CloudRunner.buildParameters.cloudRunnerBranch} ${ CloudRunnerFolders.unityBuilderRepoUrl - } "${CloudRunnerFolders.builderPathAbsolute.replace(/\\/g, `/`)}" - chmod +x ${builderPath} - echo "game ci cloud runner bootstrap" - node ${builderPath} -m remote-cli`; + } "${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.builderPathAbsolute)}" && chmod +x ${builderPath}`; + + const retainedWorkspaceCommands = `if [ -e "${CloudRunnerFolders.ToLinuxFolder( + CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, + )}" ] && [ -e "${CloudRunnerFolders.ToLinuxFolder( + path.join(CloudRunnerFolders.repoPathAbsolute, `.git`), + )}" ]; then echo "Retained Workspace Already Exists!" ; fi`; + + const cloneBuilderCommands = `if [ -e "${CloudRunnerFolders.ToLinuxFolder( + CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute, + )}" ] && [ -e "${CloudRunnerFolders.ToLinuxFolder( + path.join(CloudRunnerFolders.builderPathAbsolute, `.git`), + )}" ]; then echo "Builder Already Exists!"; else ${commands}; fi`; + + return `export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 + echo "downloading game-ci..." + ${retainedWorkspaceCommands} + ${cloneBuilderCommands} + echo "bootstrap game ci cloud runner..." + node ${builderPath} -m remote-cli-pre-build`; } - private static BuildCommands(builderPath, guid) { - const linuxCacheFolder = CloudRunnerFolders.cacheFolderFull.replace(/\\/g, `/`); + private static BuildCommands(builderPath) { const distFolder = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist'); const ubuntuPlatformsFolder = path.join(CloudRunnerFolders.builderPathAbsolute, 'dist', 'platforms', 'ubuntu'); - return `echo "game ci cloud runner init" - mkdir -p ${`${CloudRunnerFolders.projectBuildFolderAbsolute}/build`.replace(/\\/g, `/`)} - cd ${CloudRunnerFolders.projectPathAbsolute} - cp -r "${path.join(distFolder, 'default-build-script').replace(/\\/g, `/`)}" "/UnityBuilderAction" - cp -r "${path.join(ubuntuPlatformsFolder, 'entrypoint.sh').replace(/\\/g, `/`)}" "/entrypoint.sh" - cp -r "${path.join(ubuntuPlatformsFolder, 'steps').replace(/\\/g, `/`)}" "/steps" + return `echo "game ci cloud runner initalized" + mkdir -p ${`${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectBuildFolderAbsolute)}/build`} + cd ${CloudRunnerFolders.ToLinuxFolder(CloudRunnerFolders.projectPathAbsolute)} + cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(distFolder, 'default-build-script'))}" "/UnityBuilderAction" + cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(ubuntuPlatformsFolder, 'entrypoint.sh'))}" "/entrypoint.sh" + cp -r "${CloudRunnerFolders.ToLinuxFolder(path.join(ubuntuPlatformsFolder, 'steps'))}" "/steps" chmod -R +x "/entrypoint.sh" chmod -R +x "/steps" - echo "game ci cloud runner start" + echo "game ci start" /entrypoint.sh - echo "game ci cloud runner push library to cache" + echo "game ci caching results" chmod +x ${builderPath} - node ${builderPath} -m cache-push --cachePushFrom ${ - CloudRunnerFolders.libraryFolderAbsolute - } --artifactName lib-${guid} --cachePushTo ${linuxCacheFolder}/Library - echo "game ci cloud runner push build to cache" - node ${builderPath} -m cache-push --cachePushFrom ${ - CloudRunnerFolders.projectBuildFolderAbsolute - } --artifactName build-${guid} --cachePushTo ${`${linuxCacheFolder}/build`.replace(/\\/g, `/`)}`; + node ${builderPath} -m remote-cli-post-build`; + } + + private static get TreeCommand(): string { + return CloudRunnerOptions.cloudRunnerDebugTree + ? `tree -L 2 ${CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute} && tree -L 2 ${CloudRunnerFolders.cacheFolderForCacheKeyFull} && du -h -s /${CloudRunnerFolders.buildVolumeFolder}/ && du -h -s ${CloudRunnerFolders.cacheFolderForAllFull}` + : `#`; } } diff --git a/src/model/cloud-runner/workflows/custom-workflow.ts b/src/model/cloud-runner/workflows/custom-workflow.ts index 056e2d9..2392ec7 100644 --- a/src/model/cloud-runner/workflows/custom-workflow.ts +++ b/src/model/cloud-runner/workflows/custom-workflow.ts @@ -1,41 +1,43 @@ -import CloudRunnerLogger from '../services/cloud-runner-logger.ts'; -import CloudRunnerSecret from '../services/cloud-runner-secret.ts'; -import { CloudRunnerFolders } from '../services/cloud-runner-folders.ts'; -import { yaml } from '../../../dependencies.ts'; -import { CloudRunner, Input } from '../../index.ts'; +import CloudRunnerLogger from '../services/cloud-runner-logger'; +import CloudRunnerSecret from '../services/cloud-runner-secret'; +import { CloudRunnerFolders } from '../services/cloud-runner-folders'; +import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable'; +import { CloudRunnerCustomSteps, CustomStep } from '../services/cloud-runner-custom-steps'; +import CloudRunner from '../cloud-runner'; export class CustomWorkflow { - public static async runCustomJob(buildSteps) { + public static async runCustomJobFromString( + buildSteps: string, + environmentVariables: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ): Promise { + return await CustomWorkflow.runCustomJob( + CloudRunnerCustomSteps.ParseSteps(buildSteps), + environmentVariables, + secrets, + ); + } + + public static async runCustomJob( + buildSteps: CustomStep[], + environmentVariables: CloudRunnerEnvironmentVariable[], + secrets: CloudRunnerSecret[], + ) { try { CloudRunnerLogger.log(`Cloud Runner is running in custom job mode`); - if (CloudRunner.buildParameters.cloudRunnerIntegrationTests) { - CloudRunnerLogger.log(`Parsing build steps: ${buildSteps}`); - } - try { - buildSteps = yaml.parse(buildSteps); - } catch (error) { - CloudRunnerLogger.log(`failed to parse a custom job "${buildSteps}"`); - throw error; - } let output = ''; + if (CloudRunner.buildParameters?.cloudRunnerDebug) { + CloudRunnerLogger.log(`Custom Job Description \n${JSON.stringify(buildSteps, undefined, 4)}`); + } for (const step of buildSteps) { - const stepSecrets: CloudRunnerSecret[] = step.secrets.map((x) => { - const secret: CloudRunnerSecret = { - ParameterKey: x.name, - EnvironmentVariable: Input.toEnvVarFormat(x.name), - ParameterValue: x.value, - }; - - return secret; - }); - output += await CloudRunner.Provider.runTask( + output += await CloudRunner.Provider.runTaskInWorkflow( CloudRunner.buildParameters.buildGuid, - step['image'], - step['commands'], + step.image, + step.commands, `/${CloudRunnerFolders.buildVolumeFolder}`, - `/${CloudRunnerFolders.buildVolumeFolder}/`, - CloudRunner.cloudRunnerEnvironmentVariables, - [...CloudRunner.defaultSecrets, ...stepSecrets], + `/${CloudRunnerFolders.projectPathAbsolute}/`, + environmentVariables, + [...secrets, ...step.secrets], ); } diff --git a/src/model/cloud-runner/workflows/workflow-composition-root.ts b/src/model/cloud-runner/workflows/workflow-composition-root.ts index 47fcbe8..c67b985 100644 --- a/src/model/cloud-runner/workflows/workflow-composition-root.ts +++ b/src/model/cloud-runner/workflows/workflow-composition-root.ts @@ -1,26 +1,26 @@ -import { CloudRunnerStepState } from '../cloud-runner-step-state.ts'; -import { CustomWorkflow } from './custom-workflow.ts'; -import { WorkflowInterface } from './workflow-interface.ts'; -import { BuildAutomationWorkflow } from './build-automation-workflow.ts'; -import CloudRunner from '../cloud-runner.ts'; +import { CloudRunnerStepState } from '../cloud-runner-step-state'; +import { CustomWorkflow } from './custom-workflow'; +import { WorkflowInterface } from './workflow-interface'; +import { BuildAutomationWorkflow } from './build-automation-workflow'; +import CloudRunner from '../cloud-runner'; export class WorkflowCompositionRoot implements WorkflowInterface { async run(cloudRunnerStepState: CloudRunnerStepState) { - try { - return await WorkflowCompositionRoot.runJob(cloudRunnerStepState.image.toString()); - } catch (error) { - throw error; - } - } - - private static async runJob(baseImage: any) { try { if (CloudRunner.buildParameters.customJob !== '') { - return await CustomWorkflow.runCustomJob(CloudRunner.buildParameters.customJob); + return await CustomWorkflow.runCustomJobFromString( + CloudRunner.buildParameters.customJob, + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, + ); } return await new BuildAutomationWorkflow().run( - new CloudRunnerStepState(baseImage, CloudRunner.cloudRunnerEnvironmentVariables, CloudRunner.defaultSecrets), + new CloudRunnerStepState( + cloudRunnerStepState.image.toString(), + cloudRunnerStepState.environment, + cloudRunnerStepState.secrets, + ), ); } catch (error) { throw error; diff --git a/src/model/cloud-runner/workflows/workflow-interface.ts b/src/model/cloud-runner/workflows/workflow-interface.ts index 68bda86..3e8763e 100644 --- a/src/model/cloud-runner/workflows/workflow-interface.ts +++ b/src/model/cloud-runner/workflows/workflow-interface.ts @@ -1,5 +1,8 @@ -import { CloudRunnerStepState } from '../cloud-runner-step-state.ts'; +import { CloudRunnerStepState } from '../cloud-runner-step-state'; export interface WorkflowInterface { - run(cloudRunnerStepState: CloudRunnerStepState); + run( + // eslint-disable-next-line no-unused-vars + cloudRunnerStepState: CloudRunnerStepState, + ); } diff --git a/src/model/github.ts b/src/model/github.ts new file mode 100644 index 0000000..4f98145 --- /dev/null +++ b/src/model/github.ts @@ -0,0 +1,5 @@ +class GitHub { + public static githubInputEnabled: boolean = true; +} + +export default GitHub; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..fb57ccd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +