├── .devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yaml │ ├── enhancement.yaml │ └── failing-test.yaml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── apply.yml │ ├── cla.yml │ ├── deploy │ └── deploy.yml │ └── preview.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── example ├── ai-agent-demo │ ├── dev │ │ ├── example_workspace.yaml │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ ├── project.yaml │ └── src │ │ ├── demo.py │ │ └── nvidia-10q.pdf ├── nginx │ ├── dev │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ └── project.yaml ├── quickstart │ ├── default │ │ ├── kcl.mod │ │ ├── main.k │ │ ├── stack.yaml │ │ └── workspace.yaml │ └── project.yaml ├── service-multi-stack │ ├── base │ │ └── base.k │ ├── dev │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ ├── prod │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ └── project.yaml ├── simple-job │ ├── dev │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ └── project.yaml ├── simple-service │ ├── dev │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ └── project.yaml ├── wordpress-cloud-rds │ ├── prod │ │ ├── kcl.mod │ │ ├── main.k │ │ └── stack.yaml │ └── project.yaml └── wordpress-local-db │ ├── prod │ ├── kcl.mod │ ├── main.k │ └── stack.yaml │ └── project.yaml └── hack ├── README.md ├── apply_changed_stacks.py ├── check_structure.py ├── get_changed_projects_and_stacks.py ├── lib ├── __init__.py ├── ansi2html │ └── converter.py ├── common.py └── util.py ├── preview_changed_stacks.py └── test_correctness.py /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kubernetes-Minikube-in-Docker", 3 | "image": "mcr.microsoft.com/devcontainers/base:bullseye", 4 | "features": { 5 | "ghcr.io/devcontainers/features/common-utils:1": { 6 | "installZsh": "true", 7 | "username": "vscode", 8 | "uid": "1000", 9 | "gid": "1000", 10 | "upgradePackages": "false", 11 | "installOhMyZsh": "true", 12 | "nonFreePackages": "true" 13 | }, 14 | "ghcr.io/devcontainers/features/docker-in-docker:1": { 15 | "enableNonRootDocker": "true", 16 | "username": "vscode", 17 | "moby": "true" 18 | }, 19 | "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { 20 | "version": "latest", 21 | "helm": "latest", 22 | "minikube": "latest" 23 | }, 24 | "ghcr.io/KusionStack/devcontainer-features/kusion:0": {} 25 | }, 26 | "overrideFeatureInstallOrder": [ 27 | "ghcr.io/devcontainers/features/common-utils", 28 | "ghcr.io/devcontainers/features/docker-in-docker", 29 | "ghcr.io/devcontainers/features/kubectl-helm-minikube", 30 | "ghcr.io/KusionStack/devcontainer-features/kusion:0" 31 | ], 32 | "extensions": [ 33 | "KusionStack.kusion", 34 | "kcl.kcl-vscode-extension" 35 | ], 36 | "customizations": { 37 | "codespaces": { 38 | "openFiles": [ 39 | "appops/guestbook/dev/main.k", 40 | "appops/guestbook/base/base.k" 41 | ] 42 | } 43 | }, 44 | "containerEnv": { 45 | "KUSION_QUICK_START": "true" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug encountered while operating Kusion 3 | labels: kind/bug 4 | body: 5 | - type: textarea 6 | id: problem 7 | attributes: 8 | label: What happened? 9 | description: | 10 | Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: expected 16 | attributes: 17 | label: What did you expect to happen? 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: repro 23 | attributes: 24 | label: How can we reproduce it (as minimally and precisely as possible)? 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: additional 30 | attributes: 31 | label: Anything else we need to know? 32 | 33 | - type: textarea 34 | id: kusionVersion 35 | attributes: 36 | label: Kusion version 37 | value: | 38 |
39 | 40 | ```console 41 | $ kusion version 42 | # paste output here 43 | ``` 44 | 45 |
46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: osVersion 51 | attributes: 52 | label: OS version 53 | value: | 54 |
55 | 56 | ```console 57 | # On Linux: 58 | $ cat /etc/os-release 59 | # paste output here 60 | $ uname -a 61 | # paste output here 62 | 63 | # On Windows: 64 | C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture 65 | # paste output here 66 | ``` 67 | 68 |
69 | 70 | - type: textarea 71 | id: installer 72 | attributes: 73 | label: Install tools 74 | value: | 75 |
76 | 77 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yaml: -------------------------------------------------------------------------------- 1 | name: Enhancement Tracking Issue 2 | description: Provide supporting details for a feature in development 3 | labels: kind/feature 4 | body: 5 | - type: textarea 6 | id: feature 7 | attributes: 8 | label: What would you like to be added? 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | id: rationale 14 | attributes: 15 | label: Why is this needed? 16 | validations: 17 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/failing-test.yaml: -------------------------------------------------------------------------------- 1 | name: Failing Test 2 | description: Report continuously failing tests or jobs in Konfig CI 3 | labels: kind/failing-test 4 | body: 5 | - type: textarea 6 | id: jobs 7 | attributes: 8 | label: Which jobs are failing? 9 | placeholder: | 10 | Please only use this template for submitting reports about continuously failing tests or jobs in Konfig CI. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: tests 16 | attributes: 17 | label: Which tests are failing? 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: since 23 | attributes: 24 | label: Since when has it been failing? 25 | validations: 26 | required: true 27 | 28 | - type: input 29 | id: testgrid 30 | attributes: 31 | label: Testgrid link 32 | 33 | - type: textarea 34 | id: reason 35 | attributes: 36 | label: Reason for failure (if possible) 37 | 38 | - type: textarea 39 | id: additional 40 | attributes: 41 | label: Anything else we need to know? -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | #### What type of PR is this? 20 | 21 | 28 | 29 | #### What this PR does / why we need it: 30 | 31 | #### Which issue(s) this PR fixes: 32 | 37 | Fixes # 38 | 39 | #### Special notes for your reviewer: 40 | 41 | #### Does this PR introduce a user-facing change? 42 | 48 | ```release-note 49 | 50 | ``` 51 | 52 | #### Additional documentation e.g., design docs, usage docs, etc.: 53 | 54 | 60 | ```docs 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /.github/workflows/apply.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Apps with Specified Workspace 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | workspace: 7 | required: true 8 | type: choice 9 | description: The specified Workspace to apply 10 | options: 11 | - default 12 | - remote-test 13 | project-stack-paths: 14 | required: true 15 | description: The paths of the Project and Stack in the repository to apply 16 | 17 | jobs: 18 | preview: 19 | runs-on: ubuntu-20.04 20 | steps: 21 | - name: Checkout Code 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install Kusion 27 | run: curl https://www.kusionstack.io/scripts/install.sh | sh -s 0.12.1-rc.2 28 | 29 | - name: Install Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: 3.9 33 | 34 | - name: Install Pytest Html 35 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 36 | 37 | - name: Setup K3d&K3s 38 | uses: nolar/setup-k3d-k3s@v1 39 | with: 40 | version: v1.25.15+k3s1 41 | 42 | - name: Preview 43 | id: preview 44 | env: 45 | WORKSPACE_NAME: ${{ github.event.inputs.workspace }} 46 | CHANGED_STACKS: ${{ github.event.inputs.project-stack-paths }} 47 | OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} 48 | OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} 49 | WORKSPACE_FILE_DIR: workspaces 50 | run: | 51 | # manually source kusion env file 52 | source "$HOME/.kusion/.env" 53 | 54 | # setup remote backend for kusion cli 55 | kusion config set backends.oss_test '{"type":"oss","configs":{"bucket":"kusion-test","endpoint":"oss-cn-shanghai.aliyuncs.com"}}' 56 | kusion config set backends.current oss_test 57 | 58 | # switch to the specified workspace 59 | kusion workspace switch $WORKSPACE_NAME 60 | 61 | # execute python script for previewing 62 | python3 hack/preview_changed_stacks.py 63 | 64 | - name: Upload Report 65 | id: upload-report 66 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: preview-report 70 | path: hack/report/preview-result.zip 71 | 72 | - name: Echo Preview Report URL 73 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 74 | run: | 75 | echo "Please check the preview result at: ${{ steps.upload-report.outputs.artifact-url }}" 76 | 77 | - name: Approve Preview 78 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 79 | uses: trstringer/manual-approval@v1 80 | with: 81 | secret: ${{ secrets.PACKAGE_TOKEN }} 82 | approvers: "liu-hm19" 83 | minimum-approvals: 1 84 | issue-title: "[Preview] Deploying ${{ github.event.inputs.project-stack-paths }}" 85 | issue-body: "Please check the preview report at: ${{ steps.upload-report.outputs.artifact-url }}" 86 | 87 | apply: 88 | needs: [ preview ] 89 | runs-on: ubuntu-20.04 90 | steps: 91 | - name: Checkout Code 92 | uses: actions/checkout@v4 93 | with: 94 | fetch-depth: 0 95 | 96 | - name: Install Kusion 97 | run: curl https://www.kusionstack.io/scripts/install.sh | sh -s 0.12.1-rc.2 98 | 99 | - name: Install Python 100 | uses: actions/setup-python@v4 101 | with: 102 | python-version: 3.9 103 | 104 | - name: Install Pytest Html 105 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 106 | 107 | - name: Setup K3d&K3s 108 | uses: nolar/setup-k3d-k3s@v1 109 | 110 | - name: Apply 111 | id: apply 112 | env: 113 | WORKSPACE_NAME: ${{ github.event.inputs.workspace }} 114 | CHANGED_STACKS: ${{ github.event.inputs.project-stack-paths }} 115 | OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} 116 | OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} 117 | WORKSPACE_FILE_DIR: workspaces 118 | run: | 119 | # manually source kusion env file 120 | source "$HOME/.kusion/.env" 121 | 122 | # setup remote backend for kusion cli 123 | kusion config set backends.oss_test '{"type":"oss","configs":{"bucket":"kusion-test","endpoint":"oss-cn-shanghai.aliyuncs.com"}}' 124 | kusion config set backends.current oss_test 125 | 126 | # switch to the specified workspace 127 | kusion workspace switch $WORKSPACE_NAME 128 | 129 | # execute python script for previewing 130 | python3 hack/apply_changed_stacks.py 131 | 132 | - name: Upload Report 133 | id: upload-report 134 | if: ${{ steps.apply.outputs.apply_success == 'true' }} 135 | uses: actions/upload-artifact@v4 136 | with: 137 | name: apply-report 138 | path: hack/report/apply-result.zip 139 | - name: Echo Apply Report URL 140 | if: ${{ steps.apply.outputs.apply_success == 'true' }} 141 | run: | 142 | echo "Please check the apply result at: ${{ steps.upload-report.outputs.artifact-url }}" 143 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | jobs: 9 | CLAssistant: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "CLA Assistant" 13 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 14 | uses: contributor-assistant/github-action@v2.3.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | # the below token should have repo scope and must be manually added by you in the repository's secret 18 | PERSONAL_ACCESS_TOKEN : ${{ secrets.KUSIONSTACK_BOT_TOKEN }} 19 | with: 20 | path-to-document: 'https://github.com/KusionStack/.github/blob/main/CLA.md' # e.g. a CLA or a DCO document 21 | 22 | # branch should not be protected 23 | lock-pullrequest-aftermerge: True 24 | path-to-signatures: 'signatures/version1/cla.json' 25 | remote-organization-name: KusionStack 26 | remote-repository-name: cla.db 27 | branch: 'main' 28 | allowlist: test,bot* 29 | 30 | #below are the optional inputs - If the optional inputs are not given, then default values will be taken 31 | #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) 32 | #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) 33 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 34 | #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' 35 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 36 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 37 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 38 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 39 | #use-dco-flag: true - If you are using DCO instead of CLA 40 | -------------------------------------------------------------------------------- /.github/workflows/deploy/deploy.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | get-changed-project-stack: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.9 20 | - name: Install Pytest Html 21 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 22 | - name: Git Diff 23 | id: git-diff 24 | uses: technote-space/get-diff-action@v6 25 | - name: Get Changed Project and Stack 26 | id: get-changed-project-and-stack 27 | env: 28 | CHANGED_PATHS: ${{ steps.git-diff.outputs.diff }} 29 | run: | 30 | export CHANGED_PATHS="${{ steps.git-diff.outputs.diff }}" 31 | python3 hack/get_changed_project_stack.py 32 | - name: Print Changed Project and Stack 33 | run: | 34 | echo "Changed projects: ${{ steps.get-changed-project-and-stack.outputs.changed_projects }}" 35 | echo "Changed stacks: ${{ steps.get-changed-project-and-stack.outputs.changed_stacks }}" 36 | outputs: 37 | changed_projects: ${{ steps.get-changed-project-and-stack.outputs.changed_projects }} 38 | changed_stacks: ${{ steps.get-changed-project-and-stack.outputs.changed_stacks }} 39 | 40 | check-structure: 41 | needs: get-changed-project-stack 42 | runs-on: ubuntu-latest 43 | container: 44 | image: kusionstack/kusion:latest 45 | steps: 46 | - name: Checkout Code 47 | uses: actions/checkout@v4 48 | with: 49 | fetch-depth: 0 50 | - name: Install Pytest Html 51 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 52 | - name: Check Structure 53 | env: 54 | CHANGED_PROJECTS: ${{ needs.get-changed-project-stack.outputs.changed_projects }} 55 | CHANGED_STACKS: ${{ needs.get-changed-project-stack.outputs.changed_stacks }} 56 | run: python3 -m pytest -v hack/check_structure.py --junitxml ./hack/report/check-structure.xml --html ./hack/report/check-structure.html 57 | - name: Upload Report 58 | if: always() 59 | uses: actions/upload-artifact@v3 60 | with: 61 | name: check-structure-report 62 | path: | 63 | hack/report/check-structure.xml 64 | hack/report/check-structure.html 65 | 66 | test-correctness: 67 | needs: get-changed-project-stack 68 | runs-on: ubuntu-latest 69 | container: 70 | image: kusionstack/kusion:latest 71 | steps: 72 | - name: Checkout Code 73 | uses: actions/checkout@v4 74 | with: 75 | fetch-depth: 0 76 | - name: Install Pytest Html 77 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 78 | - name: Test Correctness 79 | env: 80 | CHANGED_STACKS: ${{ needs.get-changed-project-stack.outputs.changed_stacks }} 81 | WORKSPACE_FILE_DIR: workspaces 82 | run: python3 -m pytest -v hack/test_correctness.py --junitxml ./hack/report/test-correctness.xml --html ./hack/report/test-correctness.html 83 | - name: Upload Report 84 | if: always() 85 | uses: actions/upload-artifact@v3 86 | with: 87 | name: test-correctness-report 88 | path: | 89 | hack/report/test-correctness.xml 90 | hack/report/test-correctness.html 91 | 92 | preview: 93 | needs: [ get-changed-project-stack, check-structure, test-correctness ] 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Checkout Code 97 | uses: actions/checkout@v4 98 | with: 99 | fetch-depth: 0 100 | - name: Install Kusion 101 | run: curl https://www.kusionstack.io/scripts/install.sh | bash 102 | - name: Install Python 103 | uses: actions/setup-python@v4 104 | with: 105 | python-version: 3.9 106 | - name: Install Pytest Html 107 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 108 | - name: Setup K3d&K3s 109 | uses: nolar/setup-k3d-k3s@v1 110 | - name: Preview 111 | id: preview 112 | env: 113 | CHANGED_STACKS: ${{ needs.get-changed-project-stack.outputs.changed_stacks }} 114 | WORKSPACE_FILE_DIR: workspaces 115 | run: | 116 | #edit the profile in the post step does not work, source kusion env file manually 117 | source "$HOME/.kusion/.env" 118 | python3 hack/preview_changed_stacks.py 119 | - name: Upload Report 120 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 121 | uses: actions/upload-artifact@v3 122 | with: 123 | name: preview-report 124 | path: hack/report/preview-result.zip 125 | 126 | apply: 127 | needs: [get-changed-project-stack, preview] 128 | runs-on: ubuntu-latest 129 | steps: 130 | - name: Checkout Code 131 | uses: actions/checkout@v4 132 | with: 133 | fetch-depth: 0 134 | - name: Install Kusion 135 | run: curl https://www.kusionstack.io/scripts/install.sh | bash 136 | - name: Install Python 137 | uses: actions/setup-python@v4 138 | with: 139 | python-version: 3.9 140 | - name: Install Pytest Html 141 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 142 | - name: Setup K3d&K3s 143 | uses: nolar/setup-k3d-k3s@v1 144 | - name: Apply 145 | id: apply 146 | env: 147 | CHANGED_STACKS: ${{ needs.get-changed-project-stack.outputs.changed_stacks }} 148 | WORKSPACE_FILE_DIR: workspaces 149 | run: | 150 | #edit the profile in the post step does not work, source kusion env file manually 151 | source "$HOME/.kusion/.env" 152 | python3 hack/apply_changed_stacks.py 153 | - name: Upload Report 154 | if: ${{ steps.apply.outputs.apply_success == 'true' }} 155 | uses: actions/upload-artifact@v3 156 | with: 157 | name: apply-report 158 | path: hack/report/apply-result.zip 159 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Preview 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | get-changed-project-stack: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Install Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: 3.9 21 | 22 | - name: Install Pytest Html 23 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 24 | 25 | - name: Git Diff 26 | id: git-diff 27 | uses: technote-space/get-diff-action@v6 28 | 29 | - name: Get Changed Projects and Stacks 30 | id: get-changed-projects-and-stacks 31 | env: 32 | CHANGED_PATHS: ${{ steps.git-diff.outputs.diff }} 33 | run: | 34 | export CHANGED_PATHS="${{ steps.git-diff.outputs.diff }}" 35 | python3 hack/get_changed_projects_and_stacks.py 36 | 37 | - name: Print Changed Projects and Stacks 38 | run: | 39 | echo "Changed projects: ${{ steps.get-changed-projects-and-stacks.outputs.changed_projects }}" 40 | echo "Changed stacks: ${{ steps.get-changed-projects-and-stacks.outputs.changed_stacks }}" 41 | outputs: 42 | changed_projects: ${{ steps.get-changed-projects-and-stacks.outputs.changed_projects }} 43 | changed_stacks: ${{ steps.get-changed-projects-and-stacks.outputs.changed_stacks }} 44 | 45 | preview: 46 | needs: [ get-changed-project-stack ] 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout Code 50 | uses: actions/checkout@v4 51 | with: 52 | fetch-depth: 0 53 | 54 | - name: Install Kusion 55 | run: curl https://www.kusionstack.io/scripts/install.sh | sh -s 0.12.0-rc.2 56 | 57 | - name: Install Python 58 | uses: actions/setup-python@v4 59 | with: 60 | python-version: 3.9 61 | 62 | - name: Install Pytest Html 63 | run: python3 -m pip install pytest-html pyyaml -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 64 | 65 | - name: Setup K3d&K3s 66 | uses: nolar/setup-k3d-k3s@v1 67 | with: 68 | version: v1.25.15+k3s1 69 | 70 | - name: Preview 71 | id: preview 72 | env: 73 | CHANGED_STACKS: ${{ needs.get-changed-project-stack.outputs.changed_stacks }} 74 | OSS_ACCESS_KEY_ID: '${{ secrets.OSS_ACCESS_KEY_ID }}' 75 | OSS_ACCESS_KEY_SECRET: '${{ secrets.OSS_ACCESS_KEY_SECRET }}' 76 | WORKSPACE_FILE_DIR: workspaces 77 | AWS_REGION: us-east-1 78 | run: | 79 | # manually source kusion env file 80 | source "$HOME/.kusion/.env" 81 | 82 | # setup remote backend for kusion cli 83 | kusion config set backends.oss_test '{"type":"oss","configs":{"bucket":"kusion-test","endpoint":"oss-cn-shanghai.aliyuncs.com"}}' 84 | kusion config set backends.current oss_test 85 | 86 | # execute python script for previewing 87 | python3 hack/preview_changed_stacks.py 88 | 89 | - name: Upload report 90 | id: upload-report 91 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 92 | uses: actions/upload-artifact@v4 93 | with: 94 | name: preview-report 95 | path: hack/report/preview-result.zip 96 | 97 | - name: Echo URL 98 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 99 | run: echo "${{ steps.upload-report.outputs.artifact-url }}" 100 | 101 | - name: Approve preview 102 | if: ${{ steps.preview.outputs.preview_success == 'true' }} 103 | uses: trstringer/manual-approval@v1 104 | with: 105 | secret: ${{ secrets.PACKAGE_TOKEN }} 106 | approvers: ${{ github.event.pull_request.user.login }} 107 | minimum-approvals: 1 108 | issue-title: "Deploying ${{ needs.get-changed-project-stack.outputs.changed_stacks }}" 109 | issue-body: "Please check the preview report at: ${{ steps.upload-report.outputs.artifact-url }}" 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X files 2 | .DS_Store 3 | 4 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 5 | .glide/ 6 | 7 | # ide 8 | .idea/ 9 | *.iml 10 | .vscode/ 11 | 12 | # KCLVM cache 13 | __pycache__ 14 | __kclcache__ 15 | .kclvm 16 | 17 | 18 | # test report 19 | report/ 20 | 21 | kusion_state.json 22 | 23 | changed_list.txt 24 | 25 | *#.lock 26 | 27 | # kusion state 28 | kusion_state.yaml 29 | 30 | # kcl mod lock files 31 | kcl.mod.lock -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # KusionStack Community Code of Conduct 2 | 3 | KusionStack follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # konfig 2 | 3 | konfig contains various example applications code, you can use these example applications to quickly try out and learn how to use [Kusion](https://github.com/KusionStack/kusion). 4 | 5 | If you're new to Kusion, it's a good idea to start by [Getting Started](https://www.kusionstack.io/docs/kusion/getting-started/install-kusion), which includes quickstart installation, guided tutorials and deployment instructions. Then you can go to [User Guides](https://www.kusionstack.io/docs) to find more Kusion practical application scenarios, and konfig holds the corresponding examples of the user guides. 6 | 7 | ## konfig examples 8 | 9 | | name | description |user guide| 10 | |----------------------------------------------------|--------------------------------------------------------------------------------|----------| 11 | | [nginx](example/nginx) | An exposing nginx service. |[Expose Application Service Deployed on CSP Kubernetes](https://www.kusionstack.io/docs/kusion/user-guides/cloud-resources/expose-service)| 12 | | [simple-job](example/simple-job) | An one-off or recurring execution task. |[Schedule a Job](https://www.kusionstack.io/docs/kusion/user-guides/working-with-k8s/job)| 13 | | [service-multi-stack](example/service-multi-stack) | A project which contains multiple stacks. |[Deploy Application Securely and Efficiently via GitHub Actions](https://www.kusionstack.io/docs/kusion/user-guides/github-actions/deploy-application-via-github-actions)| 14 | | [simple-service](example/simple-service) | A simple service only contains workload resources. |[Deploy Application](https://www.kusionstack.io/docs/kusion/user-guides/working-with-k8s/deploy-application)| 15 | | [wordpress-cloud-rds](example/wordpress-cloud-rds) | The wordpress application which contains workload and cloud database resource. |[Deliver the WordPress Application with Cloud RDS](https://www.kusionstack.io/docs/next/kusion/user-guides/cloud-resources/database)| 16 | | [wordpress-local-db](example/wordpress-lcoal-db) | The wordpress application which contains workload and local database resource. |[Deliver the WordPress Application on Kubernetes](https://www.kusionstack.io/docs/kusion/getting-started/deliver-wordpress)| 17 | -------------------------------------------------------------------------------- /example/ai-agent-demo/dev/example_workspace.yaml: -------------------------------------------------------------------------------- 1 | modules: 2 | service: 3 | path: oci://ghcr.io/kusionstack/service 4 | version: 0.1.0 5 | configs: 6 | default: 7 | replicas: 1 8 | opensearch: 9 | path: oci://ghcr.io/kusionstack/opensearch 10 | version: 0.1.0 11 | configs: 12 | default: 13 | region: us-east-1 14 | clusterConfig: 15 | instanceType: r6g.large.search 16 | ebsOptions: 17 | ebsEnabled: true 18 | volumeSize: 10 19 | statement: 20 | - effect: Allow 21 | principals: 22 | - type: AWS 23 | identifiers: 24 | - "*" 25 | action: 26 | - es:* 27 | # AWS SecretManager config 28 | secretStore: 29 | provider: 30 | aws: 31 | region: us-east-1 32 | -------------------------------------------------------------------------------- /example/ai-agent-demo/dev/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | 4 | [dependencies] 5 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 6 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 7 | opensearch = { oci = "oci://ghcr.io/kusionstack/opensearch", tag = "0.1.0" } 8 | 9 | [profile] 10 | entries = ["main.k"] 11 | 12 | -------------------------------------------------------------------------------- /example/ai-agent-demo/dev/main.k: -------------------------------------------------------------------------------- 1 | # The configuration codes in perspective of developers. 2 | import kam.v1.app_configuration as ac 3 | import service as svc 4 | import service.secret as sec 5 | import service.container as c 6 | # import opensearch.opensearch as o 7 | 8 | agent: ac.AppConfiguration { 9 | # Declare the workload configurations. 10 | workload: svc.Service { 11 | containers: { 12 | opensearch: c.Container { 13 | image: "kusionstack/ai-agent-demo:v0.1.0" 14 | env: { 15 | # "OPENAI_API_KEY": "secret://ai-agent/OPENAI_API_KEY" 16 | # "RAG_ENABLED": "true" 17 | # "OPEN_SEARCH_AK": "secret://ai-agent/OPEN_SEARCH_AK" 18 | # "OPEN_SEARCH_SK": "secret://ai-agent/OPEN_SEARCH_SK" 19 | } 20 | } 21 | } 22 | # Secrets used to retrieve secret data from AWS Secrets Manager 23 | # secrets: { 24 | # "ai-agent": sec.Secret { 25 | # type: "external" 26 | # data: { 27 | # # replace all references with your own tokens 28 | # # "OPENAI_API_KEY": "ref://ai-agent/OPENAI_API_KEY" 29 | # # "OPEN_SEARCH_AK": "ref://ai-agent/OPEN_SEARCH_AK" 30 | # # "OPEN_SEARCH_SK": "ref://ai-agent/OPEN_SEARCH_SK" 31 | # } 32 | # } 33 | # } 34 | } 35 | # Declare the openSearch module configurations. 36 | accessories: { 37 | # "opensearch": o.OpenSearch { 38 | # domainName: "ai-agent-demo" 39 | # engineVersion: "OpenSearch_2.13" 40 | # } 41 | } 42 | } -------------------------------------------------------------------------------- /example/ai-agent-demo/dev/stack.yaml: -------------------------------------------------------------------------------- 1 | name: dev 2 | -------------------------------------------------------------------------------- /example/ai-agent-demo/project.yaml: -------------------------------------------------------------------------------- 1 | name: ai-agent-demo 2 | -------------------------------------------------------------------------------- /example/ai-agent-demo/src/demo.py: -------------------------------------------------------------------------------- 1 | # Kusion AI agent RAG demo 2 | 3 | # Import Libraries 4 | # ---------------- 5 | import os 6 | 7 | import boto3 8 | from flask import Flask, request, jsonify 9 | from langchain.chains import ConversationalRetrievalChain 10 | from langchain.chains.conversation.memory import ConversationBufferWindowMemory 11 | from langchain.embeddings.openai import OpenAIEmbeddings 12 | from langchain.text_splitter import RecursiveCharacterTextSplitter 13 | from langchain_community.chat_models import ChatOpenAI 14 | from langchain_community.document_loaders import PyPDFLoader 15 | from langchain_community.vectorstores import OpenSearchVectorSearch 16 | from langchain_core.output_parsers import StrOutputParser 17 | from opensearchpy import OpenSearch, RequestsHttpConnection 18 | from requests_aws4auth import AWS4Auth 19 | 20 | # Constants and API Keys 21 | # ---------------------- 22 | OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') 23 | RAG_ENABLED = os.getenv('RAG_ENABLED') 24 | OPEN_SEARCH_ENDPOINT = os.getenv('OPEN_SEARCH_ENDPOINT') 25 | OPEN_SEARCH_AK = os.getenv('OPEN_SEARCH_AK') 26 | OPEN_SEARCH_SK = os.getenv('OPEN_SEARCH_SK') 27 | OPEN_SEARCH_REGION = os.getenv('OPEN_SEARCH_REGION') 28 | NVIDIA_PDF_PATH = "./nvidia-10q.pdf" 29 | VECTOR_DB_DIRECTORY = "/tmp/vectordb" 30 | GPT_MODEL_NAME = 'gpt-3.5-turbo' 31 | CHUNK_SIZE = 700 32 | CHUNK_OVERLAP = 50 33 | 34 | 35 | # Function Definitions 36 | # -------------------- 37 | def load_and_split_document(pdf_path): 38 | """Loads and splits the document into pages.""" 39 | loader = PyPDFLoader(pdf_path) 40 | return loader.load_and_split() 41 | 42 | 43 | def split_text_into_chunks(pages, chunk_size, chunk_overlap): 44 | """Splits text into smaller chunks for processing.""" 45 | text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap) 46 | return text_splitter.split_documents(pages) 47 | 48 | 49 | def create_embeddings(api_key): 50 | """Creates embeddings from text.""" 51 | return OpenAIEmbeddings(openai_api_key=api_key) 52 | 53 | 54 | def setup_awsauth(): 55 | service = "es" # must set the service as 'es' 56 | region = OPEN_SEARCH_REGION 57 | ak = OPEN_SEARCH_AK 58 | sk = OPEN_SEARCH_SK 59 | credentials = boto3.Session( 60 | aws_access_key_id=ak, aws_secret_access_key=sk 61 | ).get_credentials() 62 | awsauth = AWS4Auth(ak, sk, region, service, session_token=credentials.token) 63 | return awsauth 64 | 65 | 66 | def setup_client(awsauth, host_url): 67 | client = OpenSearch( 68 | hosts=[{'host': host_url, 'port': 443}], 69 | http_auth=awsauth, 70 | use_ssl=True, 71 | verify_certs=True, 72 | connection_class=RequestsHttpConnection, 73 | timeout=300 74 | ) 75 | return client 76 | 77 | 78 | def check_index_exists(client, index_name): 79 | if client.indices.exists(index=index_name): 80 | print("index exists") 81 | client.indices.delete(index=index_name) 82 | return True 83 | else: 84 | print("index does not exist") 85 | return False 86 | 87 | 88 | def setup_vector_database(awsauth, documents, embeddings, host_url): 89 | """Sets up a vector database for storing embeddings.""" 90 | service = "es" # must set the service as 'es' 91 | region = OPEN_SEARCH_REGION 92 | ak = OPEN_SEARCH_AK 93 | sk = OPEN_SEARCH_SK 94 | credentials = boto3.Session( 95 | aws_access_key_id=ak, aws_secret_access_key=sk 96 | ).get_credentials() 97 | awsauth = AWS4Auth(ak, sk, region, service, session_token=credentials.token) 98 | return OpenSearchVectorSearch.from_documents( 99 | documents, 100 | embeddings, 101 | opensearch_url=host_url, 102 | http_auth=awsauth, 103 | timeout=300, 104 | use_ssl=True, 105 | verify_certs=True, 106 | connection_class=RequestsHttpConnection, 107 | index_name="test-index", 108 | ) 109 | 110 | 111 | def initialize_chat_model(api_key, model_name): 112 | """Initializes the chat model with specified AI model.""" 113 | return ChatOpenAI(openai_api_key=api_key, model_name=model_name, temperature=0.0) 114 | 115 | 116 | def create_retrieval_qa_chain(chat_model, vector_database): 117 | """Creates a retrieval QA chain combining model and database.""" 118 | memory = ConversationBufferWindowMemory(memory_key='chat_history', k=5, return_messages=True) 119 | return ConversationalRetrievalChain.from_llm( 120 | chat_model, 121 | retriever=vector_database.as_retriever(), 122 | verbose=True, 123 | # return_generated_question=True, 124 | # return_source_documents=True, 125 | memory=memory) 126 | 127 | 128 | def create_regular_qa_chain(chat_model): 129 | """Creates a regular QA chain combining model and database.""" 130 | output_parser = StrOutputParser() 131 | regular_chain = chat_model | output_parser 132 | return regular_chain 133 | 134 | 135 | def ask_question_and_get_answer(qa_chain, question): 136 | """Asks a question and retrieves the answer.""" 137 | return qa_chain({"question": question})['answer'] 138 | 139 | 140 | # Main Execution Flow 141 | # ------------------- 142 | 143 | app = Flask(__name__) 144 | 145 | 146 | @app.route('/ask', methods=['POST']) 147 | def ask(): 148 | try: 149 | question = request.json.get('question') 150 | if not question: 151 | return jsonify({"error": "No question provided"}), 400 152 | if RAG_ENABLED == "true": 153 | response = ask_question_and_get_answer(qa_chain, question) 154 | else: 155 | response = regular_chain.invoke(question) 156 | return jsonify({"answer": response}), 200 157 | except Exception as e: 158 | return jsonify({"error": str(e)}), 500 159 | 160 | 161 | if __name__ == '__main__': 162 | """Main function to execute the RAG workflow.""" 163 | chat_model = initialize_chat_model(OPENAI_API_KEY, GPT_MODEL_NAME) 164 | regular_chain = create_regular_qa_chain(chat_model) 165 | 166 | if RAG_ENABLED == "true": 167 | print("RAG is enabled") 168 | pages = load_and_split_document(NVIDIA_PDF_PATH) 169 | documents = split_text_into_chunks(pages, CHUNK_SIZE, CHUNK_OVERLAP) 170 | embeddings = create_embeddings(OPENAI_API_KEY) 171 | host = OPEN_SEARCH_ENDPOINT 172 | host_url = "https://" + host 173 | indexName = "test-index" 174 | 175 | awsauth = setup_awsauth() 176 | client = setup_client(awsauth, host) 177 | index_exists = check_index_exists(client, indexName) 178 | if not index_exists: 179 | vector_database = setup_vector_database(awsauth, documents, embeddings, host_url) 180 | print("index created") 181 | else: 182 | print("index exists, returning vector database object") 183 | vector_database = OpenSearchVectorSearch( 184 | host_url, 185 | indexName, 186 | embeddings, 187 | http_auth=awsauth, 188 | timeout=300, 189 | use_ssl=True, 190 | verify_certs=True, 191 | connection_class=RequestsHttpConnection, 192 | ) 193 | qa_chain = create_retrieval_qa_chain(chat_model, vector_database) 194 | else: 195 | print("RAG is disabled") 196 | app.run(host='0.0.0.0', port=8888) -------------------------------------------------------------------------------- /example/ai-agent-demo/src/nvidia-10q.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KusionStack/konfig/61408d4cb33e9c8f8008e9907d29bbd63ef9c552/example/ai-agent-demo/src/nvidia-10q.pdf -------------------------------------------------------------------------------- /example/nginx/dev/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nginx" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 8 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } 9 | 10 | [profile] 11 | entries = ["main.k"] 12 | -------------------------------------------------------------------------------- /example/nginx/dev/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | import network as n 5 | 6 | nginx: ac.AppConfiguration { 7 | workload: service.Service { 8 | containers: { 9 | wordpress: c.Container { 10 | image = "nginx:1.25.2" 11 | resources: { 12 | "cpu": "500m" 13 | "memory": "512Mi" 14 | } 15 | } 16 | } 17 | replicas: 1 18 | } 19 | accessories: { 20 | "network": n.Network { 21 | ports: [ 22 | n.Port { 23 | port: 80 24 | protocol: "TCP" 25 | } 26 | ] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /example/nginx/dev/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: dev 3 | -------------------------------------------------------------------------------- /example/nginx/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info 2 | name: nginx 3 | -------------------------------------------------------------------------------- /example/quickstart/default/kcl.mod: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | kam = { git = "git://github.com/KusionStack/kam", tag = "0.2.2" } 3 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.3.0" } 4 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.2.1" } 5 | -------------------------------------------------------------------------------- /example/quickstart/default/main.k: -------------------------------------------------------------------------------- 1 | # The configuration codes in perspective of developers. 2 | import kam.v1.app_configuration as ac 3 | import service 4 | import service.container as c 5 | import network as n 6 | 7 | # `main.k` declares the customized configuration codes for default stack. 8 | # 9 | # Please replace the ${APPLICATION_NAME} with the name of your application, and complete the 10 | # 'AppConfiguration' instance with your own workload and accessories. 11 | quickstart: ac.AppConfiguration { 12 | workload: service.Service { 13 | containers: { 14 | quickstart: c.Container { 15 | image: "kusionstack/kusion-quickstart:latest" 16 | } 17 | } 18 | } 19 | accessories: { 20 | "network": n.Network { 21 | ports: [ 22 | n.Port { 23 | port: 8080 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/quickstart/default/stack.yaml: -------------------------------------------------------------------------------- 1 | # The metadata information of the stack. 2 | name: default 3 | -------------------------------------------------------------------------------- /example/quickstart/default/workspace.yaml: -------------------------------------------------------------------------------- 1 | modules: 2 | kam: 3 | path: git://github.com/KusionStack/kam 4 | version: 0.2.2 5 | configs: 6 | default: {} 7 | network: 8 | path: oci://ghcr.io/kusionstack/network 9 | version: 0.3.0 10 | configs: 11 | default: {} 12 | service: 13 | path: oci://ghcr.io/kusionstack/service 14 | version: 0.2.1 15 | configs: 16 | default: {} 17 | context: 18 | KUBECONFIG_PATH: /path/to/kubeconfig 19 | -------------------------------------------------------------------------------- /example/quickstart/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info. 2 | name: quickstart 3 | -------------------------------------------------------------------------------- /example/service-multi-stack/base/base.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | import network as n 5 | 6 | echoserver: ac.AppConfiguration { 7 | workload: service.Service { 8 | containers: { 9 | "server": c.Container { 10 | image = "" 11 | resources: { 12 | "cpu": "250m" 13 | "memory": "256Mi" 14 | } 15 | } 16 | } 17 | replicas: 1 18 | } 19 | accessories: { 20 | "network": n.Network { 21 | ports: [ 22 | n.Port { 23 | port: 80 24 | } 25 | ] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /example/service-multi-stack/dev/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-multi-stack-dev" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 8 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } 9 | 10 | [profile] 11 | entries = ["../base/base.k", "main.k"] 12 | 13 | -------------------------------------------------------------------------------- /example/service-multi-stack/dev/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | 5 | # main.k declares customized configurations for dev stack. 6 | echoserver: ac.AppConfiguration { 7 | workload: service.Service { 8 | containers: { 9 | "server": c.Container { 10 | # dev stack use latest echoserver image 11 | image = "cilium/echoserver" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /example/service-multi-stack/dev/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: dev -------------------------------------------------------------------------------- /example/service-multi-stack/prod/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-multi-stack-prod" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 8 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } 9 | 10 | [profile] 11 | entries = ["../base/base.k", "main.k"] 12 | 13 | -------------------------------------------------------------------------------- /example/service-multi-stack/prod/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | 5 | # main.k declares customized configurations for prod stack. 6 | echoserver: ac.AppConfiguration { 7 | workload: service.Service { 8 | containers: { 9 | "server": c.Container { 10 | # prod stack use older stable echoserver image 11 | image = "cilium/echoserver:1.10.3" 12 | # also prod stack require more resources 13 | resources: { 14 | "cpu" = "500m" 15 | "memory" = "512Mi" 16 | } 17 | } 18 | } 19 | # replicas is 2 for prod 20 | replicas = 2 21 | } 22 | } -------------------------------------------------------------------------------- /example/service-multi-stack/prod/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: prod -------------------------------------------------------------------------------- /example/service-multi-stack/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info 2 | name: service-multi-stack 3 | -------------------------------------------------------------------------------- /example/simple-job/dev/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-job" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | job = { oci = "oci://ghcr.io/kusionstack/job", tag = "0.1.0" } 8 | 9 | [profile] 10 | entries = ["main.k"] 11 | 12 | -------------------------------------------------------------------------------- /example/simple-job/dev/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import job 3 | import job.container as c 4 | 5 | helloworld: ac.AppConfiguration { 6 | workload: job.Job { 7 | containers: { 8 | "busybox": c.Container { 9 | # The target image 10 | image: "busybox:1.28" 11 | # Run the following command as defined 12 | command: ["/bin/sh", "-c", "echo hello"] 13 | } 14 | } 15 | # Run every hour. 16 | schedule: "0 * * * *" 17 | } 18 | } -------------------------------------------------------------------------------- /example/simple-job/dev/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: dev -------------------------------------------------------------------------------- /example/simple-job/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info 2 | name: simple-job 3 | -------------------------------------------------------------------------------- /example/simple-service/dev/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-service" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 8 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } 9 | opsrule = { oci = "oci://ghcr.io/kusionstack/opsrule", tag = "0.2.0" } 10 | monitoring = { oci = "oci://ghcr.io/kusionstack/monitoring", tag = "0.2.0" } 11 | 12 | [profile] 13 | entries = ["main.k"] 14 | 15 | -------------------------------------------------------------------------------- /example/simple-service/dev/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | import service.container.probe as p 5 | import monitoring as m 6 | import opsrule as o 7 | import network as n 8 | 9 | helloworld: ac.AppConfiguration { 10 | workload: service.Service { 11 | containers: { 12 | "helloworld": c.Container { 13 | image: "gcr.io/google-samples/gb-frontend:v4" 14 | env: { 15 | "env1": "VALUE" 16 | "env2": "VALUE2" 17 | } 18 | resources: { 19 | "cpu": "500m" 20 | "memory": "512M" 21 | } 22 | # Configure an HTTP readiness probe 23 | readinessProbe: p.Probe { 24 | probeHandler: p.Http { 25 | url: "http://localhost:80" 26 | } 27 | initialDelaySeconds: 10 28 | } 29 | } 30 | } 31 | replicas: 2 32 | } 33 | accessories: { 34 | "network": n.Network { 35 | ports: [ 36 | n.Port { 37 | port: 8080 38 | targetPort: 80 39 | } 40 | ] 41 | } 42 | "monitoring": m.Prometheus { 43 | path: "/metrics" 44 | } 45 | "opsRule": o.OpsRule { 46 | maxUnavailable: "30%" 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /example/simple-service/dev/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: dev -------------------------------------------------------------------------------- /example/simple-service/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info 2 | name: simple-service 3 | generator: 4 | type: AppConfiguration -------------------------------------------------------------------------------- /example/wordpress-cloud-rds/prod/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wordpress-cloud-rds" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 8 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } 9 | mysql = { oci = "oci://ghcr.io/kusionstack/mysql", tag = "0.2.0" } 10 | 11 | [profile] 12 | entries = ["main.k"] 13 | 14 | -------------------------------------------------------------------------------- /example/wordpress-cloud-rds/prod/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | import network as n 5 | import mysql as m 6 | 7 | # main.k declares customized configurations for prod stacks. 8 | wordpress: ac.AppConfiguration { 9 | workload: service.Service { 10 | containers: { 11 | wordpress: c.Container { 12 | image: "wordpress:6.3" 13 | env: { 14 | "WORDPRESS_DB_HOST": "$(KUSION_DB_HOST_WORDPRESS_MYSQL)" 15 | "WORDPRESS_DB_USER": "$(KUSION_DB_USERNAME_WORDPRESS_MYSQL)" 16 | "WORDPRESS_DB_PASSWORD": "$(KUSION_DB_PASSWORD_WORDPRESS_MYSQL)" 17 | "WORDPRESS_DB_NAME": "mysql" 18 | } 19 | resources: { 20 | "cpu": "500m" 21 | "memory": "512Mi" 22 | } 23 | } 24 | } 25 | replicas: 1 26 | } 27 | accessories: { 28 | "network": n.Network { 29 | ports: [ 30 | n.Port { 31 | port: 80 32 | } 33 | ] 34 | } 35 | "mysql": m.MySQL { 36 | type: "cloud" 37 | version: "8.0" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/wordpress-cloud-rds/prod/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: prod 3 | -------------------------------------------------------------------------------- /example/wordpress-cloud-rds/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info 2 | name: wordpress-cloud-rds -------------------------------------------------------------------------------- /example/wordpress-local-db/prod/kcl.mod: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wordpress-local-db" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } 7 | service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } 8 | network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } 9 | mysql = { oci = "oci://ghcr.io/kusionstack/mysql", tag = "0.2.0" } 10 | 11 | [profile] 12 | entries = ["main.k"] 13 | 14 | -------------------------------------------------------------------------------- /example/wordpress-local-db/prod/main.k: -------------------------------------------------------------------------------- 1 | import kam.v1.app_configuration as ac 2 | import service 3 | import service.container as c 4 | import network as n 5 | import mysql as m 6 | 7 | # main.k declares customized configurations for prod stack. 8 | wordpress: ac.AppConfiguration { 9 | workload: service.Service { 10 | containers: { 11 | wordpress: c.Container { 12 | image: "wordpress:6.3" 13 | env: { 14 | "WORDPRESS_DB_HOST": "$(KUSION_DB_HOST_WORDPRESS_MYSQL)" 15 | "WORDPRESS_DB_USER": "$(KUSION_DB_USERNAME_WORDPRESS_MYSQL)" 16 | "WORDPRESS_DB_PASSWORD": "$(KUSION_DB_PASSWORD_WORDPRESS_MYSQL)" 17 | "WORDPRESS_DB_NAME": "mysql" 18 | } 19 | } 20 | } 21 | replicas: 1 22 | } 23 | accessories: { 24 | "network": n.Network { 25 | ports: [ 26 | n.Port { 27 | port: 80 28 | } 29 | ] 30 | } 31 | "mysql": m.MySQL { 32 | type: "local" 33 | version: "8.0" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/wordpress-local-db/prod/stack.yaml: -------------------------------------------------------------------------------- 1 | # The stack basic info 2 | name: prod 3 | -------------------------------------------------------------------------------- /example/wordpress-local-db/project.yaml: -------------------------------------------------------------------------------- 1 | # The project basic info 2 | name: wordpress-local-db 3 | generator: 4 | type: AppConfiguration 5 | -------------------------------------------------------------------------------- /hack/README.md: -------------------------------------------------------------------------------- 1 | # Konfig hack GuideLines 2 | 3 | This document describes how you can use the scripts from [`hack`](.) directory 4 | and gives a brief introduction and explanation of these scripts. 5 | 6 | ## Overview 7 | 8 | The [`hack`](.) directory contains many scripts that ensure continuous development of Konfig, 9 | enhance the robustness of the code, improve development efficiency, etc. 10 | The explanations and descriptions of these scripts are helpful for contributors. 11 | 12 | -------------------------------------------------------------------------------- /hack/apply_changed_stacks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from pathlib import Path 4 | from typing import List 5 | import zipfile 6 | from lib.common import * 7 | from lib import util 8 | 9 | 10 | report_dir = os.path.dirname(os.path.abspath(__file__)) + "/report" 11 | os.makedirs(report_dir, exist_ok=True) 12 | result_files = [] 13 | result_pack_path = report_dir + "/apply-result.zip" 14 | 15 | 16 | # If succeeded, return True; if skipped, return False; if failed, exception 17 | def apply_stacks(stack_dirs: List[str]) -> bool: 18 | stacks = [] 19 | for stack_dir in stack_dirs: 20 | if not stack_dir: 21 | continue 22 | if util.should_ignore_stack(stack_dir): 23 | print(f"Ignore stack {stack_dir}.") 24 | continue 25 | stacks.append(stack_dir) 26 | if len(stacks) == 0: 27 | print(f"ignored or no changed stacks, skip apply") 28 | return False 29 | 30 | for stack in stacks: 31 | apply(stack) 32 | return True 33 | 34 | 35 | def apply(stack_dir: str): 36 | print(f"Apply stack {stack_dir}...") 37 | cmd = [KUSION_CMD, APPLY_CMD, YES_FLAG, NO_STYLE_FLAG, NO_WATCH_FLAG] 38 | process = subprocess.run( 39 | cmd, capture_output=True, cwd=Path(stack_dir), env=dict(os.environ) 40 | ) 41 | if process.returncode == 0: 42 | write_to_result_file(process.stdout, stack_dir) 43 | else: 44 | raise Exception(f"apply stack {stack_dir} failed", 45 | f"stdout = {process.stdout.decode().strip()}", 46 | f"stderr = {process.stderr.decode().strip()}", 47 | f"returncode = {process.returncode}") 48 | 49 | 50 | def write_to_result_file(content: bytes, stack: str): 51 | result_file_path = report_dir + "/apply-result-" + stack.replace("/", "_") 52 | with open(result_file_path, 'w') as file: 53 | file.write(content.decode()) 54 | result_files.append(result_file_path) 55 | 56 | 57 | def pack_result_files(): 58 | with zipfile.ZipFile(result_pack_path, 'w') as zipf: 59 | for file in result_files: 60 | arc_name = os.path.basename(file) 61 | zipf.write(file, arcname=arc_name) 62 | 63 | 64 | stack_dirs = util.get_changed_stacks() 65 | # util.create_workspaces(stack_dirs) 66 | success = apply_stacks(stack_dirs) 67 | if success: 68 | pack_result_files() 69 | with open(os.environ[GITHUB_OUTPUT], 'a') as fh: 70 | print(f'apply_success={str(success).lower()}', file=fh) 71 | -------------------------------------------------------------------------------- /hack/check_structure.py: -------------------------------------------------------------------------------- 1 | """ 2 | The test items for structure verification are as follows: 3 | - in project.yaml:name is required; 4 | - in stack.yaml:name is required 5 | """ 6 | from pathlib import Path 7 | import pytest 8 | from lib.common import * 9 | from lib import util 10 | 11 | 12 | def check_project_meta(project_dir: Path): 13 | yaml_content = util.read_to_yaml(str(project_dir / PROJECT_FILE)) 14 | assert ( 15 | yaml_content.get(NAME) is not None 16 | ), "file structure error: invalid project meta: project name undefined" 17 | 18 | 19 | def check_stack_meta(stack_dir: Path): 20 | yaml_content = util.read_to_yaml(str(stack_dir / STACK_FILE)) 21 | assert ( 22 | yaml_content.get(NAME) is not None 23 | ), "file structure error: invalid stack meta: stack name undefined" 24 | 25 | 26 | project_dirs = util.get_changed_projects() 27 | stack_dirs = util.get_changed_stacks() 28 | 29 | 30 | @pytest.mark.parametrize("project_dir", project_dirs) 31 | def test_project_structure(project_dir: str): 32 | print(f"Check structure of project {project_dir}") 33 | check_project_meta(Path(project_dir)) 34 | 35 | 36 | @pytest.mark.parametrize("stack_dir", stack_dirs) 37 | def test_stack_structure(stack_dir: str): 38 | print(f"Check structure of stack {stack_dir}") 39 | check_stack_meta(Path(stack_dir)) 40 | -------------------------------------------------------------------------------- /hack/get_changed_projects_and_stacks.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import List 4 | from lib.common import * 5 | from lib import util 6 | 7 | 8 | def split_changed_paths_str(changed_paths_str: str) -> List[str]: 9 | # output "diff" of technote-space/get-diff-action@v6 has '', should remove it 10 | # ref: https://github.com/technote-space/get-diff-action/blob/v6/README.md 11 | return [item[1:-1] for item in changed_paths_str.split(" ") if item] 12 | 13 | 14 | def get_changed_project_paths(changed_paths: List[str]) -> List[str]: 15 | project_paths = [] 16 | check_files = [KCL_FILE_SUFFIX, KCL_MOD_FILE, PROJECT_FILE, STACK_FILE] 17 | for changed_path in changed_paths: 18 | if not changed_path: 19 | continue 20 | # ignore if not .k file or kcl.mod/kcl.mod.lock/stack.yaml/project.yaml 21 | if not exist_suffix(check_files, changed_path): 22 | continue 23 | # if the project has already detected, skip it 24 | if exist_prefix(project_paths, changed_path): 25 | continue 26 | 27 | # find nearest dir contains project.yaml 28 | path = find_nearest_dir_contains_file(changed_path, PROJECT_FILE) 29 | if path: 30 | project_paths.append(path) 31 | return project_paths 32 | 33 | 34 | def get_changed_stack_paths(changed_paths: List[str], project_paths: List[str]) -> List[str]: 35 | stack_paths = [] 36 | check_files = [KCL_FILE_SUFFIX, KCL_MOD_FILE, PROJECT_FILE, STACK_FILE] 37 | for changed_path in changed_paths: 38 | if not changed_path: 39 | continue 40 | # ignore if not .k file or kcl.mod/kcl.mod.lock/stack.yaml/project.yaml 41 | if not exist_suffix(check_files, changed_path): 42 | continue 43 | # if the stack has already detected, skip it 44 | if exist_prefix(stack_paths, changed_path): 45 | continue 46 | # if the changed_path is a project's base/base.k or project.yaml, then all the stacks get changed 47 | is_base_file = False 48 | for project_path in project_paths: 49 | if changed_path == project_path + "/" + BASE_FILE or changed_path == project_path + "/" + PROJECT_FILE: 50 | is_base_file = True 51 | stacks = util.detect_all_stacks(project_path) 52 | stack_paths = append_if(stack_paths, stacks) 53 | break 54 | if is_base_file: 55 | continue 56 | 57 | # find nearest dir contains stack.yaml 58 | path = find_nearest_dir_contains_file(changed_path, STACK_FILE) 59 | if path: 60 | stack_paths.append(path) 61 | return stack_paths 62 | 63 | 64 | def exist_prefix(lists: List[str], s: str) -> bool: 65 | for item in lists: 66 | if s.startswith(item): 67 | return True 68 | return False 69 | 70 | 71 | def exist_suffix(lists: List[str], s: str) -> bool: 72 | for item in lists: 73 | if s.endswith(item): 74 | return True 75 | return False 76 | 77 | 78 | # todo: "/" does not work under windows, should upgrade it 79 | def find_nearest_dir_contains_file(path: str, file: str) -> str: 80 | splits = path.split("/") 81 | for index in range(len(splits) - 1): 82 | path = path.rsplit("/", 1)[0] 83 | if not path: 84 | continue 85 | if exist_file(path, file): 86 | return path 87 | return "" 88 | 89 | 90 | def exist_file(path: str, file_name: str) -> bool: 91 | dir_path = Path(path) 92 | if dir_path.exists(): 93 | for filename in os.listdir(path): 94 | if filename == file_name: 95 | return True 96 | return False 97 | 98 | 99 | def append_if(l1: List[str], l2: List[str]) -> List[str]: 100 | l3 = l1 101 | for item2 in l2: 102 | equal = False 103 | for item1 in l1: 104 | if item2 == item1: 105 | equal = True 106 | break 107 | if not equal: 108 | l3.append(item2) 109 | return l3 110 | 111 | 112 | changed_paths_str = os.getenv(CHANGED_PATHS) 113 | changed_paths = split_changed_paths_str(changed_paths_str) 114 | changed_projects = get_changed_project_paths(changed_paths) 115 | changed_stacks = get_changed_stack_paths(changed_paths, changed_projects) 116 | changed_projects_str = util.merge_list_to_str(changed_projects) 117 | changed_stacks_str = util.merge_list_to_str(changed_stacks) 118 | # "set-output" is getting deprecated, update it 119 | # ref: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ 120 | # https://github.com/orgs/community/discussions/28146 121 | with open(os.environ[GITHUB_OUTPUT], 'a') as fh: 122 | print(f'changed_projects={changed_projects_str}', file=fh) 123 | print(f'changed_stacks={changed_stacks_str}', file=fh) -------------------------------------------------------------------------------- /hack/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KusionStack/konfig/61408d4cb33e9c8f8008e9907d29bbd63ef9c552/hack/lib/__init__.py -------------------------------------------------------------------------------- /hack/lib/ansi2html/converter.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # This file is part of ansi2html 3 | # Convert ANSI (terminal) colours and attributes to HTML 4 | # Copyright (C) 2012 Ralph Bean 5 | # Copyright (C) 2013 Sebastian Pipping 6 | # 7 | # Inspired by and developed off of the work by pixelbeat and blackjack. 8 | # 9 | # This program is free software: you can redistribute it and/or 10 | # modify it under the terms of the GNU General Public License as 11 | # published by the Free Software Foundation, either version 3 of 12 | # the License, or (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see 21 | # . 22 | 23 | import io 24 | import optparse 25 | import re 26 | import sys 27 | 28 | import pkg_resources 29 | 30 | try: 31 | from collections import OrderedDict 32 | except ImportError: 33 | from ordereddict import OrderedDict 34 | 35 | from .style import SCHEME, get_styles 36 | 37 | ANSI_FULL_RESET = 0 38 | ANSI_INTENSITY_INCREASED = 1 39 | ANSI_INTENSITY_REDUCED = 2 40 | ANSI_INTENSITY_NORMAL = 22 41 | ANSI_STYLE_ITALIC = 3 42 | ANSI_STYLE_NORMAL = 23 43 | ANSI_BLINK_SLOW = 5 44 | ANSI_BLINK_FAST = 6 45 | ANSI_BLINK_OFF = 25 46 | ANSI_UNDERLINE_ON = 4 47 | ANSI_UNDERLINE_OFF = 24 48 | ANSI_CROSSED_OUT_ON = 9 49 | ANSI_CROSSED_OUT_OFF = 29 50 | ANSI_VISIBILITY_ON = 28 51 | ANSI_VISIBILITY_OFF = 8 52 | ANSI_FOREGROUND_CUSTOM_MIN = 30 53 | ANSI_FOREGROUND_CUSTOM_MAX = 37 54 | ANSI_FOREGROUND_256 = 38 55 | ANSI_FOREGROUND_DEFAULT = 39 56 | ANSI_BACKGROUND_CUSTOM_MIN = 40 57 | ANSI_BACKGROUND_CUSTOM_MAX = 47 58 | ANSI_BACKGROUND_256 = 48 59 | ANSI_BACKGROUND_DEFAULT = 49 60 | ANSI_NEGATIVE_ON = 7 61 | ANSI_NEGATIVE_OFF = 27 62 | ANSI_FOREGROUND_HIGH_INTENSITY_MIN = 90 63 | ANSI_FOREGROUND_HIGH_INTENSITY_MAX = 97 64 | ANSI_BACKGROUND_HIGH_INTENSITY_MIN = 100 65 | ANSI_BACKGROUND_HIGH_INTENSITY_MAX = 107 66 | 67 | VT100_BOX_CODES = { 68 | "0x71": "─", 69 | "0x74": "├", 70 | "0x75": "┤", 71 | "0x76": "┴", 72 | "0x77": "┬", 73 | "0x78": "│", 74 | "0x6a": "┘", 75 | "0x6b": "┐", 76 | "0x6c": "┌", 77 | "0x6d": "└", 78 | "0x6e": "┼", 79 | } 80 | 81 | # http://stackoverflow.com/a/15190498 82 | _latex_template = """\\documentclass{scrartcl} 83 | \\usepackage[utf8]{inputenc} 84 | \\usepackage{fancyvrb} 85 | \\usepackage[usenames,dvipsnames]{xcolor} 86 | %% \\definecolor{red-sd}{HTML}{7ed2d2} 87 | 88 | \\title{%(title)s} 89 | 90 | \\fvset{commandchars=\\\\\\{\\}} 91 | 92 | \\begin{document} 93 | 94 | \\begin{Verbatim} 95 | %(content)s 96 | \\end{Verbatim} 97 | \\end{document} 98 | """ 99 | 100 | _html_template = """ 101 | 102 | 103 | 104 | %(title)s 105 | 106 | 107 | 108 |
109 | %(content)s
110 | 
111 | 112 | 113 | 114 | """ 115 | 116 | 117 | class _State: 118 | def __init__(self): 119 | self.reset() 120 | 121 | def reset(self): 122 | self.intensity = ANSI_INTENSITY_NORMAL 123 | self.style = ANSI_STYLE_NORMAL 124 | self.blink = ANSI_BLINK_OFF 125 | self.underline = ANSI_UNDERLINE_OFF 126 | self.crossedout = ANSI_CROSSED_OUT_OFF 127 | self.visibility = ANSI_VISIBILITY_ON 128 | self.foreground = (ANSI_FOREGROUND_DEFAULT, None) 129 | self.background = (ANSI_BACKGROUND_DEFAULT, None) 130 | self.negative = ANSI_NEGATIVE_OFF 131 | 132 | def adjust(self, ansi_code, parameter=None): 133 | if ansi_code in ( 134 | ANSI_INTENSITY_INCREASED, 135 | ANSI_INTENSITY_REDUCED, 136 | ANSI_INTENSITY_NORMAL, 137 | ): 138 | self.intensity = ansi_code 139 | elif ansi_code in (ANSI_STYLE_ITALIC, ANSI_STYLE_NORMAL): 140 | self.style = ansi_code 141 | elif ansi_code in (ANSI_BLINK_SLOW, ANSI_BLINK_FAST, ANSI_BLINK_OFF): 142 | self.blink = ansi_code 143 | elif ansi_code in (ANSI_UNDERLINE_ON, ANSI_UNDERLINE_OFF): 144 | self.underline = ansi_code 145 | elif ansi_code in (ANSI_CROSSED_OUT_ON, ANSI_CROSSED_OUT_OFF): 146 | self.crossedout = ansi_code 147 | elif ansi_code in (ANSI_VISIBILITY_ON, ANSI_VISIBILITY_OFF): 148 | self.visibility = ansi_code 149 | elif ANSI_FOREGROUND_CUSTOM_MIN <= ansi_code <= ANSI_FOREGROUND_CUSTOM_MAX: 150 | self.foreground = (ansi_code, None) 151 | elif ( 152 | ANSI_FOREGROUND_HIGH_INTENSITY_MIN 153 | <= ansi_code 154 | <= ANSI_FOREGROUND_HIGH_INTENSITY_MAX 155 | ): 156 | self.foreground = (ansi_code, None) 157 | elif ansi_code == ANSI_FOREGROUND_256: 158 | self.foreground = (ansi_code, parameter) 159 | elif ansi_code == ANSI_FOREGROUND_DEFAULT: 160 | self.foreground = (ansi_code, None) 161 | elif ANSI_BACKGROUND_CUSTOM_MIN <= ansi_code <= ANSI_BACKGROUND_CUSTOM_MAX: 162 | self.background = (ansi_code, None) 163 | elif ( 164 | ANSI_BACKGROUND_HIGH_INTENSITY_MIN 165 | <= ansi_code 166 | <= ANSI_BACKGROUND_HIGH_INTENSITY_MAX 167 | ): 168 | self.background = (ansi_code, None) 169 | elif ansi_code == ANSI_BACKGROUND_256: 170 | self.background = (ansi_code, parameter) 171 | elif ansi_code == ANSI_BACKGROUND_DEFAULT: 172 | self.background = (ansi_code, None) 173 | elif ansi_code in (ANSI_NEGATIVE_ON, ANSI_NEGATIVE_OFF): 174 | self.negative = ansi_code 175 | 176 | def to_css_classes(self): 177 | css_classes = [] 178 | 179 | def append_unless_default(output, value, default): 180 | if value != default: 181 | css_class = "ansi%d" % value 182 | output.append(css_class) 183 | 184 | def append_color_unless_default( 185 | output, color, default, negative, neg_css_class 186 | ): 187 | value, parameter = color 188 | if value != default: 189 | prefix = "inv" if negative else "ansi" 190 | css_class_index = ( 191 | str(value) if (parameter is None) else "%d-%d" % (value, parameter) 192 | ) 193 | output.append(prefix + css_class_index) 194 | elif negative: 195 | output.append(neg_css_class) 196 | 197 | append_unless_default(css_classes, self.intensity, ANSI_INTENSITY_NORMAL) 198 | append_unless_default(css_classes, self.style, ANSI_STYLE_NORMAL) 199 | append_unless_default(css_classes, self.blink, ANSI_BLINK_OFF) 200 | append_unless_default(css_classes, self.underline, ANSI_UNDERLINE_OFF) 201 | append_unless_default(css_classes, self.crossedout, ANSI_CROSSED_OUT_OFF) 202 | append_unless_default(css_classes, self.visibility, ANSI_VISIBILITY_ON) 203 | 204 | flip_fore_and_background = self.negative == ANSI_NEGATIVE_ON 205 | append_color_unless_default( 206 | css_classes, 207 | self.foreground, 208 | ANSI_FOREGROUND_DEFAULT, 209 | flip_fore_and_background, 210 | "inv_background", 211 | ) 212 | append_color_unless_default( 213 | css_classes, 214 | self.background, 215 | ANSI_BACKGROUND_DEFAULT, 216 | flip_fore_and_background, 217 | "inv_foreground", 218 | ) 219 | 220 | return css_classes 221 | 222 | 223 | def linkify(line, latex_mode): 224 | url_matcher = re.compile( 225 | r"(((((https?|ftps?|gopher|telnet|nntp)://)|" 226 | r"(mailto:|news:))(%[0-9A-Fa-f]{2}|[-()_.!~*" 227 | r"\';/?:@&=+$,A-Za-z0-9])+)([).!\';/?:,][\s])?)" 228 | ) 229 | if latex_mode: 230 | return url_matcher.sub(r"\\url{\1}", line) 231 | return url_matcher.sub(r'\1', line) 232 | 233 | 234 | def map_vt100_box_code(char): 235 | char_hex = hex(ord(char)) 236 | return VT100_BOX_CODES[char_hex] if char_hex in VT100_BOX_CODES else char 237 | 238 | 239 | def _needs_extra_newline(text): 240 | if not text or text.endswith("\n"): 241 | return False 242 | return True 243 | 244 | 245 | class CursorMoveUp: 246 | pass 247 | 248 | 249 | class Ansi2HTMLConverter: 250 | """Convert Ansi color codes to CSS+HTML 251 | 252 | Example: 253 | >>> conv = Ansi2HTMLConverter() 254 | >>> ansi = " ".join(sys.stdin.readlines()) 255 | >>> html = conv.convert(ansi) 256 | """ 257 | 258 | def __init__( 259 | self, 260 | latex=False, 261 | inline=False, 262 | dark_bg=True, 263 | line_wrap=True, 264 | font_size="normal", 265 | linkify=False, 266 | escaped=True, 267 | markup_lines=False, 268 | output_encoding="utf-8", 269 | scheme="ansi2html", 270 | title="", 271 | ): 272 | 273 | self.latex = latex 274 | self.inline = inline 275 | self.dark_bg = dark_bg 276 | self.line_wrap = line_wrap 277 | self.font_size = font_size 278 | self.linkify = linkify 279 | self.escaped = escaped 280 | self.markup_lines = markup_lines 281 | self.output_encoding = output_encoding 282 | self.scheme = scheme 283 | self.title = title 284 | self._attrs = None 285 | 286 | if inline: 287 | self.styles = dict( 288 | [ 289 | (item.klass.strip("."), item) 290 | for item in get_styles(self.dark_bg, self.line_wrap, self.scheme) 291 | ] 292 | ) 293 | 294 | self.vt100_box_codes_prog = re.compile("\033\\(([B0])") 295 | self.ansi_codes_prog = re.compile("\033\\[" "([\\d;]*)" "([a-zA-z])") 296 | 297 | def apply_regex(self, ansi): 298 | styles_used = set() 299 | parts = self._apply_regex(ansi, styles_used) 300 | parts = self._collapse_cursor(parts) 301 | parts = list(parts) 302 | 303 | if self.linkify: 304 | parts = [linkify(part, self.latex) for part in parts] 305 | 306 | combined = "".join(parts) 307 | 308 | if self.markup_lines and not self.latex: 309 | combined = "\n".join( 310 | [ 311 | """%s""" % (i, line) 312 | for i, line in enumerate(combined.split("\n")) 313 | ] 314 | ) 315 | 316 | return combined, styles_used 317 | 318 | def _apply_regex(self, ansi, styles_used): 319 | if self.escaped: 320 | if ( 321 | self.latex 322 | ): # Known Perl function which does this: https://tex.stackexchange.com/questions/34580/escape-character-in-latex/119383#119383 323 | specials = OrderedDict([]) 324 | else: 325 | specials = OrderedDict( 326 | [ 327 | ("&", "&"), 328 | ("<", "<"), 329 | (">", ">"), 330 | ] 331 | ) 332 | for pattern, special in specials.items(): 333 | ansi = ansi.replace(pattern, special) 334 | 335 | def _vt100_box_drawing(): 336 | last_end = 0 # the index of the last end of a code we've seen 337 | box_drawing_mode = False 338 | for match in self.vt100_box_codes_prog.finditer(ansi): 339 | trailer = ansi[last_end : match.start()] 340 | if box_drawing_mode: 341 | for char in trailer: 342 | yield map_vt100_box_code(char) 343 | else: 344 | yield trailer 345 | last_end = match.end() 346 | box_drawing_mode = match.groups()[0] == "0" 347 | yield ansi[last_end:] 348 | 349 | ansi = "".join(_vt100_box_drawing()) 350 | 351 | state = _State() 352 | inside_span = False 353 | last_end = 0 # the index of the last end of a code we've seen 354 | for match in self.ansi_codes_prog.finditer(ansi): 355 | yield ansi[last_end : match.start()] 356 | last_end = match.end() 357 | 358 | params, command = match.groups() 359 | 360 | if command not in "mMA": 361 | continue 362 | 363 | # Special cursor-moving code. The only supported one. 364 | if command == "A": 365 | yield CursorMoveUp 366 | continue 367 | 368 | try: 369 | params = list(map(int, params.split(";"))) 370 | except ValueError: 371 | params = [ANSI_FULL_RESET] 372 | 373 | # Find latest reset marker 374 | last_null_index = None 375 | skip_after_index = -1 376 | for i, v in enumerate(params): 377 | if i <= skip_after_index: 378 | continue 379 | 380 | if v == ANSI_FULL_RESET: 381 | last_null_index = i 382 | elif v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256): 383 | skip_after_index = i + 2 384 | 385 | # Process reset marker, drop everything before 386 | if last_null_index is not None: 387 | params = params[last_null_index + 1 :] 388 | if inside_span: 389 | inside_span = False 390 | if self.latex: 391 | yield "}" 392 | else: 393 | yield "" 394 | state.reset() 395 | 396 | if not params: 397 | continue 398 | 399 | # Turn codes into CSS classes 400 | skip_after_index = -1 401 | for i, v in enumerate(params): 402 | if i <= skip_after_index: 403 | continue 404 | 405 | if v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256): 406 | try: 407 | parameter = params[i + 2] 408 | except IndexError: 409 | continue 410 | skip_after_index = i + 2 411 | else: 412 | parameter = None 413 | state.adjust(v, parameter=parameter) 414 | 415 | if inside_span: 416 | if self.latex: 417 | yield "}" 418 | else: 419 | yield "" 420 | inside_span = False 421 | 422 | css_classes = state.to_css_classes() 423 | if not css_classes: 424 | continue 425 | styles_used.update(css_classes) 426 | 427 | if self.inline: 428 | if self.latex: 429 | style = [ 430 | self.styles[klass].kwl[0][1] 431 | for klass in css_classes 432 | if self.styles[klass].kwl[0][0] == "color" 433 | ] 434 | yield "\\textcolor[HTML]{%s}{" % style[0] 435 | else: 436 | style = [ 437 | self.styles[klass].kw 438 | for klass in css_classes 439 | if klass in self.styles 440 | ] 441 | yield '' % "; ".join(style) 442 | else: 443 | if self.latex: 444 | yield "\\textcolor{%s}{" % " ".join(css_classes) 445 | else: 446 | yield '' % " ".join(css_classes) 447 | inside_span = True 448 | 449 | yield ansi[last_end:] 450 | if inside_span: 451 | if self.latex: 452 | yield "}" 453 | else: 454 | yield "" 455 | inside_span = False 456 | 457 | def _collapse_cursor(self, parts): 458 | """Act on any CursorMoveUp commands by deleting preceding tokens""" 459 | 460 | final_parts = [] 461 | for part in parts: 462 | 463 | # Throw out empty string tokens ("") 464 | if not part: 465 | continue 466 | 467 | # Go back, deleting every token in the last 'line' 468 | if part == CursorMoveUp: 469 | if final_parts: 470 | final_parts.pop() 471 | 472 | while final_parts and "\n" not in final_parts[-1]: 473 | final_parts.pop() 474 | 475 | continue 476 | 477 | # Otherwise, just pass this token forward 478 | final_parts.append(part) 479 | 480 | return final_parts 481 | 482 | def prepare(self, ansi="", ensure_trailing_newline=False): 483 | """Load the contents of 'ansi' into this object""" 484 | 485 | body, styles = self.apply_regex(ansi) 486 | 487 | if ensure_trailing_newline and _needs_extra_newline(body): 488 | body += "\n" 489 | 490 | self._attrs = { 491 | "dark_bg": self.dark_bg, 492 | "line_wrap": self.line_wrap, 493 | "font_size": self.font_size, 494 | "body": body, 495 | "styles": styles, 496 | } 497 | 498 | return self._attrs 499 | 500 | def attrs(self): 501 | """Prepare attributes for the template""" 502 | if not self._attrs: 503 | raise Exception("Method .prepare not yet called.") 504 | return self._attrs 505 | 506 | def convert(self, ansi, full=True, ensure_trailing_newline=False): 507 | attrs = self.prepare(ansi, ensure_trailing_newline=ensure_trailing_newline) 508 | if not full: 509 | return attrs["body"] 510 | if self.latex: 511 | _template = _latex_template 512 | else: 513 | _template = _html_template 514 | all_styles = get_styles(self.dark_bg, self.line_wrap, self.scheme) 515 | backgrounds = all_styles[:6] 516 | used_styles = filter( 517 | lambda e: e.klass.lstrip(".") in attrs["styles"], all_styles 518 | ) 519 | 520 | return _template % { 521 | "style": "\n".join(list(map(str, backgrounds + list(used_styles)))), 522 | "title": self.title, 523 | "font_size": self.font_size, 524 | "content": attrs["body"], 525 | "output_encoding": self.output_encoding, 526 | } 527 | 528 | def produce_headers(self): 529 | return '\n' % { 530 | "style": "\n".join( 531 | map(str, get_styles(self.dark_bg, self.line_wrap, self.scheme)) 532 | ) 533 | } 534 | 535 | 536 | def main(): 537 | """ 538 | $ ls --color=always | ansi2html > directories.html 539 | $ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html 540 | $ task burndown | ansi2html > burndown.html 541 | """ 542 | 543 | scheme_names = sorted(SCHEME.keys()) 544 | version_str = pkg_resources.get_distribution("ansi2html").version 545 | parser = optparse.OptionParser( 546 | usage=main.__doc__, version="%%prog %s" % version_str 547 | ) 548 | parser.add_option( 549 | "-p", 550 | "--partial", 551 | dest="partial", 552 | default=False, 553 | action="store_true", 554 | help="Process lines as them come in. No headers are produced.", 555 | ) 556 | parser.add_option( 557 | "-L", 558 | "--latex", 559 | dest="latex", 560 | default=False, 561 | action="store_true", 562 | help="Export as LaTeX instead of HTML.", 563 | ) 564 | parser.add_option( 565 | "-i", 566 | "--inline", 567 | dest="inline", 568 | default=False, 569 | action="store_true", 570 | help="Inline style without headers or template.", 571 | ) 572 | parser.add_option( 573 | "-H", 574 | "--headers", 575 | dest="headers", 576 | default=False, 577 | action="store_true", 578 | help="Just produce the