├── .nvmrc ├── .eslintignore ├── test └── commands │ ├── resources │ ├── validate.valid.yaml │ ├── create-issue-input.not-valid.yaml │ └── create-issue-input.valid.yaml │ ├── configure.test.ts │ └── create-issue.test.ts ├── .prettierrc.json ├── src ├── index.ts ├── types │ ├── validate.ts │ ├── service.ts │ └── issue.ts ├── sleek-command.ts ├── utils.ts ├── services │ ├── base-service.ts │ ├── helm.ts │ ├── create-issue.ts │ ├── schemaValidation.ts │ └── validate.ts ├── commandOpts │ ├── create-issue.ts │ └── validate.ts └── commands │ ├── create-issue.ts │ └── validate.ts ├── bin ├── run.cmd ├── dev.cmd ├── run.js └── dev.js ├── NOTICE ├── doc ├── img │ └── flow-diagram.jpg ├── demoCommands.md ├── userJourney.md ├── contributing.md └── examples │ └── onboarding.example.yaml ├── .gitignore ├── .eslintrc.json ├── .mocharc.json ├── CODE_OF_CONDUCT.md ├── aws-sleek-transformer.iml ├── tsconfig.json ├── .github └── workflows │ └── ci-docs-workflow.yaml ├── Makefile ├── install.sh ├── package.json ├── CONTRIBUTING.md ├── schema └── onboarding.schema.json ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.19.0 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /tmp 3 | -------------------------------------------------------------------------------- /test/commands/resources/validate.valid.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@oclif/prettier-config" 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/core'; -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /test/commands/resources/create-issue-input.not-valid.yaml: -------------------------------------------------------------------------------- 1 | accountId: 012345678901 2 | 3 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /doc/img/flow-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/addons-transformer-for-amazon-eks/main/doc/img/flow-diagram.jpg -------------------------------------------------------------------------------- /src/types/validate.ts: -------------------------------------------------------------------------------- 1 | export type ValidateOptions = { 2 | skipHooksValidation?: boolean 3 | skipReleaseService?: boolean 4 | } 5 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | async function main() { 4 | const {execute} = await import('@oclif/core') 5 | await execute({dir: import.meta.url}) 6 | } 7 | 8 | await main() 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | *-debug.log 3 | *-error.log 4 | /.idea 5 | /.nyc_output 6 | /dist 7 | /lib 8 | /package-lock.json 9 | /tmp 10 | /yarn.lock 11 | node_modules 12 | oclif.lock 13 | oclif.manifest.json 14 | *.tgz 15 | **/unzipped-*/ 16 | *.iml 17 | .idea 18 | 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "perfectionist/sort-objects": "off", 4 | "perfectionist/sort-classes": "off", 5 | "perfectionist/sort-imports": "off", 6 | "unicorn/no-static-only-class": "off" 7 | }, 8 | "extends": ["oclif", "oclif-typescript", "prettier"] 9 | } 10 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning 2 | 3 | // eslint-disable-next-line node/shebang 4 | async function main() { 5 | const {execute} = await import('@oclif/core') 6 | await execute({development: true, dir: import.meta.url}) 7 | } 8 | 9 | await main() 10 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register" 4 | ], 5 | "watch-extensions": [ 6 | "ts" 7 | ], 8 | "recursive": true, 9 | "reporter": "spec", 10 | "timeout": 60000, 11 | "node-option": [ 12 | "loader=ts-node/esm", 13 | "experimental-specifier-resolution=node" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/types/service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable perfectionist/sort-object-types */ 2 | export type ServiceResponse = { 3 | success: boolean, 4 | body?: T, 5 | error?: { 6 | input: Error | string, 7 | options?: { 8 | code?: string; 9 | exit?: number; 10 | } 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /aws-sleek-transformer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "nodenext", 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "strict": true, 8 | "target": "es2022", 9 | "moduleResolution": "nodenext", 10 | "typeRoots": [ 11 | "./node_modules/@types" 12 | ] 13 | }, 14 | "include": [ 15 | "./src/**/*", 16 | "./src/schemas/*.schema.json" 17 | ], 18 | "ts-node": { 19 | "esm": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/commands/configure.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('configure', () => { 4 | test 5 | .stdout() 6 | .command(['configure']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['configure', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/sleek-command.ts: -------------------------------------------------------------------------------- 1 | import { Command, Config } from "@oclif/core"; 2 | 3 | /** 4 | * A base command that provides common functionality for all Sleek Transformer commands: 5 | * - Configuration Loading 6 | * - Logging 7 | * - Error Handling 8 | * - Tracing 9 | * 10 | * All implementations of this class need to implement the `run` method. 11 | */ 12 | export abstract class SleekCommand extends Command { 13 | 14 | public constructor(argv: string[], config: Config) { 15 | super(argv, config); 16 | } 17 | } -------------------------------------------------------------------------------- /.github/workflows/ci-docs-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: ci-docs-workflow 2 | run-name: Generating docs for ${{ github.ref }} 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | docs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | 14 | - name: Install deps 15 | run: npm i 16 | 17 | - name: Generate docs 18 | run: npm run prepack 19 | 20 | - name: Commit docs 21 | run: | 22 | git config --local user.email "action@github.com" 23 | git config --local user.name "GitHub Action" 24 | git commit -am "Generate docs: ${{ github.ref }}" 25 | git push 26 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-string-slice */ 2 | export function getChartNameFromUrl(repoUrl:string):string { 3 | return repoUrl.substring(repoUrl.lastIndexOf('/')+1 ,repoUrl.length) 4 | } 5 | 6 | export function getProtocolFromFullQualifiedUrl(helmChartUrl: string) { 7 | return helmChartUrl?.substring(0, helmChartUrl?.indexOf(':')) 8 | } 9 | 10 | export function getRepoFromFullChartUri(helmChartUrl: string) { 11 | return helmChartUrl.substring(0, helmChartUrl.lastIndexOf(':')); 12 | } 13 | 14 | export function getVersionTagFromChartUri(helmChartUrl: string) { 15 | return helmChartUrl.lastIndexOf(':') ? `${helmChartUrl.substring(helmChartUrl.lastIndexOf(':') + 1)}` : ''; 16 | } -------------------------------------------------------------------------------- /src/services/base-service.ts: -------------------------------------------------------------------------------- 1 | import {PrettyPrintableError} from "@oclif/core/lib/errors/index.js"; 2 | 3 | import {SleekCommand} from "../sleek-command.js"; 4 | 5 | 6 | export abstract class BaseService { 7 | private commandCaller: SleekCommand; 8 | 9 | constructor(commandCaller: SleekCommand) { 10 | this.commandCaller = commandCaller; 11 | } 12 | 13 | public error(input: Error | string, options?: { 14 | code?: string; 15 | exit?: number; 16 | } & PrettyPrintableError): never { 17 | this.commandCaller.error(input, options) 18 | } 19 | 20 | public log(message?: string, ...args: any[]): void{ 21 | this.commandCaller.log(message, ...args) 22 | } 23 | 24 | public logToStderr(message?: string, ...args: any[]): void{ 25 | this.commandCaller.logToStderr(message, ...args) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/types/issue.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable perfectionist/sort-object-types */ 2 | export type AddonData = { 3 | name: string, 4 | namespace: string, 5 | version: string 6 | helmChartUrl: string, 7 | helmChartUrlProtocol: string, 8 | containerImagesUrls?: string[], 9 | kubernetesVersion: string[], 10 | customConfiguration?: string[] 11 | }; 12 | 13 | export type ChartAutoCorrection = { 14 | hooks:boolean, 15 | capabilities:boolean 16 | releaseService:boolean 17 | } 18 | 19 | export type IssueData = { 20 | addon: AddonData; 21 | sellerMarketPlaceAlias: string, 22 | chartAutoCorrection: ChartAutoCorrection 23 | }; 24 | 25 | export const AllEksSupportedKubernetesVersions = [ 26 | "1.23", 27 | "1.24", 28 | "1.25", 29 | "1.26", 30 | "1.27", 31 | "1.28", 32 | "1.29", 33 | ] 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=bash 2 | HELM_VERSION="3.8.1" 3 | default: pack 4 | 5 | check: 6 | if [ -d "${HOME}/.nvm/.git" ]; then echo "nvm installed"; else echo "nvm not installed. Install it as instructed here: https://github.com/nvm-sh/nvm#install--update-script"; exit 1; fi 7 | 8 | scrub: 9 | rm -rf ./node_modules 10 | rm -rf aws-sleek-transformer*gz 11 | 12 | setup: 13 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 14 | chmod +x get_helm.sh 15 | ./get_helm.sh -v $(HELM_VERSION) 16 | rm -rf get_helm.sh 17 | NVM_DIR="$(HOME)/.nvm" && . "$(NVM_DIR)/nvm.sh" && nvm install 18 | npm install 19 | 20 | build: 21 | npm run prepack 22 | npm pack 23 | 24 | install: 25 | npm install -g $(shell ls aws-sleek-transformer*gz) 26 | echo "Installed successfully, test by running: 'aws-sleek-transformer' --help " 27 | 28 | publish: 29 | npm publish --access public 30 | 31 | pack: check scrub setup build install -------------------------------------------------------------------------------- /src/services/helm.ts: -------------------------------------------------------------------------------- 1 | import {execSync} from "node:child_process"; 2 | 3 | import {BaseService} from "./base-service.js"; 4 | 5 | export default class HelmManagerService extends BaseService { 6 | public async pullAndUnzipChart(helmUrl: string, helmProtocol: string = "oci", chartTag: string = "", addonName?: string): Promise { 7 | // if addonName is not provided, make it random 8 | addonName ||= `addon-${Math.random().toString(36).slice(7)}`; 9 | 10 | const chartVersionFlag = chartTag ? `--version ${chartTag}`:'' 11 | const untarLocation = `./unzipped-${addonName}`; 12 | const pullCmd = `rm -rf "${untarLocation}" && 13 | mkdir "${untarLocation}" && 14 | helm pull ${helmProtocol}://${helmUrl} ${chartVersionFlag} --untar --untardir "${untarLocation}" >/dev/null`; 15 | try { 16 | execSync(pullCmd); 17 | } catch (error) { 18 | this.error(`Helm chart pull failed with error ${error}`); 19 | } 20 | 21 | return untarLocation; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # check prerequisites: AWS 4 | AWS_CHECK=$(aws sts get-caller-identity > /dev/null ;echo $?) 5 | if [ $AWS_CHECK -eq 0 ]; 6 | then 7 | echo "AWS access confirmed." 8 | else 9 | echo "No AWS access. Install and set up access as instructed here: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" 10 | exit 1; 11 | fi 12 | 13 | # check prerequisites: NVM 14 | if [ -d "$NVM_DIR/.git" ]; 15 | then 16 | echo "nvm installed"; 17 | else 18 | echo "nvm not installed. Install it as instructed here: https://github.com/nvm-sh/nvm#install--update-script"; 19 | exit 1; 20 | fi 21 | 22 | # cleanup of previous runs 23 | rm -rf ./node_modules 24 | rm -rf aws-sleek-transformer*gz 25 | 26 | # install dependencies 27 | HELM_VERSION="v3.8.1" 28 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 29 | chmod +x get_helm.sh 30 | ./get_helm.sh -v $HELM_VERSION 31 | rm -rf get_helm.sh 32 | 33 | . $NVM_DIR/nvm.sh 34 | nvm install 35 | npm install 36 | 37 | # pack 38 | npm run prepack 39 | npm pack 40 | 41 | #install 42 | PCK=$(ls aws-sleek-transformer*gz) 43 | npm install -g $PCK 44 | echo "Installed successfully, test by running: 'aws-sleek-transformer' --help " -------------------------------------------------------------------------------- /doc/demoCommands.md: -------------------------------------------------------------------------------- 1 | # Demo recording commands: 2 | 3 | ## Show help 4 | ```shell 5 | # Show command help 6 | addons-transformer-for-amazon-eks --help 7 | ``` 8 | 9 | ## Local chart validation 10 | 11 | ```shell 12 | # Show command help 13 | addons-transformer-for-amazon-eks validate --help 14 | ``` 15 | 16 | ```shell 17 | # Validate passing repo URI 18 | addons-transformer-for-amazon-eks validate oci://12345678901.dkr.ecr.us-east-2.amazonaws.com/example-charts:x.x.x 19 | ``` 20 | 21 | ```shell 22 | # Validate passing individual components of Repo URL 23 | addons-transformer-for-amazon-eks validate -r 12345678901.dkr.ecr.us-east-2.amazonaws.com/example-charts -p oci -v x.x.x 24 | ``` 25 | 26 | ```shell 27 | # Validate from input file 28 | addons-transformer-for-amazon-eks validate --file ./examples/onboarding.example.yaml 29 | ``` 30 | 31 | ## GitHub Issue creation 32 | 33 | ```shell 34 | # Show command help 35 | addons-transformer-for-amazon-eks create-issue --help 36 | ``` 37 | 38 | ```shell 39 | # Creating issue from input file by JSON schema 40 | addons-transformer-for-amazon-eks create-issue ./examples/onboarding.example.yaml --dry-run 41 | ``` 42 | 43 | ```shell 44 | # Dry-run for validate input-yaml file 45 | addons-transformer-for-amazon-eks create-issue ./examples/onboarding.example.yaml 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /src/commandOpts/create-issue.ts: -------------------------------------------------------------------------------- 1 | import {Args, Flags} from "@oclif/core"; 2 | import ValidateOpt from "./validate.js"; 3 | 4 | export default class CreateIssueOpt { 5 | 6 | static args = { 7 | file: Args.string( 8 | { 9 | description: 'Path to add-on input file', 10 | required: true, 11 | } 12 | ), 13 | } 14 | 15 | static description = ` 16 | This creates a Github Issue on the Sleek repository. 17 | 18 | It will validate the input file to match the schema 19 | ` 20 | 21 | static examples = [ 22 | '<%= config.bin %> <%= command.id %> filename', 23 | ] 24 | 25 | static flags = { 26 | dryRun: Flags.boolean({ 27 | aliases: ['dry-run', 'dryrun'], 28 | char: 'd', 29 | default: false, 30 | description: "Validates the input file schema without creating the issue nor validating the chart", 31 | }), 32 | issueSchemaUrl: ValidateOpt.flags.issueSchemaUrl, 33 | repo: Flags.string({ 34 | default: "aws-eks-addon-publication", 35 | description:"Github repository name where the issue will be created", 36 | hidden:true 37 | }), 38 | repoOwner: Flags.string({ 39 | default: "cloudsoft-fusion", 40 | description:"Github repository owner", 41 | hidden:true 42 | }), 43 | } 44 | 45 | static summary = "Creates a Github Issue based in the input file"; 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/services/create-issue.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable perfectionist/sort-objects */ 2 | import type {OctokitResponse} from "@octokit/types/dist-types/OctokitResponse.js"; 3 | 4 | import {Octokit} from "@octokit/core"; 5 | 6 | import {SleekCommand} from "../sleek-command.js"; 7 | import {ServiceResponse} from "../types/service.js"; 8 | import {BaseService} from "./base-service.js"; 9 | 10 | export default class CreateIssueService extends BaseService { 11 | 12 | private repo:string; 13 | private repoOwner:string; 14 | 15 | constructor(commandCaller: SleekCommand, repoOwner: string, repo: string) { 16 | super(commandCaller) 17 | this.repoOwner = repoOwner; 18 | this.repo = repo; 19 | } 20 | public createIssue = async (title: string, body: string, labels: string[]): Promise>> => this.createIssueOnRepo(this.repo, this.repoOwner, title, body, labels) 21 | 22 | private createIssueOnRepo = async (repo: string, owner: string, title: string, body: string, labels: string[]): Promise>> => { 23 | const octokitOptions = { 24 | auth: process.env.GITHUB_TOKEN, 25 | }; 26 | 27 | const createIssueRequest = { 28 | headers: { 29 | 'X-GitHub-Api-Version': '2022-11-28' 30 | }, 31 | body, 32 | owner, 33 | repo, 34 | title, 35 | labels 36 | }; 37 | 38 | const octokit = new Octokit(octokitOptions) 39 | const octokitResponsePromise = octokit.request('POST /repos/{owner}/{repo}/issues', createIssueRequest); 40 | return octokitResponsePromise 41 | .then((response)=> ({success: true, body: response})) 42 | .catch((error)=>{this.error(`Create issue error: ${error}`)}) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/services/schemaValidation.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable perfectionist/sort-objects */ 2 | import _Ajv from "ajv"; 3 | import * as yaml from "js-yaml"; 4 | 5 | import {IssueData} from "../types/issue.js"; 6 | import {ServiceResponse} from "../types/service.js"; 7 | import {BaseService} from "./base-service.js"; 8 | import {SleekCommand} from "../sleek-command.js"; 9 | 10 | const Ajv = _Ajv as unknown as typeof _Ajv.default; 11 | export default class SchemaValidationService extends BaseService { 12 | private issueSchemaUrl:string 13 | constructor(commandCaller: SleekCommand, issueSchemaUrl: string) { 14 | super(commandCaller) 15 | this.issueSchemaUrl = issueSchemaUrl; 16 | } 17 | public async validateInputFileSchema(fileContents: string): Promise> { 18 | const schema = await fetch(this.issueSchemaUrl, { 19 | headers: { 20 | 'Accept': 'application/json', 21 | 'Content-Type': 'application/json' 22 | }, 23 | mode: 'no-cors' 24 | }) 25 | .then(response => response.json()) 26 | .catch(error => { 27 | this.logToStderr(`Schema url: ${(this.issueSchemaUrl)}`); 28 | this.error('Error fetching the schema', {code: '1'}); 29 | }) 30 | const ajv = new Ajv({allErrors: true}) 31 | const schemaValidator = ajv.compile(schema) 32 | 33 | // const data = yaml.load(fileContents, {schema:schemaJson}) 34 | const data = yaml.load(fileContents) 35 | if (!schemaValidator(data)) { 36 | const allErrors = ['Schema validation errors: ']; 37 | schemaValidator.errors?.map(e => allErrors.push(JSON.stringify(e))); 38 | this.error(allErrors.join('\n'), {exit: 1}); 39 | } 40 | 41 | return {success: true, body: data as IssueData} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/commands/create-issue.test.ts: -------------------------------------------------------------------------------- 1 | import {CLIError} from "@oclif/core/lib/errors"; 2 | import {expect, test} from '@oclif/test' 3 | 4 | describe('create-issue', () => { 5 | 6 | describe('create-issue -d create-issue-input.not-valid.yaml', () => { 7 | test 8 | .stderr() 9 | .stdout() 10 | .command(['create-issue', 11 | '--dry-run', // avoid creation of the actual issue con GitHub 12 | './test/commands/resources/create-issue-input.not-valid.yaml', 13 | ]) 14 | .catch(error => { 15 | expect(error.message) 16 | expect(error.message).to.contain('Schema validation errors') 17 | expect(error.message).to.contain('must have required property \'sellerName\'') 18 | expect(error.message).to.contain('must have required property \'sellerMarketPlaceAlias\'') 19 | expect(error.message).to.not.contain('must have required property \'accountId\'') 20 | if (error instanceof CLIError) 21 | expect(error.oclif.exit).to.eq(1) 22 | }) 23 | .it('Didn\'t try to create the issue due to validation errors',error=>{ 24 | console.log(JSON.stringify(error)) 25 | // expect(ctx.stdout).to.contain('must have required property') 26 | }) 27 | }); 28 | 29 | describe('create-issue -d create-issue-input.valid.yaml', () => { 30 | test 31 | .stdout() 32 | .command(['create-issue', 33 | '--dry-run', // avoid creation of the actual issue con GitHub 34 | './test/commands/resources/create-issue-input.valid.yaml', 35 | ] 36 | ).it('create-issue with valid input', ctx => { 37 | expect(ctx.stdout).to.eq('File to process: ./test/commands/resources/create-issue-input.valid.yaml (dry run)\n' + 38 | 'Schema validation correct\n' 39 | ) 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/commands/resources/create-issue-input.valid.yaml: -------------------------------------------------------------------------------- 1 | sellerName: test 2 | sellerMarketPlaceAlias: cloufsoft-developer 3 | accountId: "012345678901" 4 | productName: myapp 5 | productCategory: networking 6 | productUrl: https://test.io/software 7 | addon: 8 | name: myapp 9 | versionName: test myapp 0.1.0 mp1 10 | version: 1.0.0 11 | namespace: test-myapp 12 | type: networking 13 | helmChartUrl: 123456789012.dkr.ecr.us-east-1.amazonaws.com/test-development/test-myapp-helm:0.1.6 14 | containerImagesUrls: 15 | - 123456789012.dkr.ecr.us-east-1.amazonaws.com/test-development/test-myapp:0.4.2 16 | prerequisites: > 17 | Follow instructions in: https://docs.test.io/ 18 | usageInstructions: > 19 | deploy it and enjoy 20 | kubernetesVersion: 21 | - "1.26" 22 | - "1.27" 23 | - "1.28" 24 | architectures: 25 | - arm64 26 | - amd64 27 | environmentOverride: 28 | param1: value1 29 | param2: value2 30 | customConfiguration: 31 | - myappResourceLimits.requests.cpu 32 | - myappResourceLimits.requests.memory 33 | - myappResourceLimits.limits.cpu 34 | - myappResourceLimits.limits.memory 35 | serviceAccounts: 36 | serviceAccountOne: 37 | iamManagedPolicies: 38 | - arn:aws:iam::aws:policy/AWSMarketplaceMeteringRegisterUsage 39 | iamInlinePolicies: 40 | myappS3: > 41 | { 42 | "Version": "2012-10-17", 43 | "Statement": [ 44 | { 45 | "Sid": "myappPersistence", 46 | "Action": [ 47 | "s3:GetObject", 48 | "s3:PutObject" 49 | ], 50 | "Effect": "Allow", 51 | "Resource": [ 52 | "arn:aws:s3:::test-myapp-*" 53 | ] 54 | } 55 | ] 56 | } 57 | serviceAccountTwo: 58 | iamManagedPolicies: 59 | - arn:aws:iam::aws:policy/AWSMarketplaceMeteringRegisterUsage 60 | -------------------------------------------------------------------------------- /doc/userJourney.md: -------------------------------------------------------------------------------- 1 | # EKS add-on onboarding user journey 2 | 3 | ## Personas: 4 | * Vendor: ISV, AWS partner, marketplace seller of the product 5 | * Operator: AWS or Cloudsoft engineer supporting the onboarding 6 | 7 | ## Requirements: 8 | * `aws-sleek-transformer` cli installed on vendor environment 9 | * Vendor GitHub handle added to the repository 10 | * Vendor access to the helm repository 11 | * Helm chart and images shared with the operator 12 | 13 | ## Workflow 14 | ![flow-diagram.jpg](img/flow-diagram.jpg) 15 | 16 | ## Guide 17 | 1. Install the aws-sleek-transformer cli following the instructions in the tool repo: https://github.com/aws-samples/addons-transformer-for-amazon-eks 18 | 2. Export a GitHub access token with 'repo' permissions for the account shared with the operator team. Token can be created at https://github.com/settings/tokens 19 | For exporting the variable run in the terminal: 20 | ```shell 21 | export GITHUB_TOKEN=ghp_000000000000000000000000000 22 | ``` 23 | 24 | 3. Create a yaml document following the [example (TODO: update url once merged)](https://github.com/aws-samples/addons-transformer-for-amazon-eks/blob/dev/doc/examples/onboarding.example.yaml). 25 | The yaml document must match the [JSON schema (TODO: update url once merged)](https://github.com/aws-samples/addons-transformer-for-amazon-eks/blob/dev/schema/onboarding.schema.json) . 26 | In order to help the vendor with the creation of a valid yaml document and a valid chart, the cli exposes a validate command that runs the same validations locally but it doesn’t try to create an issue. Check the tool readme for instructions. 27 | 28 | 4. Running the following command 29 | ```shell 30 | aws-sleek-transformer create-issue add-on.yaml 31 | ``` 32 | pulls the chart from the helm repo and validates it. If no issues are found in the static analysis, a new issue will be opened in the onboarding GitHub repository to progress with extra validation and testing. 33 | The onboarding process supports the automatic removal from the charts of the features not supported by EKS. The option has to be enabled explicitly, by setting the chartAutoCorrection property to true in the yaml document. The onboarding process outputs will be available on the pull request. 34 | 35 | 5. The vendor can see the progress in the issue and the other GitHub artefacts: action execution and pull request created 36 | 37 | 6. Once all tests are correct, the generated assets will be added to the internal pull request and merged in the onboarding repo making them available to the vendor 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Elamaran Shanmugam @elamaras", 3 | "license": "MIT", 4 | "name": "addons-transformer-for-amazon-eks", 5 | "description": "Addons Transformer for Amazon EKS is solution that provides pre-launch validations of the partner software on compatibility with Amazon EKS Third Party Addon guidelines, covering static and dynamic (deployment/runtime) aspects.", 6 | "homepage": "https://github.com/aws-samples/addons-transformer-for-amazon-eks.git", 7 | "bin": "./bin/run.js", 8 | "dependencies": { 9 | "@inquirer/prompts": "^3.3.0", 10 | "@inquirer/select": "^1.3.1", 11 | "@oclif/core": "^3", 12 | "@oclif/plugin-help": "^5", 13 | "@octokit/core": "^5.0.2", 14 | "ajv": "^8.12.0", 15 | "debug": "^4.3.6", 16 | "js-yaml": "^4.1.0", 17 | "json-schema": "^0.4.0", 18 | "oclif": "^4.0.3", 19 | "shx": "^0.3.4" 20 | }, 21 | "devDependencies": { 22 | "@oclif/prettier-config": "^0.2.1", 23 | "@oclif/test": "^3", 24 | "@types/chai": "^4", 25 | "@types/fs-extra": "^11.0.4", 26 | "@types/inquirer": "^9.0.7", 27 | "@types/js-yaml": "^4.0.9", 28 | "@types/mocha": "^10", 29 | "@types/node": "^18", 30 | "chai": "^4", 31 | "eslint": "^8", 32 | "eslint-config-oclif": "^5", 33 | "eslint-config-oclif-typescript": "^3", 34 | "eslint-config-prettier": "^9.0.0", 35 | "mocha": "^10", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^5" 38 | }, 39 | "engines": { 40 | "node": ">=18.0.0" 41 | }, 42 | "files": [ 43 | "/bin", 44 | "/dist", 45 | "/oclif.manifest.json" 46 | ], 47 | "oclif": { 48 | "bin": "addons-transformer-for-amazon-eks", 49 | "dirname": "addons-transformer-for-amazon-eks", 50 | "commands": "./dist/commands", 51 | "topicSeparator": " ", 52 | "macos": { 53 | "identifier": "com.aws.addons-transformer-for-amazon-eks.cli" 54 | } 55 | }, 56 | "repository": "aws-samples/addons-transformer-for-amazon-eks", 57 | "scripts": { 58 | "build": "shx rm -rf dist && tsc -b", 59 | "lint": "eslint . --ext .ts", 60 | "postpack": "shx rm -f oclif.manifest.json", 61 | "posttest": "npm run lint", 62 | "prepack": "npm run build && oclif manifest && oclif readme", 63 | "prepare": "npm run build", 64 | "version": "oclif readme && git add README.md" 65 | }, 66 | "version": "1.1.0", 67 | "bugs": "https://github.com/aws-samples/addons-transformer-for-amazon-eks/issues", 68 | "keywords": [ 69 | "oclif" 70 | ], 71 | "types": "dist/index.d.ts", 72 | "exports": "./lib/index.js", 73 | "type": "module" 74 | } 75 | -------------------------------------------------------------------------------- /doc/contributing.md: -------------------------------------------------------------------------------- 1 | # Open Source Contribution Guide 2 | Welcome potential contributors! This document provides information and guidelines for contributing to this open 3 | source project. 4 | 5 | ### Project Overview 6 | The Sleek transformer is driven to assist partners quickly onboard their helm charts onto the EKS addon 7 | environment (known as Sleek). This system attempts to cover pre-validation checks we conduct before allowing partner 8 | addons into the AWS Console. 9 | 10 | The goal is to speed up the iterative process for onboarding partners into the EKS addons environment, increasing the 11 | throughput of partner addons by letting them validate against our checks before they engage with AWS Engineering. 12 | 13 | ### Getting Started 14 | To add a new command to this CLI, simply add a new file in `./src/commands/` that extends from `SleekCommand`. 15 | Our standard best practice is to separate the static aspects used to generate the OCLIF docs into a separate folder such 16 | as `./src/commandOpts` just to make it easier to read the functional vs. descriptive aspects of code. 17 | 18 | This entire CLI is heavily I/O bound, so please try and use Async/Await at appropriate locations. We separate the interface 19 | and logic aspects of the CLI by creating services that are called from the UI classes: 20 | * `./src/commands/` - contains all the interface files 21 | * `./src/services/` - contains all the services and logic 22 | * `./src/commandOpts/` - contains all the interface static properties (used to generate OCLIF documentation) 23 | 24 | ### Command Management 25 | This project uses OCLIF (Open CLI Framework) to manage commands. OCLIF provides a simple yet powerful framework 26 | for building CLI applications in Node.js. With OCLIF, commands are defined as classes that extend the base 27 | Command class. This allows commands to have common properties like name, description, args and flags. 28 | 29 | The commands are then registered with OCLIF via the commands property on the program class. OCLIF takes 30 | care of parsing args, initializing the command class, and executing the command. This makes it easy to add, remove, 31 | and manage commands without having to rewrite the CLI parser. 32 | 33 | ### Contribution Workflow 34 | 35 | 1. Fork the repo. 36 | 2. Make modifications to code/documentation that are relevant to your contribution 37 | 3. Ensure the CLI compiles and is still usable: 38 | 1. Run `npm run prepack` in the context of the folder to ensure everything in the build and docs is up to date 39 | 2. Run `npm pack` to package your modifications 40 | 3. Install the modifications to your local environment using `npm i ` 41 | 4. Validate that the modifications you made pass runtime verification. 42 | 4. Submit a PR against this repo with your modifications 43 | 5. If there's feedback against the PR, address it. 44 | 6. Once the maintainers think it's appropriate to merge, they will merge it. 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /src/commandOpts/validate.ts: -------------------------------------------------------------------------------- 1 | import {Args, Flags} from "@oclif/core"; 2 | 3 | export default class ValidateOpt { 4 | static description = ` 5 | This performs pre-launch validations of the partner software on compatibility with Sleek guidelines, covering static 6 | and dynamic (deployment/runtime) aspects. 7 | 8 | Runs the static analysis to find occurrences of: 9 | * .Capabilities 10 | * helm.sh/hook 11 | * external helm dependencies 12 | 13 | It will perform a static validation on the device and then give you the option to submit it to the marketplace for 14 | runtime and further validation before it can be included in the EKS Console marketplace. 15 | 16 | The command can accept two different formats of inputs: 17 | * Fully qualified Helm URL to download 18 | * Deconstructed URL that requires Protocol, Repo, and Version to pull 19 | ` 20 | 21 | static examples = [ 22 | '<%= config.bin %> <%= command.id %> oci://12345678901.dkr.ecr.us-east-2.amazonaws.com/example-charts:x.x.x', 23 | '<%= config.bin %> <%= command.id %> -r 12345678901.dkr.ecr.us-east-2.amazonaws.com/example-charts -p oci -v x.x.x', 24 | '<%= config.bin %> <%= command.id %> -f ./input.yaml', 25 | '<%= config.bin %> <%= command.id %> -d ./addon-folder', 26 | '<%= config.bin %> <%= command.id %> --help', 27 | ] 28 | 29 | static args = { 30 | helmUrl: Args.string( 31 | { 32 | required: false, 33 | description: "Fully qualified Helm URL of the addon" 34 | } 35 | ), 36 | } 37 | 38 | static flags = { 39 | // todo check Flags.url type 40 | file: Flags.string({ 41 | description: "Path to add-on input file", 42 | exclusive: ['helmUrl'], char: 'f' 43 | }), // or file or URL, full or bits 44 | directory: Flags.string({ 45 | description: "Path to the local addon folder", 46 | char: 'd', 47 | exclusive: ['file', 'helmUrl'] 48 | }), 49 | helmUrl: Flags.string({ 50 | description: "Fully qualified URL of the Repo including version tag", 51 | exclusive: ['file'] 52 | }), // fully qualified URL of helm repo 53 | helmRepo: Flags.string({ 54 | description: "URL of the helm repo containing protocol and repo", 55 | exclusive: ['file', 'helmUrl'], 56 | char: 'r' 57 | }), // construct it piecemeal 58 | protocol: Flags.string({ 59 | description: "Protocol of the helm hosting to use", 60 | exclusive: ['file', 'helmUrl'], 61 | char: 'p' 62 | }), 63 | version: Flags.string({ 64 | description: "Version of the addon to validate", 65 | exclusive: ['file'], 66 | char: 'v' 67 | }), 68 | addonName: Flags.string({ 69 | description: "Name of the addon" 70 | }), 71 | addonNamespace: Flags.string({ 72 | description: "Add-on namespace", 73 | char: 'n' 74 | }), 75 | k8sVersions: Flags.string({ 76 | description: "Comma separated list of supported kubernetes versions" 77 | }), 78 | skipHooks: Flags.boolean({ 79 | description: "Skip helm hooks validation", 80 | default: false 81 | }), 82 | skipReleaseService: Flags.boolean({ 83 | description: "Skip .Release.Service occurrences", 84 | default: false 85 | }), 86 | issueSchemaUrl: Flags.string({ 87 | default: "https://raw.githubusercontent.com/aws-samples/addons-transformer-for-amazon-eks/main/schema/onboarding.schema.json", 88 | description: "URL for the schema used for issue input file", 89 | hidden: true 90 | }), 91 | } 92 | 93 | static summary = "Validates the addon after pulling it from the helm repository."; 94 | } 95 | -------------------------------------------------------------------------------- /src/commands/create-issue.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import CreateIssueOpt from "../commandOpts/create-issue.js"; 4 | import CreateIssueService from "../services/create-issue.js"; 5 | import HelmManagerService from "../services/helm.js"; 6 | import SchemaValidationService from "../services/schemaValidation.js"; 7 | import ChartValidatorService from "../services/validate.js"; 8 | import {SleekCommand} from "../sleek-command.js"; 9 | import {IssueData} from "../types/issue.js"; 10 | import {ValidateOptions} from "../types/validate.js"; 11 | import {getChartNameFromUrl} from "../utils.js"; 12 | 13 | 14 | export class CreateIssue extends SleekCommand { 15 | static args = CreateIssueOpt.args; 16 | static description = CreateIssueOpt.description; 17 | static examples = CreateIssueOpt.examples; 18 | static flags = CreateIssueOpt.flags; 19 | static summary = CreateIssueOpt.summary; 20 | 21 | async run(): Promise { 22 | const {args, flags} = await this.parse(CreateIssue); 23 | const isDryRun = flags.dryRun; 24 | const filePath = args.file; 25 | 26 | this.log(`File to process: ${filePath} ${isDryRun ? '(dry run)' : ''}`) 27 | const fileContents = fs.readFileSync(filePath, 'utf8'); 28 | const schemaValidator = new SchemaValidationService(this, flags.issueSchemaUrl); 29 | const data = await schemaValidator.validateInputFileSchema(fileContents); 30 | this.log('Schema validation correct') // it exits if not valid 31 | 32 | if (isDryRun) return; 33 | 34 | const inputDataParsed = data.body as IssueData; 35 | const addonData = inputDataParsed.addon; 36 | const repo= addonData.helmChartUrl.slice(0,Math.max(0, addonData.helmChartUrl.lastIndexOf(':'))) 37 | const chartTag = addonData.helmChartUrl.lastIndexOf(':') ? `${addonData.helmChartUrl.slice(Math.max(0, addonData.helmChartUrl.lastIndexOf(':')+1))}` : '' 38 | const chartName = getChartNameFromUrl(repo); 39 | const helmManager = new HelmManagerService(this); 40 | const charPath= `${await helmManager.pullAndUnzipChart(repo!, addonData.helmChartUrlProtocol!, chartTag!, addonData.name)}/${chartName}`; 41 | const validatorService = new ChartValidatorService(this, charPath, addonData); 42 | const validateOps: ValidateOptions ={ 43 | skipHooksValidation: inputDataParsed.chartAutoCorrection?.hooks 44 | } 45 | const validatorServiceResp = await validatorService.validate(validateOps); 46 | // todo: if validatorService exits when errors, not need to handle here !success 47 | this.log(validatorServiceResp.body); 48 | if(!validatorServiceResp.success){ 49 | this.error(validatorServiceResp.error?.input!, validatorServiceResp.error?.options ) 50 | } 51 | 52 | this.log(`Chart validation successful`) 53 | 54 | // create issue base in the file input 55 | const title = `Onboarding ${inputDataParsed.sellerMarketPlaceAlias} ${addonData.name}@${addonData.version}`; 56 | // the yaml contend is captured from the issue body when running the pipelines by searching the ```yaml and ``` markers 57 | const body = `Issue body:\n\n\`\`\`yaml\n${fileContents}\`\`\`\n`; 58 | const createIssueService = new CreateIssueService(this, flags.repoOwner, flags.repo); 59 | 60 | // Add label 'DEV_MODE' for forcing pull aws-sleek-transformer from the repo instead of the npm Registry 61 | const createIssueResponse = await createIssueService.createIssue(title, body, ['pending']) 62 | 63 | this.log(`Issue created: ${createIssueResponse.body?.data.html_url}`) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /doc/examples/onboarding.example.yaml: -------------------------------------------------------------------------------- 1 | # Seller and product related information 2 | ## Seller commercial name 3 | sellerName: Example 4 | ## Seller alias in the AWS marketplace 5 | sellerMarketPlaceAlias: example-software 6 | ## Seller AWS account ID 7 | accountId: 123456789012 8 | ## Product name 9 | productName: Example Solution 10 | ## Product category 11 | productCategory: networking 12 | ## Product URL 13 | productUrl: https://example.io/solution 14 | ## Marketplace product ID 15 | marketplaceProductId: prod-exa12thsdnghk 16 | ## Marketplace product URL 17 | marketplaceProductUrl: https://aws.amazon.com/marketplace/pp/prodview-1abcd2najvj5y 18 | 19 | ## Opt-in autocorrection. All of them default to false 20 | chartAutoCorrection: 21 | ## Opt-in removing hooks references from the templates and repackage the chart 22 | hooks: true 23 | ## Opt-in removing hooks unsupported capabilities from the templates and repackage the chart 24 | capabilities: true 25 | ## Opt-in replacing references to .Release.Service with 'eks' from the templates and repackage the chart 26 | releaseService: false 27 | 28 | # Add-on information 29 | addon: 30 | ## Add-on name 31 | name: solution 32 | ## Add-on version name 33 | versionName: Example Software Solution 1 34 | ## Add-on version e.g. 0.1.0 35 | version: 1.0.0 36 | ## Add-on namespace 37 | namespace: ex-sol 38 | ## Add-on type 39 | type: networking 40 | ## Add-on helm chart URL 41 | helmChartUrl: 123456789012.dkr.ecr.us-east-1.amazonaws.com/example-software/solution-helm:0.1.0 42 | ## Add-on helm chart URL protocol 43 | helmChartUrlProtocol: oci 44 | ## Add-on container images URLs 45 | containerImagesUrls: 46 | - 123456789012.dkr.ecr.us-east-1.amazonaws.com/example-software/solution:0.1.0 47 | ## Add-on deployment prerequisites 48 | prerequisites: > 49 | Follow instructions in: https://docs.example.com/installation/prerequisites 50 | ## Add-on deployment instructions 51 | usageInstructions: > 52 | Follow instructions in: https://docs.example.com/instructions 53 | ## The description will be shown in the product details page and when the buyer is configuring or launching the product 54 | deliveryOptionDescription: > 55 | EKS add-on installation 56 | ## List of add-on supported kubernetes versions 57 | kubernetesVersion: 58 | - 1.26 59 | - 1.27 60 | - 1.28 61 | ## Add-on containers supported architectures 62 | architectures: 63 | - arm64 64 | - amd64 65 | ## Parameters that will be used while installing this add-on on a EKS cluster 66 | environmentOverride: 67 | param1: value1 68 | param2: value2 69 | ## List of expanded values keys to be allowed to be modified when deploying the add-on 70 | customConfiguration: 71 | - resourceLimits.requests.cpu 72 | - resourceLimits.requests.memory 73 | - resourceLimits.limits.cpu 74 | - resourceLimits.limits.memory 75 | ## Map of parameters supported for inject secrets and the keys expected to be found in. DO NOT INCLUDE SECRET VALUES 76 | secretMapping: 77 | ### `exampleSecret` is the name of the parameter expected on the helm chart for the pre-created secret 78 | exampleSecret: 79 | ### `secretKeyOne` and `secretKeyOne` are the name of the keys to be resolved from AWS Secret Manager and added to the kubernetes `exampleWithSecretKeys` secret 80 | - secretKeyOne 81 | - secretKeyTwo 82 | ## Service accounts - IAM policies mapping 83 | serviceAccounts: 84 | serviceAccountOne: 85 | iamManagedPolicies: 86 | - arn:aws:iam::aws:policy/AWSMarketplaceMeteringRegisterUsage 87 | iamInlinePolicies: 88 | persistenceS3: > 89 | { 90 | "Version": "2012-10-17", 91 | "Statement": [ 92 | { 93 | "Sid": "SolutionPersistence", 94 | "Action": [ 95 | "s3:GetObject", 96 | "s3:PutObject" 97 | ], 98 | "Effect": "Allow", 99 | "Resource": [ 100 | "arn:aws:s3:::example-solution-*" 101 | ] 102 | } 103 | ] 104 | } 105 | serviceAccountTwo: 106 | iamManagedPolicies: 107 | - arn:aws:iam::aws:policy/CloudWatchLogsReadOnlyAccess 108 | -------------------------------------------------------------------------------- /src/commands/validate.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import ValidateOpt from "../commandOpts/validate.js"; 4 | import HelmManagerService from "../services/helm.js"; 5 | import SchemaValidationService from "../services/schemaValidation.js"; 6 | import ChartValidatorService from "../services/validate.js"; 7 | import {SleekCommand} from "../sleek-command.js"; 8 | import {AddonData, AllEksSupportedKubernetesVersions, IssueData} from "../types/issue.js"; 9 | import { 10 | getChartNameFromUrl, 11 | getProtocolFromFullQualifiedUrl, 12 | getRepoFromFullChartUri, 13 | getVersionTagFromChartUri 14 | } from "../utils.js"; 15 | 16 | export default class Validate extends SleekCommand { 17 | static args = ValidateOpt.args; 18 | static description = ValidateOpt.description; 19 | static examples = ValidateOpt.examples; 20 | static flags = ValidateOpt.flags; 21 | static summary = ValidateOpt.summary; 22 | 23 | // eslint-disable-next-line complexity 24 | public async run(): Promise { 25 | const {args, flags} = await this.parse(Validate); 26 | 27 | // if helmURL is given, download the chart then validate 28 | // if file is given, validate based on the path 29 | // else, raise error stating one or the other arg/flag should be provided 30 | let repoProtocol; let repoUrl; let chartName; let versionTag; let addonName; 31 | let skipHooksValidation = flags.skipHooks; 32 | // eslint-disable-next-line prefer-destructuring 33 | let skipReleaseService = flags.skipReleaseService; 34 | let addonData: AddonData | undefined; 35 | 36 | // uncomment for debugging purposes 37 | // this.log('---') 38 | // this.log(`>> args: ${JSON.stringify(args)}`) 39 | // this.log(`>> flags: ${JSON.stringify(flags)}`) 40 | // this.log('---') 41 | if (flags.addonName) { 42 | addonName = flags.addonName; 43 | } 44 | 45 | if (args.helmUrl || flags.helmUrl) { 46 | // JD decompose url, pull and validate 47 | const repoUrlInput = args.helmUrl || flags.helmUrl; 48 | 49 | this.log(`Validating chart from url: ${repoUrlInput}`) 50 | repoProtocol = getProtocolFromFullQualifiedUrl(repoUrlInput!); 51 | repoUrl = getRepoFromFullChartUri(repoUrlInput!).slice(Math.max(0, repoProtocol.length + 3)); // 3 == '://'.length 52 | 53 | chartName = getChartNameFromUrl(repoUrl); 54 | versionTag = getVersionTagFromChartUri(repoUrlInput!); 55 | } else if ( 56 | // eslint-disable-next-line no-dupe-else-if 57 | (args.helmUrl || flags.helmUrl) && (flags.helmRepo || flags.protocol || flags.version) // base url + flags to override // todo 58 | ) { 59 | const repoUrlInput = args.helmUrl || flags.helmUrl; 60 | 61 | repoProtocol = flags.protocol || getProtocolFromFullQualifiedUrl(repoUrlInput!); 62 | repoUrl = flags.helmRepo || getRepoFromFullChartUri(repoUrlInput!).slice(Math.max(0, repoProtocol.length + 3)); 63 | versionTag = flags.version || getVersionTagFromChartUri(repoUrlInput!); 64 | } else if ( 65 | flags.helmRepo && flags.protocol && flags.version // all the url bits 66 | ) { 67 | repoProtocol = flags.protocol; 68 | repoUrl = flags.helmRepo; 69 | chartName = getChartNameFromUrl(repoUrl!); 70 | versionTag = flags.version; 71 | this.log(`Validating chart from flags: ${repoProtocol}://${repoUrl}:${versionTag}`) 72 | } else if (flags.file) { 73 | const filePath = flags.file; 74 | this.log(`Validating chart from input file ${filePath}`) 75 | // schema validation 76 | const fileContents = fs.readFileSync(filePath, 'utf8'); 77 | const schemaValidator = new SchemaValidationService(this, flags.issueSchemaUrl); 78 | const data = await schemaValidator.validateInputFileSchema(fileContents); 79 | // get url 80 | const inputDataParsed = data.body as IssueData; 81 | addonData = inputDataParsed.addon; 82 | repoProtocol = addonData.helmChartUrlProtocol; 83 | repoUrl = getRepoFromFullChartUri(addonData.helmChartUrl); 84 | chartName = getChartNameFromUrl(repoUrl); 85 | versionTag = getVersionTagFromChartUri(addonData.helmChartUrl); 86 | addonName = inputDataParsed.addon.name; 87 | skipHooksValidation = inputDataParsed.chartAutoCorrection?.hooks; 88 | skipReleaseService = inputDataParsed.chartAutoCorrection?.releaseService; 89 | } else if (flags.directory) { 90 | this.log(`Validating chart from input directory ${flags.directory}`) 91 | } else { 92 | this.error("Parameters not valid. Please run 'validate --help' for see valid options"); 93 | } 94 | 95 | // verify that the things are populated 96 | if (!flags.directory) { 97 | let errorMessage = ''; 98 | if (!repoProtocol) { 99 | errorMessage = `${errorMessage} protocol is required`; 100 | } 101 | 102 | if (!repoUrl) { 103 | errorMessage = `${errorMessage} repo is required`; 104 | } 105 | 106 | if (!chartName) { 107 | errorMessage = `${errorMessage} Chart name is required`; 108 | } 109 | 110 | if (!versionTag) { 111 | errorMessage = `${errorMessage} version tag is required`; 112 | } 113 | 114 | if (errorMessage !== '') { 115 | this.error(`Parameters are not valid: ${errorMessage}`); 116 | } 117 | } 118 | 119 | 120 | const helmManager = new HelmManagerService(this); 121 | const chartPath = flags.directory ?? `${await helmManager.pullAndUnzipChart(repoUrl!, repoProtocol!, versionTag!, addonName)}/${chartName}`; 122 | 123 | // addonData is initialized when reading from the input yaml; when using flags the parameters are inferred 124 | addonData ||= { 125 | helmChartUrl: flags.directory ? 'local-testing' : `${repoProtocol}://${repoUrl}:${versionTag}`, 126 | helmChartUrlProtocol: flags.directory ? 'local-testing' : repoProtocol!, 127 | kubernetesVersion: flags.k8sVersions?.split(',') || AllEksSupportedKubernetesVersions, 128 | name: addonName!, 129 | namespace: flags.addonNamespace || 'test-namespace', 130 | version: versionTag! 131 | }; 132 | 133 | 134 | const validatorService = new ChartValidatorService(this, chartPath, addonData!); 135 | const validatorServiceResp = await validatorService.validate({skipHooksValidation, skipReleaseService}); 136 | 137 | if (validatorServiceResp === undefined) { 138 | this.error('Error validating service'); 139 | } else if (validatorServiceResp.success) { 140 | this.log(validatorServiceResp.body); 141 | this.log('Validation successful'); 142 | } else if (validatorServiceResp.error !== undefined) { 143 | this.error(validatorServiceResp.error?.input, validatorServiceResp.error?.options) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /schema/onboarding.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "EKS add-on workflow - Issue creation", 4 | "type": "object", 5 | "definitions": { 6 | "basicSemver": { 7 | "type": "string", 8 | "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" 9 | }, 10 | "serviceAccount": { 11 | "type": "object", 12 | "additionalProperties": false, 13 | "patternProperties": { 14 | "^[A-Za-z0-9.-]+$": { 15 | "type": "object", 16 | "properties": { 17 | "iamManagedPolicies": { 18 | "type": "array", 19 | "items": { 20 | "type": "string" 21 | } 22 | }, 23 | "iamInlinePolicies": { 24 | "type": "object", 25 | "additionalProperties": { 26 | "type": "string" 27 | }, 28 | "patternProperties": { 29 | "^[a-zA-Z0-9_+=,.@-]+$": { 30 | "type": "string" 31 | } 32 | } 33 | } 34 | }, 35 | "additionalProperties": false 36 | } 37 | } 38 | }, 39 | "chartAutoCorrection": { 40 | "type": "object", 41 | "title": "Opt-in autocorrection", 42 | "additionalProperties": false, 43 | "properties": { 44 | "hooks": { 45 | "title": "Opt-in removing hooks references from the templates and repackage the chart", 46 | "type": "boolean", 47 | "default": false 48 | }, 49 | "capabilities": { 50 | "title": "Opt-in removing hooks unsupported capabilities from the templates and repackage the chart", 51 | "type": "boolean", 52 | "default": false 53 | }, 54 | "releaseService": { 55 | "title": "Opt-in replacing references to .Release.Service with 'eks' from the templates and repackage the chart", 56 | "type": "boolean", 57 | "default": false 58 | } 59 | } 60 | }, 61 | "addon": { 62 | "type": "object", 63 | "additionalProperties": false, 64 | "properties": { 65 | "name": { 66 | "title": "Add-on name", 67 | "type": "string", 68 | "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$", 69 | "maxLength": 53 70 | }, 71 | "versionName": { 72 | "title": "Add-on version name", 73 | "type": "string" 74 | }, 75 | "version": { 76 | "title": "Add-on version e.g. 0.1.0", 77 | "$ref": "#/definitions/basicSemver" 78 | }, 79 | "namespace": { 80 | "title": "Add-on namespace", 81 | "type": "string", 82 | "maxLength": 64, 83 | "minLength": 1 84 | }, 85 | "type": { 86 | "title": "Add-on type", 87 | "type": "string", 88 | "enum": [ 89 | "gitops", 90 | "monitoring", 91 | "logging", 92 | "cert-management", 93 | "policy-management", 94 | "cost-management", 95 | "autoscaling", 96 | "storage", 97 | "kubernetes-management", 98 | "service-mesh", 99 | "etcd-backup", 100 | "ingress-service-type", 101 | "load-balancer", 102 | "local-registry", 103 | "networking", 104 | "security", 105 | "backup", 106 | "ingress-controller", 107 | "observability" 108 | ] 109 | }, 110 | "helmChartUrl": { 111 | "title": "Add-on helm chart URL", 112 | "type": "string" 113 | }, 114 | "helmChartUrlProtocol": { 115 | "title": "Add-on helm chart URL protocol", 116 | "enum": [ 117 | "oci", 118 | "https" 119 | ] 120 | }, 121 | "containerImagesUrls": { 122 | "title": "Add-on container images URLs", 123 | "type": "array", 124 | "items": { 125 | "type": "string" 126 | } 127 | }, 128 | "prerequisites": { 129 | "title": "Add-on deployment prerequisites", 130 | "type": "string" 131 | }, 132 | "deliveryOptionDescription": { 133 | "title": "Delivery option description", 134 | "type": "string", 135 | "maxLength": 4000 136 | }, 137 | "usageInstructions": { 138 | "title": "Add-on deployment instructions", 139 | "type": "string" 140 | }, 141 | "kubernetesVersion": { 142 | "title": "List of add-on supported kubernetes versions", 143 | "type": "array", 144 | "items": { 145 | "enum": [ 146 | "1.23", 147 | "1.24", 148 | "1.25", 149 | "1.26", 150 | "1.27", 151 | "1.28", 152 | "1.29", 153 | "1.30" 154 | ] 155 | } 156 | }, 157 | "architectures": { 158 | "title": "Add-on containers supported architectures", 159 | "type": "array", 160 | "items": { 161 | "type": "string", 162 | "enum": [ 163 | "arm64", 164 | "amd64" 165 | ] 166 | } 167 | }, 168 | "environmentOverride": { 169 | "title": "Parameters that will be used while installing this add-on on a EKS cluster", 170 | "type": "object" 171 | }, 172 | "customConfiguration": { 173 | "title": "List of expanded values keys to be allowed to be modified when deploying the add-on", 174 | "type": "array", 175 | "items": { 176 | "type": "string" 177 | } 178 | }, 179 | "secretMapping": { 180 | "title": "Map of parameters supported for inject secrets and the keys expected to be found in. DO NOT INCLUDE SECRET VALUES", 181 | "type": "object", 182 | "additionalProperties": { 183 | "type": "array", 184 | "items": { 185 | "type": "string" 186 | } 187 | } 188 | }, 189 | "serviceAccounts": { 190 | "title": "Service accounts - IAM policies mapping", 191 | "$ref": "#/definitions/serviceAccount" 192 | } 193 | }, 194 | "required": [ 195 | "name", 196 | "versionName", 197 | "type", 198 | "helmChartUrl", 199 | "containerImagesUrls", 200 | "namespace", 201 | "prerequisites", 202 | "usageInstructions", 203 | "kubernetesVersion", 204 | "architectures" 205 | ] 206 | } 207 | }, 208 | "properties": { 209 | "additionalProperties": false, 210 | "sellerName": { 211 | "title": "Seller commercial name", 212 | "type": "string" 213 | }, 214 | "sellerMarketPlaceAlias": { 215 | "title": "Seller alias in the AWS marketplace", 216 | "type": "string" 217 | }, 218 | "accountId": { 219 | "title": "Seller AWS account ID", 220 | "type": "number", 221 | "minimum": 100000000000, 222 | "maximum": 999999999999 223 | }, 224 | "productName": { 225 | "title": "Product name", 226 | "type": "string" 227 | }, 228 | "productUrl": { 229 | "title": "Product URL", 230 | "type": "string" 231 | }, 232 | "marketplaceProductId": { 233 | "title": "Marketplace product ID", 234 | "type": "string" 235 | }, 236 | "marketplaceProductUrl": { 237 | "title": "Marketplace product URL", 238 | "type": "string" 239 | }, 240 | "productCategory": { 241 | "title": "Product category", 242 | "type": "string" 243 | }, 244 | "chartAutoCorrection": { 245 | "title": "Chart corrections to apply automatically", 246 | "$ref": "#/definitions/chartAutoCorrection" 247 | }, 248 | "addon": { 249 | "title": "Add-on properties", 250 | "$ref": "#/definitions/addon" 251 | } 252 | }, 253 | "required": [ 254 | "sellerName", 255 | "sellerMarketPlaceAlias", 256 | "accountId", 257 | "productName", 258 | "marketplaceProductId", 259 | "marketplaceProductUrl", 260 | "productCategory", 261 | "addon" 262 | ] 263 | } 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Addons Transformer CLI for Amazon EKS 2 | ===================================== 3 | 4 | * [Introduction](#introduction) 5 | * [Pre-requisites](#pre-requisites) 6 | * [Features](#features) 7 | * [Installation](#installation) 8 | * [Commands](#commands) 9 | 10 | ## Introduction 11 | 12 | Addons Transformer for Amazon EKS is a solution that provides pre-launch validations of the partner software based on 13 | compatibility with [Amazon EKS Third Party Addon guidelines](https://docs.aws.amazon.com/marketplace/latest/userguide/container-product-policies.html#publishing-eks-add-on), 14 | covering static and dynamic (deployment/runtime) aspects. 15 | 16 | ## Pre-requisites 17 | To implement this solution, you need the following prerequisites: 18 | 19 | * The [AWS Command Line Interface](http://aws.amazon.com/cli) (AWS CLI) [installed](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). 20 | The AWS CLI is a unified tool to manage your AWS services. 21 | * AWS CLI default profile should be configured to access your AWS Account. 22 | * [Node](https://nodejs.org/en/download/current/) version 18.12.1 or later. 23 | * [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) version 8.19.2 or later. 24 | * [Helm CLI](https://helm.sh/docs/intro/install/) to interact with helm charts. 25 | 26 | ## Quick-install 27 | 28 | You can run `make` or execute `install.sh` to build this project and install the resulting library. In this case only the following are required: 29 | 30 | * The [AWS Command Line Interface](http://aws.amazon.com/cli) (AWS CLI) [installed](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). 31 | The AWS CLI is a unified tool to manage your AWS services. 32 | * [NVM](https://github.com/nvm-sh/nvm#install--update-script) 33 | 34 | Both of these install the suitable Node, Npm and Helm versions required. 35 | 36 | ## Cloud Shell Installation 37 | To quickly get started with this transformer, you can leverage CloudShell in the AWS Console. Some prerequisites you need: 38 | * Access to the helm chart to pull it 39 | * Install the Helm CLI in CloudShell using the following commands: 40 | ```shell 41 | $ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 42 | $ chmod 700 get_helm.sh 43 | $ ./get_helm.sh 44 | ``` 45 | * A GitHub token as required by [The GitHub Service](README.md#request-submission-for-onboarding-the-add-on-to-the-program) 46 | 47 | To use this CLI in CloudShell, 48 | * Log into the AWS Console with a role that has access to the location of the helm chart 49 | * If the chart is in a private ECR repo, ensure the role can pull from that repo. 50 | * If the chart is in a public repo, ensure that there aren't any permissions restricting access to the public domain 51 | * Use the npm install command to directly install the CLI into the shell: `npm i -g addons-transformer-for-amazon-eks` 52 | * Follow steps in [the Helm chart validation section](README.md#helm-chart-validation) for all other questions. 53 | 54 | ## Features 55 | This npm module has the following features: 56 | 57 | ### Helm chart validation 58 | 59 | This NPM module accepts two kinds of input: 60 | - CLI Args as described in the [Commands](README.md#commands) section 61 | - Input file as described in [AddOn Submission](#request-submission-for-onboarding-the-add-on-to-the-program) 62 | 63 | The module then performs static validation to attempt to find the following: 64 | - Finding occurrences of unsupported `.Capabilities` 65 | - Templates creating `helm.sh/hook` 66 | - Use of `.Release.Service` 67 | - Use of helm lookup function 68 | - Dependencies external to the main chart 69 | - Errors running `helm lint` see [lint command](#helm-lint-command) below 70 | - Errors running `helm template...` (see [template command](#helm-template-command) below 71 | 72 | 73 | If the chart is not in a public registry, login on it in advance is necessary, for example, for login on ECR: 74 | 75 | ```shell 76 | export AWS_ACCOUNT= 77 | export AWS_REGION= 78 | export CHART_NAME= 79 | export ECR_HELM_REPOSITORY=${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/${CHART_NAME} 80 | aws ecr get-login-password --region eu-west-1 | helm registry login --username AWS --password-stdin ${ECR_HELM_REPOSITORY%%/*} 81 | ``` 82 | 83 | #### Helm lint command 84 | ```shell 85 | helm lint --strict --with-subcharts $CHART_LOCATION 86 | ``` 87 | 88 | 89 | #### Helm template command 90 | ```shell 91 | helm template $CHART_NAME $CHART_LOCATION 92 | --set k8version=$KUBERNETES_VERSION 93 | --kube-version $KUBERNETES_VERSION 94 | --namespace $ADDON_NAMESPACE 95 | --include-crds 96 | --no-hooks 97 | ``` 98 | 99 | ### Request submission for onboarding the add-on to the program 100 | 101 | This functionality creates a GitHub issue in the onboarding repository for starting the 102 | process. As input, it takes the path to a `yaml` template that should contain the vendor, 103 | product and the add-on required information. The json-schema for its creation can be found 104 | in this repo [schema](./schema/onboarding.schema.json) and an example in the [doc/examples](./doc/examples/onboarding.example.yaml) 105 | directory. 106 | 107 | For validation the template, it supports the flag `--dry-run` that prevents the issue creation. 108 | 109 | As it will run locally `aws-sleek-transformer validate` passing the file as input, it needs to be able to download the 110 | chart. 111 | 112 | ## Installation 113 | 114 | ```sh-session 115 | $ npm install -g addons-transformer-for-amazon-eks 116 | $ addons-transformer-for-amazon-eks COMMAND 117 | running command... 118 | $ addons-transformer-for-amazon-eks (--version) 119 | addons-transformer-for-amazon-eks/1.1.0 darwin-arm64 node-v20.16.0 120 | $ addons-transformer-for-amazon-eks --help [COMMAND] 121 | USAGE 122 | $ addons-transformer-for-amazon-eks COMMAND 123 | ... 124 | ``` 125 | 126 | ## Commands 127 | 128 | * [`addons-transformer-for-amazon-eks create-issue FILE`](#addons-transformer-for-amazon-eks-create-issue-file) 129 | * [`addons-transformer-for-amazon-eks validate [HELMURL]`](#addons-transformer-for-amazon-eks-validate-helmurl) 130 | 131 | ## `addons-transformer-for-amazon-eks create-issue FILE` 132 | 133 | Creates a Github Issue based in the input file 134 | 135 | ``` 136 | USAGE 137 | $ addons-transformer-for-amazon-eks create-issue FILE [-d] 138 | 139 | ARGUMENTS 140 | FILE Path to add-on input file 141 | 142 | FLAGS 143 | -d, --dryRun Validates the input file schema without creating the issue nor validating the chart 144 | 145 | DESCRIPTION 146 | Creates a Github Issue based in the input file 147 | 148 | 149 | This creates a Github Issue on the Sleek repository. 150 | 151 | It will validate the input file to match the schema 152 | 153 | 154 | EXAMPLES 155 | $ addons-transformer-for-amazon-eks create-issue filename 156 | ``` 157 | 158 | _See code: [src/commands/create-issue.ts](https://github.com/aws-samples/addons-transformer-for-amazon-eks/blob/v1.1.0/src/commands/create-issue.ts)_ 159 | 160 | ## `addons-transformer-for-amazon-eks validate [HELMURL]` 161 | 162 | Validates the addon after pulling it from the helm repository. 163 | 164 | ``` 165 | USAGE 166 | $ addons-transformer-for-amazon-eks validate [HELMURL] [-d | [-f | --helmUrl ] | ] 167 | [-r | | ] [-p | | ] [-v | ] [--addonName ] [-n ] [--k8sVersions ] 168 | [--skipHooks] [--skipReleaseService] 169 | 170 | ARGUMENTS 171 | HELMURL Fully qualified Helm URL of the addon 172 | 173 | FLAGS 174 | -d, --directory= Path to the local addon folder 175 | -f, --file= Path to add-on input file 176 | -n, --addonNamespace= Add-on namespace 177 | -p, --protocol= Protocol of the helm hosting to use 178 | -r, --helmRepo= URL of the helm repo containing protocol and repo 179 | -v, --version= Version of the addon to validate 180 | --addonName= Name of the addon 181 | --helmUrl= Fully qualified URL of the Repo including version tag 182 | --k8sVersions= Comma separated list of supported kubernetes versions 183 | --skipHooks Skip helm hooks validation 184 | --skipReleaseService Skip .Release.Service occurrences 185 | 186 | DESCRIPTION 187 | Validates the addon after pulling it from the helm repository. 188 | 189 | 190 | This performs pre-launch validations of the partner software on compatibility with Sleek guidelines, covering static 191 | and dynamic (deployment/runtime) aspects. 192 | 193 | Runs the static analysis to find occurrences of: 194 | * .Capabilities 195 | * helm.sh/hook 196 | * external helm dependencies 197 | 198 | It will perform a static validation on the device and then give you the option to submit it to the marketplace for 199 | runtime and further validation before it can be included in the EKS Console marketplace. 200 | 201 | The command can accept two different formats of inputs: 202 | * Fully qualified Helm URL to download 203 | * Deconstructed URL that requires Protocol, Repo, and Version to pull 204 | 205 | 206 | EXAMPLES 207 | $ addons-transformer-for-amazon-eks validate oci://12345678901.dkr.ecr.us-east-2.amazonaws.com/example-charts:x.x.x 208 | 209 | $ addons-transformer-for-amazon-eks validate -r 12345678901.dkr.ecr.us-east-2.amazonaws.com/example-charts -p oci -v x.x.x 210 | 211 | $ addons-transformer-for-amazon-eks validate -f ./input.yaml 212 | 213 | $ addons-transformer-for-amazon-eks validate -d ./addon-folder 214 | 215 | $ addons-transformer-for-amazon-eks validate --help 216 | ``` 217 | 218 | _See code: [src/commands/validate.ts](https://github.com/aws-samples/addons-transformer-for-amazon-eks/blob/v1.1.0/src/commands/validate.ts)_ 219 | 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /src/services/validate.ts: -------------------------------------------------------------------------------- 1 | import {spawnSync} from "node:child_process"; 2 | 3 | import {SleekCommand} from "../sleek-command.js"; 4 | import {AddonData} from "../types/issue.js"; 5 | import {ServiceResponse} from "../types/service.js"; 6 | import {ValidateOptions} from "../types/validate.js"; 7 | import {BaseService} from "./base-service.js"; 8 | 9 | import {Debug} from "@oclif/core/lib/config/util.js"; 10 | 11 | export const SuccessResponse: ServiceResponse = { 12 | success: true, 13 | }; 14 | 15 | export const ValidationSkipped: ServiceResponse = { 16 | success: true, 17 | body: 'validation skipped' 18 | }; 19 | export const ExtendValidationSuccess: ServiceResponse = { 20 | success: true, 21 | body: 'Extend validation to be implemented; SUCCESS' 22 | }; 23 | export const ExtendValidationFail: ServiceResponse = { 24 | success: false, 25 | body: 'Extend validation to be implemented; FAIL', 26 | error: { 27 | input: 'Extend validation to be implemented; FAIL', 28 | options: { 29 | exit: 2, 30 | } 31 | } 32 | }; 33 | 34 | export default class ChartValidatorService extends BaseService { 35 | private debug = Debug("validator"); 36 | 37 | private readonly name: string; 38 | private readonly namespace: string; 39 | private readonly supportedKubernetesVersions: string[]; 40 | // this will always be a local filepath 41 | private readonly toValidate: string; 42 | 43 | constructor(commandCaller: SleekCommand, toValidate: string, addonData: AddonData) { 44 | super(commandCaller); 45 | this.toValidate = `"${toValidate}"`; 46 | this.name = `"${addonData.name}"`; 47 | this.namespace = `"${addonData.namespace}"`; 48 | this.supportedKubernetesVersions = addonData.kubernetesVersion!; 49 | } 50 | 51 | public async validate(ops: ValidateOptions): Promise> { 52 | const lintResult = await this.runHelmLint(); 53 | const templateResult = await this.runHelmTemplate(); 54 | const capabilities = await this.findCapabilities(); 55 | const hooks = ops.skipHooksValidation ? ValidationSkipped : await this.findHooks(); 56 | const dependencies = await this.findDependencies(); 57 | const unsupportedReleaseObjects = await this.findUnsupportedReleaseObject(ops.skipReleaseService!); 58 | const lookups = await this.findLookups(); 59 | 60 | const allValidation = [ 61 | lintResult, 62 | templateResult, 63 | capabilities, 64 | hooks, 65 | dependencies, 66 | unsupportedReleaseObjects, 67 | lookups, 68 | ] 69 | let response: ServiceResponse = { 70 | success: false, 71 | body: "", 72 | error: { 73 | input: "", 74 | options: { 75 | code: "", 76 | exit: 5 77 | } 78 | } 79 | }; 80 | 81 | if (allValidation.every(validation => validation.success)) { 82 | response = { 83 | success: true, 84 | body: "Addon pre-validation complete" 85 | } 86 | return response; 87 | } 88 | 89 | // Failure scenarios: 90 | response.body = "Addon pre-validation failed, reasons listed below: " 91 | for (const validationResponse of allValidation 92 | .filter(validation => !validation.success)) { 93 | response = { 94 | success: false, 95 | body: `${response.body} \n * ${validationResponse.body}`, 96 | error: { 97 | input: `${response.error?.input} ${validationResponse.error?.input}`, 98 | } 99 | } 100 | } 101 | 102 | return response; 103 | } 104 | 105 | private async findCapabilities(): Promise> { 106 | // create two templates, one with CRDs and the other without 107 | // then grep the two generated outputs to see if there's a delta in how many capabilities are found 108 | // if there is a delta, then there are unsupported capabilities in the chart 109 | // this is a bit hacky, but it works for now 110 | let allVersionSuccess = true; 111 | const errors: string[] = []; 112 | 113 | for (const k8sVersion of this.supportedKubernetesVersions) { 114 | const withCrds = this.getNoCrdsTemplateResult(k8sVersion); 115 | const withoutCrds = this.getTemplateResult(k8sVersion); 116 | 117 | const capsWithCrds = spawnSync('grep', ['-ilne', '".Capabilities"', "<<<", withCrds.stdout], {shell: true, encoding: "utf8"}); 118 | const capsWithoutCrds = spawnSync('grep', ['-ilne', '".Capabilities"', "<<<", withoutCrds.stdout], {shell: true, encoding: "utf8"}); 119 | 120 | if (capsWithCrds.stdout !== capsWithoutCrds.stdout) { 121 | allVersionSuccess = false; 122 | errors.push(`Unsupported system Capabilities are used in chart for Kubernetes version ${k8sVersion}.`) 123 | } 124 | } 125 | 126 | // success execution 127 | if (allVersionSuccess) { 128 | return SuccessResponse; 129 | } 130 | 131 | // any off the templates failed 132 | return { 133 | success: false, 134 | body: `.Capabilities detected for at least one Kubernetes version'`, 135 | error: { 136 | input: errors.join(''), 137 | options: { 138 | code: "E501", 139 | exit: 1 140 | } 141 | } 142 | }; 143 | } 144 | 145 | private async findDependencies(): Promise> { 146 | const grepDependencies = spawnSync('helm', ['dependency', 'list', this.toValidate], { 147 | shell: true, 148 | encoding: "utf8" 149 | }); 150 | 151 | // split every line in the output 152 | // discard headers 153 | // in every subsequent line, get the output at index 1 (which is the relative path of a dependency) 154 | const dependencies = grepDependencies.stdout.toString() 155 | .split('/n') 156 | .slice(1) 157 | .map(line => line.split('/t')[1]) 158 | .filter(Boolean); 159 | 160 | this.debug(`dependencies found: ${dependencies}`); 161 | 162 | if (dependencies.length === 0) { 163 | return SuccessResponse; 164 | } 165 | 166 | // check dependencies to ensure they all contain file:// 167 | const allDepsFiles = dependencies.every(dep => dep.includes('file://')); 168 | 169 | return allDepsFiles ? SuccessResponse : { 170 | success: false, 171 | body: "Not all dependencies reside in the main chart.", 172 | error: { 173 | input: dependencies.toString(), 174 | options: { 175 | code: "E503", 176 | exit: 1 177 | } 178 | } 179 | }; 180 | } 181 | 182 | private async findHooks(): Promise> { 183 | const hooks = spawnSync('grep', ['-Rine', '"helm.sh/hook"', this.toValidate], {shell: true, encoding: "utf8"}); 184 | 185 | return hooks.stdout === "" ? SuccessResponse : { 186 | success: false, 187 | body: "Unsupported system Hooks are used in chart.", 188 | error: { 189 | input: hooks.stdout, 190 | options: { 191 | code: "E502", 192 | exit: 1 193 | } 194 | } 195 | }; 196 | } 197 | 198 | private async findLookups(): Promise> { 199 | // Find any instance of "lookup" that starts with an opening parenthesis and ignore any white spaces between opening 200 | // and the word itself 201 | const grepLookup = spawnSync('grep', ['-RinE', "'\\{\\{-\\?*.*\\s*(lookup)\\s'", `"${this.toValidate}"`], {shell: true, encoding: "utf8"}); 202 | 203 | this.debug(`grepLookup out: ${grepLookup.stdout}`); 204 | 205 | if (grepLookup.stdout === "") { 206 | return SuccessResponse; 207 | } 208 | 209 | return { 210 | success: false, 211 | body: "Helm charts use lookup functions", 212 | error: { 213 | input: "Lookup functions not permitted", 214 | options: { 215 | code: "E507", 216 | exit: 1 217 | } 218 | } 219 | } 220 | } 221 | 222 | private async findUnsupportedReleaseObject(skipReleaseService: boolean): Promise> { 223 | // beta guide 6.1.b) All Release objects (except .Name and .Namespace) are not supported 224 | const unsupportedReleaseObjectsRegex = skipReleaseService ? 225 | "'.Release.[Name|Namespace|Service]'" : 226 | "'.Release.[Name|Namespace]'"; 227 | 228 | const allReleaseObjects = spawnSync('grep', ['-rn', '.Release.', this.toValidate], {shell: true, encoding: "utf8"}); 229 | const unsupportedReleaseObjects = spawnSync('grep', ['-vn', unsupportedReleaseObjectsRegex, this.toValidate], { 230 | shell: true, 231 | encoding: "utf8", 232 | input: allReleaseObjects.stdout 233 | }); 234 | 235 | return unsupportedReleaseObjects.stdout === "" ? SuccessResponse : { 236 | success: false, 237 | body: "Unsupported release objects are used in chart.", 238 | error: { 239 | input: unsupportedReleaseObjects.stdout, 240 | options: { 241 | code: "E504", 242 | exit: 1 243 | } 244 | } 245 | }; 246 | } 247 | 248 | /** 249 | * 250 | * helm template 251 | * --set k8version= 252 | * --kube-version 253 | * --namespace 254 | * --include-crds 255 | * --no-hooks 256 | * --f TODO 257 | * 258 | * @param k8sVersion 259 | * @private 260 | */ 261 | private getTemplateResult(k8sVersion: string) { 262 | return spawnSync('helm', [ 263 | 'template', 264 | this.name, 265 | this.toValidate, 266 | `--set k8version=${k8sVersion}`, 267 | '--kube-version', k8sVersion, 268 | '--namespace', this.namespace, 269 | '--include-crds', 270 | '--no-hooks', 271 | ], {shell: true, encoding: "utf8"}); 272 | } 273 | 274 | /** 275 | * https://helm.sh/docs/helm/helm_lint/ 276 | * @private 277 | */ 278 | private async runHelmLint(): Promise> { 279 | const lintResult = spawnSync('helm', ['lint', ' --strict', '--with-subcharts', this.toValidate], { 280 | shell: true, 281 | encoding: "utf8" 282 | }); 283 | 284 | // success execution 285 | if (lintResult.status === 0) { 286 | return SuccessResponse 287 | } 288 | 289 | // lint issues found 290 | return { 291 | success: false, 292 | body: `Helm linter found errors running 'helm lint --strict --with-subcharts ${this.toValidate}'`, 293 | error: { 294 | input: lintResult.stdout, 295 | options: { 296 | code: "E505", 297 | exit: 1 298 | } 299 | } 300 | } 301 | } 302 | 303 | /** 304 | * https://helm.sh/docs/helm/helm_template/ 305 | * @private 306 | */ 307 | private async runHelmTemplate(): Promise> { 308 | const errors: string[] = []; 309 | let allVersionSuccess = true; 310 | 311 | for (const k8sVersion of this.supportedKubernetesVersions) { 312 | const templateResult = this.getTemplateResult(k8sVersion); 313 | // this.log(`Templating for k8s version ${k8sVersion} ${templateResult.status===0?'successful':'errored'}`) 314 | if (templateResult.status !== 0) { 315 | allVersionSuccess = false 316 | errors.push(`Kubernetes version: ${k8sVersion} - ${templateResult.stderr}`) 317 | } 318 | } 319 | 320 | // success execution 321 | if (allVersionSuccess) { 322 | return SuccessResponse; 323 | } 324 | 325 | // any off the templates failed 326 | return { 327 | success: false, 328 | body: `Helm templated failed for at least one kubernetes version'`, 329 | error: { 330 | input: errors.join(''), 331 | options: { 332 | code: "E506", 333 | exit: 1 334 | } 335 | } 336 | } 337 | } 338 | 339 | // eslint-disable-next-line valid-jsdoc 340 | /** 341 | * helm template 342 | * --set k8version= 343 | * --kube-version 344 | * --namespace 345 | * --skip-crds 346 | * --f TODO 347 | */ 348 | private getNoCrdsTemplateResult(k8sVersion: string) { 349 | return spawnSync('helm', [ 350 | 'template', 351 | this.name, 352 | this.toValidate, 353 | `--set k8version=${k8sVersion}`, 354 | '--kube-version', k8sVersion, 355 | '--namespace', this.namespace, 356 | '--skip-crds', 357 | ], {shell: true, encoding: "utf8"}); 358 | } 359 | } 360 | --------------------------------------------------------------------------------