Skip to content

Commit f1942a3

Browse files
Merge pull request #15 from agrasth/RTDEV-50754
Worker Sample Improvement for restrict-overwrite
2 parents d20bd99 + ba5af41 commit f1942a3

File tree

20 files changed

+696
-24
lines changed

20 files changed

+696
-24
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Artifactory Restrict Overwrite: Before Copy
2+
3+
## Overview
4+
The "Restrict Overwrite: Before Copy" worker script enforces immutability by preventing overwrites of artifacts during the "copy" operation in JFrog Artifactory. It ensures that artifacts cannot be accidentally replaced, maintaining consistency and integrity across deployments.
5+
6+
## Features
7+
- **Overwrite Protection**: Blocks attempts to copy artifacts if an artifact with the same name already exists in the target path.
8+
- **Folder Handling**: If the target is a folder, the script allows the operation only when the folder is empty.
9+
- **Error Logging**: Logs all operations, including failed attempts, for debugging and auditing purposes.
10+
11+
## Behavior
12+
- If the target artifact already exists:
13+
- The script stops the operation and returns a message indicating the conflict.
14+
- If the target artifact does not exist:
15+
- The operation proceeds without interruption.
16+
17+
## Example Responses
18+
### File Does Not Exist
19+
```json
20+
{
21+
"status": 1
22+
}
23+
```
24+
### File Already Exists
25+
```json
26+
{
27+
"status": 2,
28+
"message": "example-repo:file.txt already exists"
29+
}
30+
```
31+
## Usage
32+
33+
### Execution Flow
34+
1. **Trigger Point**: This worker script is triggered before the "copy" operation in Artifactory.
35+
36+
2. **AQL Query**: The script runs an AQL query to check if the target artifact exists in the specified repository path.
37+
38+
3. **Decision**: Based on the existence of the target artifact:
39+
- **Proceed**: If the artifact does not exist.
40+
- **Stop**: If the artifact already exists, with an appropriate error message.
41+
42+
### Implementation Notes
43+
- **Logging**: All operations and exceptions are logged for traceability.
44+
45+
- **AQL Query**: The AQL query fetches information about the target artifact, including its type (file or folder).
46+
47+
## Limitations
48+
- **Specific to Copy**: This script applies only to the "before copy" trigger. Separate implementations are required for "before create," "before upload," and "before move" operations.
49+
50+
- **Flat Repositories**: The script supports flat repository structures. For deeply nested repositories, ensure the paths are correctly configured.
51+
52+
## Notes
53+
- **Immutability Enforcement**: Ensures existing artifacts remain unchanged unless explicitly deleted beforehand.
54+
55+
- **Folder Handling**: Prevents overwrites only if the folder already exists and is not empty.
56+
57+
- **Error Logging**: Provides detailed logs for troubleshooting and auditing.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "restrict-overwrite",
3+
"description": "Restrict overwrite",
4+
"filterCriteria": {
5+
"artifactFilterCriteria": {
6+
"repoKeys": [
7+
"example-repo-local"
8+
]
9+
}
10+
},
11+
"secrets": {},
12+
"sourceCodePath": "./worker.ts",
13+
"action": "BEFORE_COPY",
14+
"enabled": false,
15+
"debug": true
16+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "restrict-overwrite",
3+
"description": "Run a script on BEFORE_COPY",
4+
"version": "1.0.0",
5+
"scripts": {
6+
"deploy": "jf worker deploy",
7+
"undeploy": "jf worker rm \"restrict-overwrite\"",
8+
"test": "jest"
9+
},
10+
"license": "ISC",
11+
"devDependencies": {
12+
"jfrog-workers": "^0.4.0",
13+
"@golevelup/ts-jest": "^0.4.0",
14+
"@types/jest": "^29.5.12",
15+
"jest": "^29.7.0",
16+
"jest-jasmine2": "^29.7.0",
17+
"ts-jest": "^29.1.2"
18+
},
19+
"jest": {
20+
"moduleFileExtensions": [
21+
"ts",
22+
"js"
23+
],
24+
"rootDir": ".",
25+
"testEnvironment": "node",
26+
"clearMocks": true,
27+
"maxConcurrency": 1,
28+
"testRegex": "\\.spec\\.ts$",
29+
"moduleDirectories": ["node_modules"],
30+
"collectCoverageFrom": [
31+
"**/*.ts"
32+
],
33+
"coverageDirectory": "../coverage",
34+
"transform": {
35+
"^.+\\.(t|j)s$": "ts-jest"
36+
},
37+
"testRunner": "jest-jasmine2",
38+
"verbose": true
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"declaration": true,
5+
"target": "es2017",
6+
"skipLibCheck": true,
7+
"forceConsistentCasingInFileNames": false,
8+
"noFallthroughCasesInSwitch": false,
9+
"allowJs": true
10+
},
11+
"include": [
12+
"**/*.ts",
13+
"node_modules/@types/**/*.d.ts"
14+
]
15+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { PlatformContext, BeforeCopyRequest, PlatformClients, PlatformHttpClient, ActionStatus } from 'jfrog-workers';
2+
import { createMock, DeepMocked } from '@golevelup/ts-jest';
3+
import runWorker from './worker';
4+
5+
describe("restrict-overwrite tests", () => {
6+
let context: DeepMocked<PlatformContext>;
7+
let request: DeepMocked<BeforeCopyRequest>;
8+
9+
beforeEach(() => {
10+
context = createMock<PlatformContext>({
11+
clients: createMock<PlatformClients>({
12+
platformHttp: createMock<PlatformHttpClient>({
13+
get: jest.fn().mockResolvedValue({ status: 200 })
14+
})
15+
})
16+
});
17+
request = createMock<BeforeCopyRequest>({
18+
metadata: { repoPath: { key: 'my-repo', path: 'artifact.txt' } }
19+
});
20+
})
21+
22+
it('should run', async () => {
23+
await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({
24+
message: 'Overwritten by worker-service if an error occurs.',
25+
status: ActionStatus.PROCEED,
26+
modifiedRepoPath: { key: 'my-repo', path: 'artifact.txt' }
27+
}))
28+
})
29+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { PlatformContext, BeforeCopyRequest, BeforeCopyResponse, ActionStatus} from 'jfrog-workers';
2+
3+
4+
export default async function (context: PlatformContext, data: BeforeCopyRequest): Promise<Partial<BeforeCopyResponse>> {
5+
try {
6+
return restrictOverwrite(context, data);
7+
} catch (x) {
8+
return { status: ActionStatus.STOP, message: x.message };
9+
}
10+
}
11+
12+
async function restrictOverwrite(context: PlatformContext, data: BeforeCopyRequest): Promise<Partial<BeforeCopyResponse>> {
13+
const existingItem = await getExistingItemInfo(context, data.targetRepoPath);
14+
15+
if (existingItem && !(data.metadata.repoPath.isFolder && existingItem.isFolder)) {
16+
return {
17+
status: ActionStatus.STOP,
18+
message: `${data.metadata.repoPath.id} already exists`,
19+
};
20+
}
21+
22+
return { status: ActionStatus.PROCEED };
23+
}
24+
25+
async function getExistingItemInfo(context: PlatformContext, repoPath: RepoPath): Promise<RepoItemInfo | undefined> {
26+
const pathParts = repoPath.path.split('/')
27+
const query = {
28+
repo: repoPath.key,
29+
path: pathParts.length === 1 ? '.' : pathParts.slice(0, pathParts.length - 1).join('/'),
30+
name: pathParts[pathParts.length - 1]
31+
};
32+
const aqlResult = await runAql(context, `items.find(${JSON.stringify(query)}).include("type").limit(1)`)
33+
if (aqlResult.length) {
34+
return { isFolder: aqlResult[0].type === 'folder' };
35+
}
36+
}
37+
38+
async function runAql(context: PlatformContext, query: string) {
39+
console.log(`Running AQL: ${query}`)
40+
try {
41+
const queryResponse = await context.clients.platformHttp.post(
42+
'/artifactory/api/search/aql',
43+
query,
44+
{
45+
'Content-Type': 'text/plain'
46+
});
47+
return (queryResponse.data.results || []) as Array<any>;
48+
} catch (x) {
49+
console.log(`AQL query failed: ${x.message}`);
50+
}
51+
return [];
52+
}
53+
54+
interface RepoItemInfo {
55+
isFolder: boolean
56+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
# Artifactory Restrict Overwrite: Before Create
3+
4+
## Overview
5+
6+
The "Restrict Overwrite: Before Create" worker script enforces immutability by preventing overwrites of artifacts during the "create" operation in JFrog Artifactory. It ensures that artifacts cannot be accidentally replaced, maintaining consistency and integrity across deployments.
7+
8+
## Features
9+
10+
- **Overwrite Protection**: Blocks attempts to create artifacts if an artifact with the same name already exists in the target path.
11+
- **Folder Handling**: If the target is a folder, the script allows the operation only when the folder is empty.
12+
- **Error Logging**: Logs all operations, including failed attempts, for debugging and auditing purposes.
13+
14+
## Behavior
15+
16+
- If the target artifact already exists:
17+
- The script stops the operation and returns a message indicating the conflict.
18+
- If the target artifact does not exist:
19+
- The operation proceeds without interruption.
20+
21+
## Example Responses
22+
23+
### File Does Not Exist
24+
```json
25+
{
26+
"status": 1
27+
}
28+
```
29+
30+
### File Already Exists
31+
```json
32+
{
33+
"status": 2,
34+
"message": "example-repo:file.txt already exists"
35+
}
36+
```
37+
38+
## Usage
39+
40+
### Execution Flow
41+
1. **Trigger Point**: This worker script is triggered before the "create" operation in Artifactory.
42+
2. **AQL Query**: The script runs an AQL query to check if the target artifact exists in the specified repository path.
43+
3. **Decision**: Based on the existence of the target artifact:
44+
- **Proceed**: If the artifact does not exist.
45+
- **Stop**: If the artifact already exists, with an appropriate error message.
46+
47+
### Implementation Notes
48+
- **Logging**: All operations and exceptions are logged for traceability.
49+
- **AQL Query**: The AQL query fetches information about the target artifact, including its type (file or folder).
50+
51+
## Limitations
52+
53+
- **Specific to Create**: This script applies only to the "before create" trigger. Separate implementations are required for "before upload," "before copy," and "before move" operations.
54+
- **Flat Repositories**: The script supports flat repository structures. For deeply nested repositories, ensure the paths are correctly configured.
55+
56+
## Notes
57+
58+
- **Immutability Enforcement**: Ensures existing artifacts remain unchanged unless explicitly deleted beforehand.
59+
- **Folder Handling**: Prevents overwrites only if the folder already exists and is not empty.
60+
- **Error Logging**: Provides detailed logs for troubleshooting and auditing.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "restrict-overwrite",
3+
"description": "Restrict overwrite",
4+
"filterCriteria": {
5+
"artifactFilterCriteria": {
6+
"repoKeys": [
7+
"example-repo-local"
8+
]
9+
}
10+
},
11+
"secrets": {},
12+
"sourceCodePath": "./worker.ts",
13+
"action": "BEFORE_CREATE",
14+
"enabled": false,
15+
"debug": true
16+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "restrict-overwrite",
3+
"description": "Run a script on BEFORE_CREATE",
4+
"version": "1.0.0",
5+
"scripts": {
6+
"deploy": "jf worker deploy",
7+
"undeploy": "jf worker rm \"restrict-overwrite\"",
8+
"test": "jest"
9+
},
10+
"license": "ISC",
11+
"devDependencies": {
12+
"jfrog-workers": "^0.4.0",
13+
"@golevelup/ts-jest": "^0.4.0",
14+
"@types/jest": "^29.5.12",
15+
"jest": "^29.7.0",
16+
"jest-jasmine2": "^29.7.0",
17+
"ts-jest": "^29.1.2"
18+
},
19+
"jest": {
20+
"moduleFileExtensions": [
21+
"ts",
22+
"js"
23+
],
24+
"rootDir": ".",
25+
"testEnvironment": "node",
26+
"clearMocks": true,
27+
"maxConcurrency": 1,
28+
"testRegex": "\\.spec\\.ts$",
29+
"moduleDirectories": ["node_modules"],
30+
"collectCoverageFrom": [
31+
"**/*.ts"
32+
],
33+
"coverageDirectory": "../coverage",
34+
"transform": {
35+
"^.+\\.(t|j)s$": "ts-jest"
36+
},
37+
"testRunner": "jest-jasmine2",
38+
"verbose": true
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"declaration": true,
5+
"target": "es2017",
6+
"skipLibCheck": true,
7+
"forceConsistentCasingInFileNames": false,
8+
"noFallthroughCasesInSwitch": false,
9+
"allowJs": true
10+
},
11+
"include": [
12+
"**/*.ts",
13+
"node_modules/@types/**/*.d.ts"
14+
]
15+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { PlatformContext, BeforeCreateRequest, PlatformClients, PlatformHttpClient, ActionStatus } from 'jfrog-workers';
2+
import { createMock, DeepMocked } from '@golevelup/ts-jest';
3+
import runWorker from './worker';
4+
5+
describe("restrict-overwrite tests", () => {
6+
let context: DeepMocked<PlatformContext>;
7+
let request: DeepMocked<BeforeCreateRequest>;
8+
9+
beforeEach(() => {
10+
context = createMock<PlatformContext>({
11+
clients: createMock<PlatformClients>({
12+
platformHttp: createMock<PlatformHttpClient>({
13+
get: jest.fn().mockResolvedValue({ status: 200 })
14+
})
15+
})
16+
});
17+
request = createMock<BeforeCreateRequest>({
18+
metadata: { repoPath: { key: 'my-repo', path: 'artifact.txt' } }
19+
});
20+
})
21+
22+
it('should run', async () => {
23+
await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({
24+
message: 'Overwritten by worker-service if an error occurs.',
25+
status: ActionStatus.PROCEED,
26+
modifiedRepoPath: { key: 'my-repo', path: 'artifact.txt' }
27+
}))
28+
})
29+
});

0 commit comments

Comments
 (0)