├── .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