├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── image.yml │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bin └── .gitkeep ├── cmd └── main.go ├── csi_gcs_suite_test.go ├── deploy ├── base │ ├── daemonset.yaml │ ├── driver.yaml │ ├── kustomization.yaml │ ├── published-volumes-crd.yaml │ └── rbac.yaml └── overlays │ ├── dev │ ├── kustomization.yaml │ └── uselocalimage.yaml │ ├── stable-gke │ └── kustomization.yaml │ └── stable │ └── kustomization.yaml ├── dev-env.Dockerfile ├── docs ├── .scripts │ ├── 49_inject_stable_version.py │ └── 99_global_refs.py ├── .snippets │ ├── abbrs.txt │ ├── links.txt │ └── refs.txt ├── assets │ ├── css │ │ └── custom.css │ └── images │ │ └── favicon.ico ├── contributing │ ├── authors.md │ └── setup.md ├── csi_compatibility.md ├── dynamic_provisioning.md ├── getting_started.md ├── index.md ├── static_provisioning.md └── troubleshooting.md ├── examples ├── dynamic │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── pvc.yaml │ └── sc.yaml └── static │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── pv.yaml │ ├── pvc.yaml │ └── sc.yaml ├── go.mod ├── go.sum ├── hack └── update-codegen.sh ├── mkdocs.yml ├── pkg ├── apis │ └── published-volume │ │ └── v1beta1 │ │ ├── doc.go │ │ ├── register.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go ├── client │ └── clientset │ │ └── clientset │ │ ├── clientset.go │ │ ├── doc.go │ │ ├── fake │ │ ├── clientset_generated.go │ │ ├── doc.go │ │ └── register.go │ │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ │ └── typed │ │ └── published-volume │ │ └── v1beta1 │ │ ├── doc.go │ │ ├── fake │ │ ├── doc.go │ │ ├── fake_published-volume_client.go │ │ └── fake_publishedvolume.go │ │ ├── generated_expansion.go │ │ ├── published-volume_client.go │ │ └── publishedvolume.go ├── driver │ ├── constants.go │ ├── controller.go │ ├── driver.go │ ├── driver_suite_test.go │ ├── identity.go │ ├── node.go │ └── version.go ├── flags │ ├── flags.go │ ├── flags_suite_test.go │ └── flags_test.go └── util │ ├── common.go │ ├── common_test.go │ └── util_suite_test.go ├── requirements.txt ├── tasks ├── __init__.py ├── build.py ├── codegen.py ├── constants.py ├── docs.py ├── env.py ├── image.py ├── test.py └── utils.py ├── test └── sanity_test.go └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - ofek 3 | custom: 4 | - https://ofek.dev/donate/ 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: monthly 11 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | create: 5 | tags: 6 | - v* 7 | push: 8 | paths: 9 | - docs/** 10 | - .github/workflows/docs.yml 11 | - tasks/docs.py 12 | - mkdocs.yml 13 | - tox.ini 14 | pull_request: 15 | paths: 16 | - docs/** 17 | - .github/workflows/docs.yml 18 | - tasks/docs.py 19 | - mkdocs.yml 20 | - tox.ini 21 | 22 | jobs: 23 | build: 24 | name: Build Docs 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Fetch Tags 30 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 31 | 32 | - name: Set up Python 3.8 33 | uses: actions/setup-python@v4 34 | with: 35 | python-version: "3.8" 36 | 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip setuptools 40 | python -m pip install --upgrade -r requirements.txt 41 | 42 | - name: Build docs 43 | run: | 44 | invoke docs.build 45 | 46 | - uses: actions/upload-artifact@v3 47 | with: 48 | name: Documentation 49 | path: site 50 | 51 | publish: 52 | name: Publish Docs 53 | runs-on: ubuntu-latest 54 | # Only publish tags 55 | if: github.event_name == 'create' && github.event.ref_type == 'tag' 56 | needs: 57 | - build 58 | steps: 59 | - uses: actions/download-artifact@v3 60 | with: 61 | name: Documentation 62 | path: site 63 | 64 | - name: Publish generated content to GitHub Pages 65 | uses: peaceiris/actions-gh-pages@v3 66 | with: 67 | github_token: ${{ secrets.GITHUB_TOKEN }} 68 | publish_dir: ./site 69 | -------------------------------------------------------------------------------- /.github/workflows/image.yml: -------------------------------------------------------------------------------- 1 | name: image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build_driver: 13 | name: Build Driver 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 50 19 | 20 | - name: Fetch Tags 21 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 22 | 23 | - name: Install Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: 1.18.2 27 | 28 | - name: Set up Python 3.8 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: "3.8" 32 | 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip setuptools 36 | python -m pip install --upgrade -r requirements.txt 37 | 38 | - uses: actions/cache@v3 39 | with: 40 | path: ~/go/pkg/mod 41 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 42 | restore-keys: | 43 | ${{ runner.os }}-go- 44 | 45 | - name: Build Driver 46 | run: | 47 | invoke build --release 48 | 49 | - uses: actions/upload-artifact@v3 50 | with: 51 | name: Driver 52 | path: bin 53 | 54 | build_container: 55 | name: Build Docker Container 56 | runs-on: ubuntu-latest 57 | needs: 58 | - build_driver 59 | steps: 60 | - uses: actions/checkout@v3 61 | with: 62 | fetch-depth: 50 63 | 64 | - name: Fetch Tags 65 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 66 | 67 | - name: Install Go 68 | uses: actions/setup-go@v4 69 | with: 70 | go-version: 1.18.2 71 | 72 | - name: Set up Python 3.8 73 | uses: actions/setup-python@v4 74 | with: 75 | python-version: "3.8" 76 | 77 | - name: Install dependencies 78 | run: | 79 | python -m pip install --upgrade pip setuptools 80 | python -m pip install --upgrade -r requirements.txt 81 | 82 | - uses: actions/download-artifact@v3 83 | with: 84 | name: Driver 85 | path: bin 86 | 87 | - name: Make Driver executable 88 | run: "chmod +x bin/*" 89 | 90 | - name: Build Docker Container 91 | run: | 92 | invoke image --release 93 | 94 | - name: Package Container 95 | run: | 96 | TAG=$(git describe --long --tags --match='v*' --dirty) 97 | docker save -o ./container.tar.gz ofekmeister/csi-gcs:$TAG 98 | 99 | - uses: actions/upload-artifact@v3 100 | with: 101 | name: Container 102 | path: ./container.tar.gz 103 | 104 | - name: Login to Docker.io 105 | # Only Publish Images from our repository, not for MR 106 | if: github.event_name == 'push' 107 | run: | 108 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 109 | env: 110 | DOCKER_USERNAME: ${{ secrets.DockerUsername }} 111 | DOCKER_PASSWORD: ${{ secrets.DockerPassword }} 112 | 113 | - name: Publish Docker Container 114 | # Only Publish Images from our repository, not for MR 115 | if: github.event_name == 'push' 116 | run: | 117 | invoke image.deploy 118 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test_unit: 13 | name: Unit Tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.18.2 22 | 23 | - name: Set up Python 3.8 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: "3.8" 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip setuptools 31 | python -m pip install --upgrade -r requirements.txt 32 | 33 | - uses: actions/cache@v3 34 | with: 35 | path: ~/go/pkg/mod 36 | key: ${{ runner.os }}-go-test_unit-${{ hashFiles('**/go.sum') }} 37 | restore-keys: | 38 | ${{ runner.os }}-go-test_unit- 39 | ${{ runner.os }}-go- 40 | 41 | - name: Run Tests 42 | run: | 43 | invoke test.unit 44 | 45 | test_sanity: 46 | name: Test Sanity 47 | runs-on: ubuntu-latest 48 | # Secrets are only available for Push 49 | if: github.event_name == 'push' 50 | steps: 51 | - uses: actions/checkout@v3 52 | 53 | - name: Install Go 54 | uses: actions/setup-go@v4 55 | with: 56 | go-version: 1.18.2 57 | 58 | - name: Set up Python 3.8 59 | uses: actions/setup-python@v4 60 | with: 61 | python-version: "3.8" 62 | 63 | - name: Install dependencies 64 | run: | 65 | python -m pip install --upgrade pip setuptools 66 | python -m pip install --upgrade -r requirements.txt 67 | 68 | - uses: actions/cache@v3 69 | with: 70 | path: ~/go/pkg/mod 71 | key: ${{ runner.os }}-go-test_sanity-${{ hashFiles('**/go.sum') }} 72 | restore-keys: | 73 | ${{ runner.os }}-go-test_sanity- 74 | ${{ runner.os }}-go- 75 | 76 | - name: Install Test Secrets 77 | shell: bash 78 | env: 79 | TEST_SECRETS: ${{ secrets.TestSecrets }} 80 | run: | 81 | echo "$TEST_SECRETS" > test/secret.yaml 82 | 83 | - name: Run Tests 84 | run: | 85 | invoke env -c "invoke test.sanity" 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | __pycache__ 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # IDEs & editors 16 | /.idea 17 | /.vscode 18 | 19 | # MkDocs documentation 20 | /site 21 | 22 | # Python environments 23 | /.tox 24 | .venv/ 25 | 26 | # Test Service Account 27 | /service-account.json 28 | /test/secret.yaml 29 | 30 | # Artifacts 31 | /bin/* 32 | !/bin/.gitkeep 33 | 34 | # go vendor 35 | vendor/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ----- 4 | 5 | Please go [here](https://ofek.dev/csi-gcs/contributing/setup/) 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18.2-alpine3.15 AS build-gcsfuse 2 | 3 | ARG gcsfuse_version 4 | ARG global_ldflags 5 | 6 | RUN apk add --update --no-cache fuse fuse-dev git 7 | 8 | WORKDIR ${GOPATH}/src/github.com/googlecloudplatform/gcsfuse 9 | 10 | # Create Tmp Bin Dir 11 | RUN mkdir /tmp/bin 12 | 13 | # Install gcsfuse using the specified version or commit hash 14 | RUN git clone https://github.com/googlecloudplatform/gcsfuse . && git checkout "v${gcsfuse_version}" 15 | RUN go install ./tools/build_gcsfuse 16 | RUN mkdir /tmp/gcsfuse 17 | RUN build_gcsfuse . /tmp/gcsfuse ${gcsfuse_version} -ldflags "all=${global_ldflags}" -ldflags "-X main.gcsfuseVersion=${gcsfuse_version} ${global_ldflags}" 18 | 19 | FROM alpine:3.15 20 | 21 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 22 | LABEL "org.opencontainers.image.authors"="Ofek Lev " 23 | LABEL "org.opencontainers.image.description"="CSI driver for Google Cloud Storage" 24 | LABEL "org.opencontainers.image.licenses"="Apache-2.0 OR MIT" 25 | LABEL "org.opencontainers.image.source"="https://github.com/ofek/csi-gcs" 26 | LABEL "org.opencontainers.image.title"="csi-gcs" 27 | 28 | RUN apk add --update --no-cache ca-certificates fuse tini && rm -rf /tmp/* 29 | 30 | # Allow non-root users to specify the allow_other or allow_root mount options 31 | RUN echo "user_allow_other" > /etc/fuse.conf 32 | 33 | # Create directories for mounts and temporary key storage 34 | RUN mkdir -p /var/lib/kubelet/pods /tmp/keys 35 | 36 | WORKDIR / 37 | 38 | ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/driver"] 39 | 40 | # Copy the binaries 41 | COPY --from=build-gcsfuse /tmp/gcsfuse/bin/* /usr/local/bin/ 42 | COPY --from=build-gcsfuse /tmp/gcsfuse/sbin/* /sbin/ 43 | COPY bin/driver /usr/local/bin/ 44 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ofek Lev 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ofek Lev 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csi-gcs 2 | 3 | | | | 4 | | --- | --- | 5 | | CI/CD | [![CI - Test](https://github.com/ofek/csi-gcs/actions/workflows/test.yml/badge.svg)](https://github.com/ofek/csi-gcs/actions/workflows/test.yml) [![CI - Image](https://github.com/ofek/csi-gcs/actions/workflows/image.yml/badge.svg)](https://github.com/ofek/csi-gcs/actions/workflows/image.yml) | 6 | | Docs | [![Docs](https://github.com/ofek/csi-gcs/actions/workflows/docs.yml/badge.svg)](https://github.com/ofek/csi-gcs/actions/workflows/docs.yml) | 7 | | Image | [![Docker - Version](https://img.shields.io/docker/v/ofekmeister/csi-gcs.svg?sort=semver)](https://hub.docker.com/r/ofekmeister/csi-gcs) [![Docker - Pulls](https://img.shields.io/docker/pulls/ofekmeister/csi-gcs.svg)](https://hub.docker.com/r/ofekmeister/csi-gcs) | 8 | | Meta | [![License - MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-9400d3.svg)](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social)](https://github.com/sponsors/ofek) | 9 | 10 | ----- 11 | 12 | An easy-to-use, cross-platform, and highly optimized Kubernetes CSI driver for mounting Google Cloud Storage buckets. 13 | 14 | Feel free to read the [documentation](https://ofek.dev/csi-gcs/)! 15 | 16 | ## License 17 | 18 | `csi-gcs` is distributed under the terms of both 19 | 20 | - [Apache License, Version 2.0](https://choosealicense.com/licenses/apache-2.0) 21 | - [MIT License](https://choosealicense.com/licenses/mit) 22 | 23 | at your option. 24 | 25 | ## Disclaimer 26 | 27 | This is not an official Google product. 28 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofek/csi-gcs/c1639b1978fa14dab231f67b4d4cd72d0a6314f9/bin/.gitkeep -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/ofek/csi-gcs/pkg/driver" 10 | "k8s.io/klog" 11 | ) 12 | 13 | var ( 14 | version = "development" 15 | nodeNameFlag = flag.String("node-name", "", "Node identifier") 16 | driverNameFlag = flag.String("driver-name", driver.CSIDriverName, "CSI driver name") 17 | endpointFlag = flag.String("csi-endpoint", "unix:///csi/csi.sock", "CSI endpoint") 18 | versionFlag = flag.Bool("version", false, "Print the version and exit") 19 | deleteOrphanedPods = flag.Bool("delete-orphaned-pods", false, "Delete Orphaned Pods on StartUp") 20 | ) 21 | 22 | func main() { 23 | _ = flag.Set("alsologtostderr", "true") 24 | klog.InitFlags(nil) 25 | setEnvVarFlags() 26 | flag.Parse() 27 | 28 | if *versionFlag { 29 | versionJSON, err := driver.GetVersionJSON() 30 | if err != nil { 31 | klog.Error(err.Error()) 32 | os.Exit(1) 33 | } 34 | fmt.Println(versionJSON) 35 | os.Exit(0) 36 | } 37 | 38 | d, err := driver.NewGCSDriver(*driverNameFlag, *nodeNameFlag, *endpointFlag, version, *deleteOrphanedPods) 39 | if err != nil { 40 | klog.Error(err.Error()) 41 | os.Exit(1) 42 | } 43 | 44 | if err = d.Run(); err != nil { 45 | klog.Error(err.Error()) 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func setEnvVarFlags() { 51 | flagset := flag.CommandLine 52 | 53 | // I wish Golang had sets 54 | set := map[string]string{} 55 | 56 | // https://golang.org/pkg/flag/#FlagSet.Visit 57 | flagset.Visit(func(f *flag.Flag) { 58 | set[f.Name] = "" 59 | }) 60 | 61 | // https://golang.org/pkg/flag/#FlagSet.VisitAll 62 | flagset.VisitAll(func(f *flag.Flag) { 63 | envVar := strings.Replace(strings.ToUpper(f.Name), "-", "_", -1) 64 | 65 | if val := os.Getenv(envVar); val != "" { 66 | if _, defined := set[f.Name]; !defined { 67 | _ = flagset.Set(f.Name, val) 68 | } 69 | } 70 | 71 | // Display it in the help text too 72 | f.Usage = fmt.Sprintf("%s [%s]", f.Usage, envVar) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /csi_gcs_suite_test.go: -------------------------------------------------------------------------------- 1 | package csi_gcs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCsiGcs(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "CsiGcs Suite") 13 | } 14 | -------------------------------------------------------------------------------- /deploy/base/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: csi-gcs 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: csi-gcs 9 | template: 10 | metadata: 11 | labels: 12 | app: csi-gcs 13 | spec: 14 | nodeSelector: 15 | kubernetes.io/os: linux 16 | priorityClassName: system-node-critical 17 | tolerations: 18 | - operator: "Exists" 19 | hostNetwork: true 20 | serviceAccount: csi-gcs 21 | containers: 22 | - name: csi-node-driver-registrar 23 | image: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 24 | imagePullPolicy: Always 25 | args: 26 | - "--v=5" 27 | - "--csi-address=$(ADDRESS)" 28 | - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)" 29 | env: 30 | - name: ADDRESS 31 | value: /csi/csi.sock 32 | - name: DRIVER_REG_SOCK_PATH 33 | value: /var/lib/kubelet/plugins/gcs.csi.ofek.dev/csi.sock 34 | - name: KUBE_NODE_NAME 35 | valueFrom: 36 | fieldRef: 37 | apiVersion: v1 38 | fieldPath: spec.nodeName 39 | volumeMounts: 40 | - name: registration-dir 41 | mountPath: /registration 42 | - name: socket-dir 43 | mountPath: /csi 44 | resources: 45 | limits: 46 | cpu: 1 47 | memory: 1Gi 48 | requests: 49 | cpu: 10m 50 | memory: 20Mi 51 | - name: csi-provisioner 52 | image: quay.io/k8scsi/csi-provisioner:v1.6.0 53 | args: 54 | - "--csi-address=$(ADDRESS)" 55 | - "--extra-create-metadata" 56 | - "--enable-leader-election" 57 | - "--leader-election-namespace=$(NAMESPACE)" 58 | env: 59 | - name: ADDRESS 60 | value: /var/lib/csi/sockets/pluginproxy/csi.sock 61 | - name: NAMESPACE 62 | value: kube-system 63 | imagePullPolicy: "IfNotPresent" 64 | volumeMounts: 65 | - name: socket-dir 66 | mountPath: /var/lib/csi/sockets/pluginproxy/ 67 | - name: csi-resizer 68 | image: quay.io/k8scsi/csi-resizer:v0.2.0 69 | args: 70 | - "--v=5" 71 | - "--csi-address=$(ADDRESS)" 72 | - "--leader-election" 73 | - "--leader-election-namespace=$(NAMESPACE)" 74 | env: 75 | - name: ADDRESS 76 | value: /var/lib/csi/sockets/pluginproxy/csi.sock 77 | - name: NAMESPACE 78 | value: kube-system 79 | imagePullPolicy: "IfNotPresent" 80 | volumeMounts: 81 | - name: socket-dir 82 | mountPath: /var/lib/csi/sockets/pluginproxy/ 83 | resources: 84 | limits: 85 | cpu: 1 86 | memory: 1Gi 87 | requests: 88 | cpu: 10m 89 | memory: 20Mi 90 | - name: csi-gcs 91 | securityContext: 92 | privileged: true 93 | capabilities: 94 | add: ["SYS_ADMIN"] 95 | allowPrivilegeEscalation: true 96 | image: docker.io/ofekmeister/csi-gcs:latest 97 | imagePullPolicy: Always 98 | args: 99 | - "--node-name=$(KUBE_NODE_NAME)" 100 | # https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md 101 | - "--v=5" 102 | - "--delete-orphaned-pods=true" 103 | env: 104 | - name: KUBE_NODE_NAME 105 | valueFrom: 106 | fieldRef: 107 | fieldPath: spec.nodeName 108 | volumeMounts: 109 | - name: fuse-device 110 | mountPath: /dev/fuse 111 | - name: mountpoint-dir 112 | mountPath: /var/lib/kubelet/pods 113 | mountPropagation: Bidirectional 114 | - name: socket-dir 115 | mountPath: /csi 116 | resources: 117 | limits: 118 | cpu: 1 119 | memory: 1Gi 120 | requests: 121 | cpu: 10m 122 | memory: 80Mi 123 | volumes: 124 | - name: fuse-device 125 | hostPath: 126 | path: /dev/fuse 127 | # https://kubernetes-csi.github.io/docs/deploying.html#driver-volume-mounts 128 | - name: socket-dir 129 | hostPath: 130 | path: /var/lib/kubelet/plugins/gcs.csi.ofek.dev 131 | type: DirectoryOrCreate 132 | - name: mountpoint-dir 133 | hostPath: 134 | path: /var/lib/kubelet/pods 135 | type: Directory 136 | - name: registration-dir 137 | hostPath: 138 | path: /var/lib/kubelet/plugins_registry 139 | type: Directory 140 | -------------------------------------------------------------------------------- /deploy/base/driver.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1 2 | kind: CSIDriver 3 | metadata: 4 | name: gcs.csi.ofek.dev 5 | # https://kubernetes-csi.github.io/docs/csi-driver-object.html 6 | spec: 7 | attachRequired: false 8 | podInfoOnMount: true 9 | -------------------------------------------------------------------------------- /deploy/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: kube-system 4 | resources: 5 | - driver.yaml 6 | - published-volumes-crd.yaml 7 | - rbac.yaml 8 | - daemonset.yaml -------------------------------------------------------------------------------- /deploy/base/published-volumes-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: publishedvolumes.gcs.csi.ofek.dev 5 | spec: 6 | group: gcs.csi.ofek.dev 7 | versions: 8 | - name: v1beta1 9 | served: true 10 | storage: true 11 | schema: 12 | openAPIV3Schema: 13 | type: object 14 | required: 15 | - spec 16 | properties: 17 | spec: 18 | type: object 19 | required: 20 | - node 21 | - targetPath 22 | - volumeHandle 23 | - options 24 | - pod 25 | properties: 26 | node: 27 | type: string 28 | targetPath: 29 | type: string 30 | volumeHandle: 31 | type: string 32 | options: 33 | type: object 34 | additionalProperties: 35 | type: string 36 | pod: 37 | type: object 38 | required: 39 | - name 40 | - namespace 41 | properties: 42 | name: 43 | type: string 44 | namespace: 45 | type: string 46 | preserveUnknownFields: false 47 | scope: Cluster 48 | names: 49 | plural: publishedvolumes 50 | singular: publishedvolume 51 | kind: PublishedVolume 52 | 53 | -------------------------------------------------------------------------------- /deploy/base/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: csi-gcs 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: ClusterRole 8 | metadata: 9 | name: csi-gcs-node 10 | rules: 11 | - apiGroups: [""] 12 | resources: ["secrets"] 13 | verbs: ["get", "list"] 14 | - apiGroups: [""] 15 | resources: ["pods"] 16 | verbs: ["delete"] 17 | - apiGroups: [""] 18 | resources: ["nodes"] 19 | verbs: ["get", "list", "update", "patch"] 20 | - apiGroups: [""] 21 | resources: ["namespaces"] 22 | verbs: ["get", "list"] 23 | - apiGroups: [""] 24 | resources: ["persistentvolumes"] 25 | verbs: ["get", "list", "watch", "update"] 26 | - apiGroups: ["storage.k8s.io"] 27 | resources: ["volumeattachments"] 28 | verbs: ["get", "list", "watch", "update"] 29 | - apiGroups: ["gcs.csi.ofek.dev"] 30 | resources: ["publishedvolumes"] 31 | verbs: ["get", "list", "watch", "update", "create", "delete"] 32 | --- 33 | apiVersion: rbac.authorization.k8s.io/v1 34 | kind: ClusterRoleBinding 35 | metadata: 36 | name: csi-gcs-node 37 | subjects: 38 | - kind: ServiceAccount 39 | name: csi-gcs 40 | roleRef: 41 | kind: ClusterRole 42 | name: csi-gcs-node 43 | apiGroup: rbac.authorization.k8s.io 44 | --- 45 | kind: ClusterRole 46 | apiVersion: rbac.authorization.k8s.io/v1 47 | metadata: 48 | name: csi-gcs-provisioner 49 | rules: 50 | - apiGroups: [""] 51 | resources: ["secrets"] 52 | verbs: ["get", "list", "watch"] 53 | - apiGroups: [""] 54 | resources: ["persistentvolumes"] 55 | verbs: ["get", "list", "watch", "create", "delete"] 56 | - apiGroups: [""] 57 | resources: ["persistentvolumeclaims"] 58 | verbs: ["get", "list", "watch", "update"] 59 | - apiGroups: ["storage.k8s.io"] 60 | resources: ["storageclasses"] 61 | verbs: ["get", "list", "watch"] 62 | - apiGroups: [""] 63 | resources: ["nodes"] 64 | verbs: ["get", "list", "watch"] 65 | - apiGroups: ["storage.k8s.io"] 66 | resources: ["csinodes"] 67 | verbs: ["get", "list", "watch"] 68 | - apiGroups: [""] 69 | resources: ["events"] 70 | verbs: ["list", "watch", "create", "update", "patch"] 71 | - apiGroups: ["snapshot.storage.k8s.io"] 72 | resources: ["volumesnapshots"] 73 | verbs: ["get", "list"] 74 | - apiGroups: ["snapshot.storage.k8s.io"] 75 | resources: ["volumesnapshotcontents"] 76 | verbs: ["get", "list"] 77 | --- 78 | kind: ClusterRoleBinding 79 | apiVersion: rbac.authorization.k8s.io/v1 80 | metadata: 81 | name: csi-gcs-provisioner 82 | subjects: 83 | - kind: ServiceAccount 84 | name: csi-gcs 85 | roleRef: 86 | kind: ClusterRole 87 | name: csi-gcs-provisioner 88 | apiGroup: rbac.authorization.k8s.io 89 | --- 90 | kind: ClusterRole 91 | apiVersion: rbac.authorization.k8s.io/v1 92 | metadata: 93 | name: csi-gcs-resizer 94 | rules: 95 | - apiGroups: [""] 96 | resources: ["secrets"] 97 | verbs: ["get", "list", "watch"] 98 | - apiGroups: [""] 99 | resources: ["persistentvolumes"] 100 | verbs: ["get", "list", "watch", "update", "patch"] 101 | - apiGroups: [""] 102 | resources: ["persistentvolumeclaims"] 103 | verbs: ["get", "list", "watch"] 104 | - apiGroups: [""] 105 | resources: ["persistentvolumeclaims/status"] 106 | verbs: ["update", "patch"] 107 | - apiGroups: ["storage.k8s.io"] 108 | resources: ["storageclasses"] 109 | verbs: ["get", "list", "watch"] 110 | - apiGroups: [""] 111 | resources: ["events"] 112 | verbs: ["list", "watch", "create", "update", "patch"] 113 | - apiGroups: ["coordination.k8s.io"] 114 | resources: ["leases"] 115 | verbs: ["get", "list", "watch", "create", "update", "patch"] 116 | - apiGroups: [""] 117 | resources: ["endpoints"] 118 | verbs: ["get", "list", "watch", "create", "update", "patch"] 119 | --- 120 | kind: ClusterRoleBinding 121 | apiVersion: rbac.authorization.k8s.io/v1 122 | metadata: 123 | name: csi-gcs-resizer 124 | subjects: 125 | - kind: ServiceAccount 126 | name: csi-gcs 127 | roleRef: 128 | kind: ClusterRole 129 | name: csi-gcs-resizer 130 | apiGroup: rbac.authorization.k8s.io 131 | -------------------------------------------------------------------------------- /deploy/overlays/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | bases: 4 | - ../../base 5 | images: 6 | - name: docker.io/ofekmeister/csi-gcs 7 | newTag: dev 8 | patchesStrategicMerge: 9 | - uselocalimage.yaml 10 | -------------------------------------------------------------------------------- /deploy/overlays/dev/uselocalimage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: csi-gcs 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: csi-gcs 9 | template: 10 | metadata: 11 | labels: 12 | app: csi-gcs 13 | spec: 14 | containers: 15 | - name: csi-gcs 16 | imagePullPolicy: Never -------------------------------------------------------------------------------- /deploy/overlays/stable-gke/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | bases: 4 | - ../stable 5 | images: 6 | - name: quay.io/k8scsi/csi-node-driver-registrar 7 | newName: gcr.io/gke-release/csi-node-driver-registrar 8 | newTag: v1.2.0-gke.0 9 | - name: quay.io/k8scsi/csi-provisioner 10 | newName: gcr.io/gke-release/csi-provisioner 11 | newTag: v1.6.0-gke.0 12 | - name: quay.io/k8scsi/csi-resizer 13 | newName: gcr.io/gke-release/csi-resizer 14 | newTag: v0.2.0-gke.0 15 | -------------------------------------------------------------------------------- /deploy/overlays/stable/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | bases: 4 | - ../../base 5 | images: 6 | - name: docker.io/ofekmeister/csi-gcs 7 | newTag: v0.9.0 8 | -------------------------------------------------------------------------------- /dev-env.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18.2-alpine3.15 AS build-gcsfuse 2 | 3 | ARG gcsfuse_version 4 | 5 | RUN apk add --update --no-cache fuse fuse-dev git 6 | 7 | WORKDIR ${GOPATH}/src/github.com/googlecloudplatform/gcsfuse 8 | 9 | # Create Tmp Bin Dir 10 | RUN mkdir /tmp/bin 11 | 12 | # Install gcsfuse using the specified version or commit hash 13 | RUN git clone https://github.com/googlecloudplatform/gcsfuse . && git checkout "v${gcsfuse_version}" 14 | RUN go install ./tools/build_gcsfuse 15 | RUN mkdir /tmp/gcsfuse 16 | RUN build_gcsfuse . /tmp/gcsfuse ${gcsfuse_version} -ldflags "-X main.gcsfuseVersion=${gcsfuse_version}" 17 | 18 | FROM golang:1.18.2-alpine3.15 19 | 20 | RUN apk add --update --no-cache fuse fuse-dev git python3 python3-dev py3-pip bash build-base docker 21 | 22 | COPY --from=build-gcsfuse /tmp/gcsfuse/bin/* /usr/local/bin/ 23 | COPY --from=build-gcsfuse /tmp/gcsfuse/sbin/* /sbin/ 24 | 25 | RUN python3 -m pip install --upgrade pip setuptools 26 | 27 | RUN mkdir /driver 28 | WORKDIR /driver 29 | 30 | COPY requirements.txt /tmp/requirements.txt 31 | 32 | RUN python3 -m pip install --upgrade --ignore-installed -r /tmp/requirements.txt 33 | 34 | RUN rm /tmp/requirements.txt 35 | -------------------------------------------------------------------------------- /docs/.scripts/49_inject_stable_version.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | 4 | MARKER = '' 5 | VERSION = None 6 | 7 | 8 | def get_latest_tag(): 9 | result = subprocess.run(['git', 'tag'], capture_output=True, check=True) 10 | lines = result.stdout.decode('utf-8').splitlines() 11 | 12 | pattern = re.compile(r'^v(\d+).(\d+).(\d+)$') 13 | tags = [] 14 | for line in lines: 15 | if match := pattern.search(line): 16 | tags.append(tuple(map(int, match.groups()))) 17 | 18 | if not tags: 19 | raise Exception('No tags found') 20 | 21 | tags.sort() 22 | latest = '.'.join(map(str, tags[-1])) 23 | 24 | return f"v{latest}" 25 | 26 | 27 | def patch(lines): 28 | """This injects the latest stable version based on tags.""" 29 | global VERSION 30 | if VERSION is None: 31 | VERSION = get_latest_tag() 32 | 33 | for i, line in enumerate(lines): 34 | lines[i] = line.replace(MARKER, VERSION) 35 | -------------------------------------------------------------------------------- /docs/.scripts/99_global_refs.py: -------------------------------------------------------------------------------- 1 | def patch(lines): 2 | """This ensures links and abbreviations are always available.""" 3 | lines.extend(('', '--8<-- "refs.txt"', '')) 4 | -------------------------------------------------------------------------------- /docs/.snippets/abbrs.txt: -------------------------------------------------------------------------------- 1 | *[CSI]: Container Storage Interface 2 | *[PVC]: Persistent Volume Claim 3 | *[PV]: Persistent Volume 4 | *[SC]: Storage Class 5 | *[GCS]: Google Cloud Storage 6 | *[GCP]: Google Cloud Platform 7 | -------------------------------------------------------------------------------- /docs/.snippets/links.txt: -------------------------------------------------------------------------------- 1 | [k8s-daemonset]: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ 2 | [k8s-statefulset]: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ 3 | [k8s-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ 4 | [k8s-volume-csi]: https://kubernetes.io/docs/concepts/storage/volumes/#csi 5 | [k8s-storage-class]: https://kubernetes.io/docs/concepts/storage/storage-classes/ 6 | [csi-deploy-controller]: https://kubernetes-csi.github.io/docs/deploying.html#controller-plugin 7 | [csi-deploy-node]: https://kubernetes-csi.github.io/docs/deploying.html#node-plugin 8 | [google-cloud-storage]: https://cloud.google.com/storage 9 | [gcp-create-sa-key]: https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-gcloud 10 | [gcp-service-account]: https://cloud.google.com/iam/docs/understanding-service-accounts 11 | [gcs-iam-permission]: https://cloud.google.com/storage/docs/access-control/iam-permissions 12 | [gcs-location]: https://cloud.google.com/storage/docs/locations#available_locations 13 | [gcsfuse-github]: https://github.com/GoogleCloudPlatform/gcsfuse 14 | [gcsfuse-implicit-dirs]: https://github.com/GoogleCloudPlatform/gcsfuse/blob/master/docs/semantics.md#implicit-directories 15 | [fuse-mount-options]: https://man7.org/linux/man-pages/man8/mount.fuse3.8.html#OPTIONS 16 | [libfuse-github]: https://github.com/libfuse/libfuse 17 | [key-locator-heuristics]: https://pkg.go.dev/golang.org/x/oauth2/google#FindDefaultCredentials 18 | -------------------------------------------------------------------------------- /docs/.snippets/refs.txt: -------------------------------------------------------------------------------- 1 | --8<-- 2 | links.txt 3 | abbrs.txt 4 | --8<-- 5 | -------------------------------------------------------------------------------- /docs/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/squidfunk/mkdocs-material/issues/1522 */ 2 | .md-typeset h5 { 3 | color: var(--md-default-fg-color); 4 | text-transform: none; 5 | } 6 | 7 | /* Brighter links for dark mode */ 8 | [data-md-color-scheme=slate] { 9 | /* https://github.com/squidfunk/mkdocs-material/blob/8.2.15/src/assets/stylesheets/main/_colors.scss */ 10 | --md-typeset-a-color: var(--md-primary-fg-color--light); 11 | } 12 | -------------------------------------------------------------------------------- /docs/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofek/csi-gcs/c1639b1978fa14dab231f67b4d4cd72d0a6314f9/docs/assets/images/favicon.ico -------------------------------------------------------------------------------- /docs/contributing/authors.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | ----- 4 | 5 | ## Maintainers 6 | 7 | - Ofek Lev [:material-web:](https://ofek.dev) [:material-github:](https://github.com/ofek) [:material-twitter:](https://twitter.com/Ofekmeister) 8 | - Jonatan Männchen [:material-github:](https://github.com/maennchen) 9 | 10 | ## Contributors 11 | 12 | - Ofek Lev [:material-github:](https://github.com/ofek) [:material-twitter:](https://twitter.com/Ofekmeister) 13 | - Jonatan Männchen [:material-github:](https://github.com/maennchen) 14 | - Joel Cressy [:material-github:](https://github.com/jtcressy) [:material-twitter:](https://twitter.com/jtcressy) 15 | - Alex Khaerov [:material-github:](https://github.com/hayorov) [:material-twitter:](https://twitter.com/hayorov) 16 | -------------------------------------------------------------------------------- /docs/contributing/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ----- 4 | 5 | ## Getting started 6 | 7 | * Dependencies 8 | - You'll need to have [Python 3.6+](https://www.python.org/downloads/) in your PATH 9 | - `python -m pip install --upgrade -r requirements.txt` 10 | * Minikube 11 | - Setup [`minikube`](https://kubernetes.io/docs/tasks/tools/install-minikube/#installing-minikube) 12 | - Start `minikube` (`minikube start`) 13 | * Build 14 | - Enable `minikube` Docker Env (`eval $(minikube docker-env)`) 15 | - Build Docker Image `invoke image` 16 | * `gcloud` 17 | - Install [`gcloud`](https://cloud.google.com/sdk/install) 18 | - Login to `gcloud` (`gcloud auth login`) 19 | * Google Cloud Project 20 | - Create Test Project (`gcloud projects create [PROJECT_ID] --name=[PROJECT_NAME]`) 21 | * Google Cloud Service Account 22 | - Create (`gcloud iam service-accounts create [ACCOUNT_NAME] --display-name="Test Account" --description="Test Account for GCS CSI" --project=[PROJECT_ID]`) 23 | - Create Key (`gcloud iam service-accounts keys create service-account.json --iam-account=[ACCOUNT_NAME]@[PROJECT_ID].iam.gserviceaccount.com --project=[PROJECT_ID]`) 24 | - Give Storage Admin Permission (`gcloud projects add-iam-policy-binding [PROJECT_ID] --member=serviceAccount:[ACCOUNT_NAME]@[PROJECT_ID].iam.gserviceaccount.com --role=roles/storage.admin`) 25 | * Create Secret 26 | - `kubectl create secret generic csi-gcs-secret --from-file=key=service-account.json` 27 | * Pull Needed Images 28 | - `docker pull quay.io/k8scsi/csi-node-driver-registrar:v1.2.0` 29 | * Apply config `kubectl apply -k deploy/overlays/dev` 30 | 31 | ## Rebuild & Test Manually in Minikube 32 | 33 | ```console 34 | # Build Binary 35 | invoke build 36 | 37 | # Build Container 38 | invoke image 39 | ``` 40 | 41 | Afterwards kill the currently running pod. 42 | 43 | ## Documentation 44 | 45 | ```console 46 | # Build 47 | invoke docs.build 48 | 49 | # Server 50 | invoke docs.serve 51 | ``` 52 | 53 | 54 | ## Sanity Tests 55 | 56 | Needs root privileges and `gcsfuse` installed, execution via docker recommended. 57 | 58 | ```console 59 | # Local 60 | invoke test.sanity 61 | 62 | # Docker 63 | invoke docker -c "invoke test.sanity" 64 | ``` 65 | 66 | Additionally the file `./test/secret.yaml` has to be created with the following content: 67 | 68 | ```yml 69 | CreateVolumeSecret: 70 | projectId: [Google Cloud Project ID] 71 | key: | 72 | [Storage Admin Key JSON] 73 | DeleteVolumeSecret: 74 | projectId: [Google Cloud Project ID] 75 | key: | 76 | [Storage Admin Key JSON] 77 | ControllerPublishVolumeSecret: 78 | projectId: [Google Cloud Project ID] 79 | key: | 80 | [Storage Admin Key JSON] 81 | ControllerUnpublishVolumeSecret: 82 | projectId: [Google Cloud Project ID] 83 | key: | 84 | [Storage Admin Key JSON] 85 | NodeStageVolumeSecret: 86 | projectId: [Google Cloud Project ID] 87 | key: | 88 | [Storage Object Admin Key JSON] 89 | NodePublishVolumeSecret: 90 | projectId: [Google Cloud Project ID] 91 | key: | 92 | [Storage Object Admin Key JSON] 93 | ControllerValidateVolumeCapabilitiesSecret: 94 | projectId: [Google Cloud Project ID] 95 | key: | 96 | [Storage Admin Key JSON] 97 | ``` 98 | 99 | ## Develop inside Docker 100 | 101 | Run all `invoke` commands through `invoke env -c "[CMD]"`. 102 | 103 | ## Regenerating the API Client 104 | 105 | If any changes are made in the `pkg/apis` package, the `pkg/client` needs to be regenerated. 106 | 107 | To regenerate the client package, run `invoke codegen`. 108 | -------------------------------------------------------------------------------- /docs/csi_compatibility.md: -------------------------------------------------------------------------------- 1 | # CSI Specification Compatibility 2 | 3 | This page describes compatibility to the [CSI specification](https://github.com/container-storage-interface/spec/blob/master/spec.md). 4 | 5 | ## Capacity 6 | 7 | !!! warning "Important" 8 | Google Cloud Storage has no concept of capacity limits. Therefore, this driver is unable to provide capacity limit enforcement. 9 | 10 | The driver only sets a `capacity` label for the `bucket` containing the requested bytes. 11 | 12 | ## Snapshots 13 | 14 | [Snapshots](https://github.com/container-storage-interface/spec/blob/master/spec.md#createsnapshot) are not currently supported, but are on the roadmap for the future. 15 | 16 | ## `CreateVolume` / `VolumeContentSource` 17 | 18 | [`CreateVolume` / `VolumeContentSource`](https://github.com/container-storage-interface/spec/blob/master/spec.md#createvolume) is not currently supported, but is on the roadmap for the future. 19 | 20 | ## Fuse 21 | 22 | Since [`gcsfuse`][gcsfuse-github] is backed by [`fuse`][libfuse-github], the mount needs a process to back it. This is an unsolved problem with CSI. See https://github.com/kubernetes/kubernetes/issues/70013 23 | 24 | Because of this problem, all mounts will terminate if a pod of the `csi-gcs-node` DaemonSet is restarted. This for example happens when the driver is updated. 25 | 26 | To counteract the problem of having pods with broken mounts, the `csi-gcs-node` Pod will terminate all Pods with broken mounts on start. 27 | 28 | ??? info "Disabling Pod Termination" 29 | 30 | The Pod Termination can be disabled by changing the argument `delete-orphaned-pods` to `false` on the DaemonSet. -------------------------------------------------------------------------------- /docs/dynamic_provisioning.md: -------------------------------------------------------------------------------- 1 | # Dynamic provisioning 2 | 3 | --- 4 | 5 | ## Secrets 6 | 7 | After acquiring [service account keys](#permission), create 2 [secrets][k8s-secret] (we'll call 8 | them `csi-gcs-secret-mounter` and `csi-gcs-secret-creator` in the following example): 9 | 10 | ```console 11 | kubectl create secret generic csi-gcs-secret-mounter --from-file=key= 12 | kubectl create secret generic csi-gcs-secret-creator --from-file=key= --from-literal=projectId=csi-gcs 13 | ``` 14 | 15 | ## Usage 16 | 17 | Let's run another example application! 18 | 19 | ```console 20 | kubectl apply -k "github.com/ofek/csi-gcs/examples/dynamic?ref=" 21 | ``` 22 | 23 | Confirm it's working by running 24 | 25 | ```console 26 | kubectl get pods,sc,pv,pvc 27 | ``` 28 | 29 | You should see something like 30 | 31 | ``` 32 | NAME READY STATUS RESTARTS AGE 33 | pod/csi-gcs-test-5f677df9f9-qd8nt 2/2 Running 0 100s 34 | 35 | NAME PROVISIONER AGE 36 | storageclass.storage.k8s.io/csi-gcs gcs.csi.ofek.dev 100s 37 | 38 | NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE 39 | persistentvolume/pvc-906ed812-2c06-4eaa-a80e-7115e8ffd653 5Gi RWO Delete Bound default/csi-gcs-pvc csi-gcs 98s 40 | 41 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE 42 | persistentvolumeclaim/csi-gcs-pvc Bound pvc-906ed812-2c06-4eaa-a80e-7115e8ffd653 5Gi RWO csi-gcs 100s 43 | ``` 44 | 45 | Note the pod name, in this case `csi-gcs-test-5f677df9f9-qd8nt`. The pod in the example deployment has 2 containers: a `writer` and a `reader`. 46 | 47 | Now create some data! 48 | 49 | ```console 50 | kubectl exec csi-gcs-test-5f677df9f9-qd8nt -c writer -- /bin/sh -c "echo Hello from Google Cloud Storage! > /data/test.txt" 51 | ``` 52 | 53 | Let's read what we just put in the bucket 54 | 55 | ``` 56 | $ kubectl exec csi-gcs-test-5f677df9f9-qd8nt -c reader -it -- /bin/sh 57 | / # ls -lh /data 58 | total 1K 59 | -rw-r--r-- 1 root root 33 May 26 21:23 test.txt 60 | / # cat /data/test.txt 61 | Hello from Google Cloud Storage! 62 | ``` 63 | 64 | Notice that while the `writer` container's permission is completely governed by the `mounter`'s service account key, 65 | the `reader` container is further restricted to read-only access 66 | 67 | ``` 68 | / # touch /data/forbidden.txt 69 | touch: /data/forbidden.txt: Read-only file system 70 | ``` 71 | 72 | To clean up everything, run the following commands 73 | 74 | ```console 75 | kubectl delete -f "https://github.com/ofek/csi-gcs/blob//examples/dynamic/deployment.yaml" 76 | kubectl delete -f "https://github.com/ofek/csi-gcs/blob//examples/dynamic/pvc.yaml" 77 | kubectl delete -f "https://github.com/ofek/csi-gcs/blob//examples/dynamic/sc.yaml" 78 | kubectl delete -k "github.com/ofek/csi-gcs/deploy/overlays/stable?ref=" 79 | kubectl delete secret csi-gcs-secret-creator 80 | kubectl delete secret csi-gcs-secret-mounter 81 | ``` 82 | 83 | ??? note 84 | Cleanup is necessarily verbose until [this](https://github.com/kubernetes-sigs/kustomize/issues/2138) is resolved. 85 | 86 | ## Driver options 87 | 88 | [StorageClass][k8s-storage-class] is the resource type that enables dynamic provisioning. 89 | 90 | ```yaml 91 | apiVersion: storage.k8s.io/v1 92 | kind: StorageClass 93 | metadata: 94 | name: 95 | provisioner: gcs.csi.ofek.dev 96 | reclaimPolicy: Delete 97 | parameters: ... 98 | ``` 99 | 100 | ### Storage Class Parameters 101 | 102 | | Annotation | Description | 103 | | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 104 | | `csi.storage.k8s.io/node-publish-secret-name` | The name of the secret allowed to mount created buckets | 105 | | `csi.storage.k8s.io/node-publish-secret-namespace` | The namespace of the secret allowed to mount created buckets | 106 | | `csi.storage.k8s.io/provisioner-secret-name` | The name of the secret allowed to create buckets | 107 | | `csi.storage.k8s.io/provisioner-secret-namespace` | The namespace of the secret allowed to create buckets | 108 | | `csi.storage.k8s.io/controller-expand-secret-name` | The name of the secret allowed to expand [bucket capacity](csi_compatibility.md#capacity) | 109 | | `csi.storage.k8s.io/controller-expand-secret-namespace` | The namespace of the secret allowed to expand [bucket capacity](csi_compatibility.md#capacity) | 110 | | `gcs.csi.ofek.dev/project-id` | The project to create the buckets in. If not specified, `projectId` will be looked up in the provisioner's secret | 111 | | `gcs.csi.ofek.dev/location` | The [location][gcs-location] to create buckets at (default `US` multi-region) | 112 | | `gcs.csi.ofek.dev/kms-key-id` | (optional) KMS encryption key ID. (projects/my-pet-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key) | 113 | | `gcs.csi.ofek.dev/max-retry-sleep` | The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries. | 114 | 115 | !!! tip 116 | You may omit the secret definition and let the code automatically detect the service account key using [standard heuristics][key-locator-heuristics]. 117 | 118 | ### Persistent Volume Claim Parameters 119 | 120 | ```yaml 121 | apiVersion: v1 122 | kind: PersistentVolumeClaim 123 | metadata: 124 | annotations: ... 125 | ``` 126 | 127 | | Annotation | Description | 128 | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 129 | | `gcs.csi.ofek.dev/project-id` | The project to create the buckets in. If not specified, `projectId` will be looked up in the provisioner's secret | 130 | | `gcs.csi.ofek.dev/location` | The [location][gcs-location] to create buckets at (default `US` multi-region) | 131 | | `gcs.csi.ofek.dev/bucket` | The name for the new bucket | 132 | | `gcs.csi.ofek.dev/kms-key-id` | (optional) KMS encryption key ID. (projects/my-pet-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key) | 133 | | `gcs.csi.ofek.dev/max-retry-sleep` | The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries. | 134 | 135 | ### Persistent buckets 136 | 137 | In our example, the dynamically created buckets are deleted during cleanup. If you want the buckets to not be ephemeral, 138 | you can set `reclaimPolicy` to `Retain`. 139 | 140 | ### Extra flags 141 | 142 | You can pass flags to [gcsfuse][gcsfuse-github]. They will be forwarded to [`PersistentVolumeClaim.spec.csi.volumeAttributes`](static_provisioning.md#extra-flags). 143 | 144 | The following flags are supported (ordered by precedence): 145 | 146 | 1. ??? info "**PersistentVolumeClaim.metadata.annotations**" 147 | 148 | ```yaml 149 | apiVersion: v1 150 | kind: PersistentVolumeClaim 151 | metadata: 152 | annotations: 153 | gcs.csi.ofek.dev/gid: "63147" 154 | gcs.csi.ofek.dev/dir-mode: "0775" 155 | gcs.csi.ofek.dev/file-mode: "0664" 156 | ``` 157 | 158 | | Option | Type | Description | 159 | | --- | --- | --- | 160 | | `gcs.csi.ofek.dev/dir-mode` | Octal Integer | Permission bits for directories. (default: 0775) | 161 | | `gcs.csi.ofek.dev/file-mode` | Octal Integer | Permission bits for files. (default: 0664) | 162 | | `gcs.csi.ofek.dev/gid` | Integer | GID owner of all inodes. (default: 63147) | 163 | | `gcs.csi.ofek.dev/uid` | Integer | UID owner of all inodes. (default: -1) | 164 | | `gcs.csi.ofek.dev/implicit-dirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 165 | | `gcs.csi.ofek.dev/billing-project` | Text | Project to use for billing when accessing requester pays buckets. | 166 | | `gcs.csi.ofek.dev/limit-bytes-per-sec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 167 | | `gcs.csi.ofek.dev/limit-ops-per-sec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 168 | | `gcs.csi.ofek.dev/stat-cache-ttl` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 169 | | `gcs.csi.ofek.dev/type-cache-ttl` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 170 | | `gcs.csi.ofek.dev/fuse-mount-options` | Text[] | Additional comma-separated system-specific [mount options][fuse-mount-options]. Be careful! | 171 | | `gcs.csi.ofek.dev/max-retry-sleep` | Integer | The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries. | 172 | 173 | 1. ??? info "**StorageClass.parameters**" 174 | 175 | ```yaml 176 | apiVersion: storage.k8s.io/v1 177 | kind: StorageClass 178 | parameters: 179 | gid: "63147" 180 | dirMode: "0775" 181 | fileMode: "0664" 182 | ``` 183 | 184 | | Option | Type | Description | 185 | | --- | --- | --- | 186 | | `dirMode` | Octal Integer | Permission bits for directories. (default: 0775) | 187 | | `fileMode` | Octal Integer | Permission bits for files. (default: 0664) | 188 | | `gid` | Integer | GID owner of all inodes. (default: 63147) | 189 | | `uid` | Integer | UID owner of all inodes. (default: -1) | 190 | | `implicitDirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 191 | | `billingProject` | Text | Project to use for billing when accessing requester pays buckets. | 192 | | `limitBytesPerSec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 193 | | `limitOpsPerSec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 194 | | `statCacheTTL` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 195 | | `typeCacheTTL` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 196 | | `fuseMountOptions` | Text[] | Additional comma-separated system-specific [mount options][fuse-mount-options]. Be careful! | 197 | | `maxRetrySleep` | Integer | The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries. | 198 | 199 | 1. ??? info "**StorageClass.mountOptions**" 200 | 201 | ```yaml 202 | apiVersion: storage.k8s.io/v1 203 | kind: StorageClass 204 | mountOptions: 205 | - --gid=63147 206 | - --dir-mode=0775 207 | - --file-mode=0664 208 | ``` 209 | 210 | | Option | Type | Description | 211 | | --- | --- | --- | 212 | | `dir-mode` | Octal Integer | Permission bits for directories. (default: 0775) | 213 | | `file-mode` | Octal Integer | Permission bits for files. (default: 0664) | 214 | | `gid` | Integer | GID owner of all inodes. (default: 63147) | 215 | | `uid` | Integer | UID owner of all inodes. (default: -1) | 216 | | `implicit-dirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 217 | | `billing-project` | Text | Project to use for billing when accessing requester pays buckets. | 218 | | `limit-bytes-per-sec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 219 | | `limit-ops-per-sec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 220 | | `stat-cache-ttl` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 221 | | `type-cache-ttl` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 222 | | `fuse-mount-option` | Text | Additional system-specific [mount option][fuse-mount-options]. Be careful! | 223 | | `max-retry-sleep` | Integer | The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries. | 224 | 225 | 1. ??? info "**StorageClass.parameters."csi.storage.k8s.io/provisioner-secret-name**"" 226 | | Option | Type | Description | 227 | | --- | --- | --- | 228 | | `dirMode` | Octal Integer | Permission bits for directories, in octal. (default: 0775) | 229 | | `fileMode` | Octal Integer | Permission bits for files, in octal. (default: 0664) | 230 | | `gid` | Integer | GID owner of all inodes. (default: 63147) | 231 | | `uid` | Integer | UID owner of all inodes. (default: -1) | 232 | | `implicitDirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 233 | | `billingProject` | Text | Project to use for billing when accessing requester pays buckets. | 234 | | `limitBytesPerSec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 235 | | `limitOpsPerSec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 236 | | `statCacheTTL` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 237 | | `typeCacheTTL` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 238 | | `fuseMountOptions` | Text[] | Additional comma-separated system-specific [mount options][fuse-mount-options]. Be careful! | 239 | | `maxRetrySleep` | Integer | The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries. | 240 | 241 | ## Permission 242 | 243 | In order to access anything stored in GCS, you will need [service accounts][gcp-service-account] with 244 | appropriate IAM roles. You will usually assign the role `roles/storage.admin`. 245 | 246 | The [easiest way][gcp-create-sa-key] to create service account keys, if you don't yet 247 | have any, is to run: 248 | 249 | ```console 250 | gcloud iam service-accounts list 251 | ``` 252 | 253 | to find the email of a desired service account, then run: 254 | 255 | ```console 256 | gcloud iam service-accounts keys create .json --iam-account 257 | ``` 258 | 259 | to create a key file. 260 | 261 | ### Mounter 262 | 263 | The [Node Plugin][csi-deploy-node] is the component that is actually mounting and serving buckets to pods. 264 | If writes are needed, you will usually select `roles/storage.objectAdmin` scoped to the desired buckets. 265 | 266 | ### Creator 267 | 268 | The [Controller Plugin][csi-deploy-controller] is the component that is in charge of creating buckets. 269 | The service account will need the `storage.buckets.create` [Cloud IAM permission][gcs-iam-permission]. 270 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ----- 4 | 5 | ## Installation 6 | 7 | Like other CSI drivers, a [StatefulSet][k8s-statefulset] and [DaemonSet][k8s-daemonset] are the recommended 8 | deployment mechanisms for the [Controller Plugin][csi-deploy-controller] and [Node Plugin][csi-deploy-node], 9 | respectively. 10 | 11 | Run 12 | 13 | ```console 14 | kubectl apply -k "github.com/ofek/csi-gcs/deploy/overlays/stable?ref=" 15 | ``` 16 | 17 | Now the output from running the command 18 | 19 | ```console 20 | kubectl get CSIDriver,daemonsets,pods -n kube-system 21 | ``` 22 | 23 | should contain something like 24 | 25 | ``` 26 | NAME CREATED AT 27 | csidriver.storage.k8s.io/gcs.csi.ofek.dev 2020-05-26T21:03:14Z 28 | 29 | NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE 30 | daemonset.apps/csi-gcs 1 1 1 1 1 kubernetes.io/os=linux 18s 31 | 32 | NAME READY STATUS RESTARTS AGE 33 | pod/csi-gcs-f9vgd 4/4 Running 0 18s 34 | ``` 35 | 36 | ## Customer-managed encryption keys (CMEK) 37 | 38 | Make sure that your Google Cloud Storage service account has `roles/cloudkms.cryptoKeyEncrypterDecrypter` for the target encryption key. 39 | 40 | `kmsKeyId`/`gcs.csi.ofek.dev/kms-key-id` could be defined as part of a secret or annotation/mount to enable [CMEK encryption for Google Storage](https://cloud.google.com/storage/docs/gsutil/addlhelp/UsingEncryptionKeys). 41 | 42 | 43 | 44 | ## Debugging 45 | 46 | ```console 47 | kubectl logs -l app=csi-gcs -c csi-gcs -n kube-system 48 | ``` 49 | 50 | ## Resource Requests / Limits 51 | 52 | To change the default resource requests & limits, override them using kustomize. 53 | 54 | **kustomization.yaml** 55 | 56 | ```yaml 57 | apiVersion: kustomize.config.k8s.io/v1beta1 58 | kind: Kustomization 59 | bases: 60 | - github.com/ofek/csi-gcs/deploy/overlays/stable-gke?ref= 61 | patchesStrategicMerge: 62 | - resources.yaml 63 | ``` 64 | 65 | **resources.yaml** 66 | 67 | ```yaml 68 | apiVersion: apps/v1 69 | kind: DaemonSet 70 | metadata: 71 | name: csi-gcs 72 | spec: 73 | template: 74 | spec: 75 | containers: 76 | - name: csi-gcs 77 | resources: 78 | limits: 79 | cpu: 1 80 | memory: 1Gi 81 | requests: 82 | cpu: 10m 83 | memory: 80Mi 84 | ``` 85 | 86 | ## Namespace 87 | 88 | This driver deploys directly into the `kube-system` namespace. That can't be changed 89 | since the `DaemonSet` requires `priorityClassName: system-node-critical` to be 90 | prioritized over normal workloads. 91 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # csi-gcs 2 | 3 | | | | 4 | | --- | --- | 5 | | CI/CD | [![CI - Test](https://github.com/ofek/csi-gcs/actions/workflows/test.yml/badge.svg){ loading=lazy }](https://github.com/ofek/csi-gcs/actions/workflows/test.yml) [![CI - Image](https://github.com/ofek/csi-gcs/actions/workflows/image.yml/badge.svg){ loading=lazy }](https://github.com/ofek/csi-gcs/actions/workflows/image.yml) | 6 | | Docs | [![Docs](https://github.com/ofek/csi-gcs/actions/workflows/docs.yml/badge.svg){ loading=lazy }](https://github.com/ofek/csi-gcs/actions/workflows/docs.yml) | 7 | | Image | [![Docker - Version](https://img.shields.io/docker/v/ofekmeister/csi-gcs.svg?sort=semver){ loading=lazy }](https://hub.docker.com/r/ofekmeister/csi-gcs) [![Docker - Pulls](https://img.shields.io/docker/pulls/ofekmeister/csi-gcs.svg){ loading=lazy }](https://hub.docker.com/r/ofekmeister/csi-gcs) | 8 | | Meta | [![License - MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-9400d3.svg){ loading=lazy }](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social){ loading=lazy }](https://github.com/sponsors/ofek) | 9 | 10 | ----- 11 | 12 | An easy-to-use, cross-platform, and highly optimized Kubernetes CSI driver for mounting [Google Cloud Storage][google-cloud-storage] buckets. 13 | 14 | ## License 15 | 16 | `csi-gcs` is distributed under the terms of both 17 | 18 | - [Apache License, Version 2.0](https://choosealicense.com/licenses/apache-2.0) 19 | - [MIT License](https://choosealicense.com/licenses/mit) 20 | 21 | at your option. 22 | 23 | ## Navigation 24 | 25 | Desktop readers can use keyboard shortcuts to navigate. 26 | 27 | | Keys | Action | 28 | | --- | --- | 29 | |
  • , (comma)
  • p
| Navigate to the "previous" page | 30 | |
  • . (period)
  • n
| Navigate to the "next" page | 31 | |
  • /
  • s
| Display the search modal | 32 | -------------------------------------------------------------------------------- /docs/static_provisioning.md: -------------------------------------------------------------------------------- 1 | # Static provisioning 2 | 3 | ----- 4 | 5 | ## Secrets 6 | 7 | After acquiring a [service account key](#permission), create a [secret][k8s-secret] (we'll call 8 | it `csi-gcs-secret` in the following example): 9 | 10 | ```console 11 | kubectl create secret generic csi-gcs-secret --from-literal=bucket= --from-file=key= 12 | ``` 13 | 14 | Note we store the desired bucket in the secret for brevity only, there are [other ways](#bucket) to select a bucket. 15 | 16 | ## Usage 17 | 18 | Let's run an example application! 19 | 20 | ```console 21 | kubectl apply -k "github.com/ofek/csi-gcs/examples/static?ref=" 22 | ``` 23 | 24 | Confirm it's working by running 25 | 26 | ```console 27 | kubectl get pods,pv,pvc 28 | ``` 29 | 30 | You should see something like 31 | 32 | ``` 33 | NAME READY STATUS RESTARTS AGE 34 | pod/csi-gcs-test-5f677df9f9-f59km 2/2 Running 0 10s 35 | 36 | NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE 37 | persistentvolume/csi-gcs-pv 5Gi RWO Retain Bound default/csi-gcs-pvc csi-gcs-test-sc 10s 38 | 39 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE 40 | persistentvolumeclaim/csi-gcs-pvc Bound csi-gcs-pv 5Gi RWO csi-gcs-test-sc 10s 41 | ``` 42 | 43 | Note the pod name, in this case `csi-gcs-test-5f677df9f9-f59km`. The pod in the example deployment has 2 containers: a `writer` and a `reader`. 44 | 45 | Now create some data! 46 | 47 | ```console 48 | kubectl exec csi-gcs-test-5f677df9f9-f59km -c writer -- /bin/sh -c "echo Hello from Google Cloud Storage! > /data/test.txt" 49 | ``` 50 | 51 | Let's read what we just put in the bucket 52 | 53 | ``` 54 | $ kubectl exec csi-gcs-test-5f677df9f9-f59km -c reader -it -- /bin/sh 55 | / # ls -lh /data 56 | total 1K 57 | -rw-r--r-- 1 root root 33 May 26 21:23 test.txt 58 | / # cat /data/test.txt 59 | Hello from Google Cloud Storage! 60 | ``` 61 | 62 | Notice that while the `writer` container's permission is completely governed by the service account key, 63 | the `reader` container is further restricted to read-only access 64 | 65 | ``` 66 | / # touch /data/forbidden.txt 67 | touch: /data/forbidden.txt: Read-only file system 68 | ``` 69 | 70 | To clean up everything, run the following commands 71 | 72 | ```console 73 | kubectl delete -k "github.com/ofek/csi-gcs/examples/static?ref=" 74 | kubectl delete -k "github.com/ofek/csi-gcs/deploy/overlays/stable?ref=" 75 | kubectl delete secret csi-gcs-secret 76 | ``` 77 | 78 | ## Driver options 79 | 80 | See the CSI section of the [Kubernetes Volume docs][k8s-volume-csi]. 81 | 82 | ### Service account key 83 | 84 | The contents of the JSON key may be passed in as a secret defined in 85 | `PersistentVolume.spec.csi.nodePublishSecretRef`. The name of the key in the secret is `key`. 86 | 87 | !!! tip 88 | You may omit the secret definition and let the code automatically detect the service account key using [standard heuristics][key-locator-heuristics]. 89 | 90 | ### Bucket 91 | 92 | The bucket name is resolved in the following order: 93 | 94 | 1. `bucket` in `PersistentVolume.spec.csi.volumeAttributes` 95 | 1. `bucket` in `PersistentVolume.spec.mountOptions` 96 | 1. `bucket` in secret referenced by `PersistentVolume.spec.csi.nodePublishSecretRef` 97 | 1. `PersistentVolume.spec.csi.volumeHandle` 98 | 99 | ### Extra flags 100 | 101 | You can pass flags to [gcsfuse][gcsfuse-github] in the following ways (ordered by precedence): 102 | 103 | 1. ??? info "**PersistentVolume.spec.csi.volumeAttributes**" 104 | ```yaml 105 | apiVersion: v1 106 | kind: PersistentVolume 107 | spec: 108 | csi: 109 | driver: gcs.csi.ofek.dev 110 | volumeAttributes: 111 | gid: "63147" 112 | dirMode: "0775" 113 | fileMode: "0664" 114 | ``` 115 | 116 | | Option | Type | Description | 117 | | --- | --- | --- | 118 | | `dirMode` | Octal Integer | Permission bits for directories. (default: 0775) | 119 | | `fileMode` | Octal Integer | Permission bits for files. (default: 0664) | 120 | | `gid` | Integer | GID owner of all inodes. (default: 63147) | 121 | | `uid` | Integer | UID owner of all inodes. (default: -1) | 122 | | `implicitDirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 123 | | `billingProject` | Text | Project to use for billing when accessing requester pays buckets. | 124 | | `limitBytesPerSec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 125 | | `limitOpsPerSec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 126 | | `statCacheTTL` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 127 | | `typeCacheTTL` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 128 | | `fuseMountOptions` | Text[] | Additional comma-separated system-specific [mount options][fuse-mount-options]. Be careful! | 129 | 130 | 1. ??? info "**PersistentVolume.spec.mountOptions**" 131 | ```yaml 132 | apiVersion: v1 133 | kind: PersistentVolume 134 | spec: 135 | mountOptions: 136 | - --gid=63147 137 | - --dir-mode=0775 138 | - --file-mode=0664 139 | ``` 140 | 141 | | Option | Type | Description | 142 | | --- | --- | --- | 143 | | `dir-mode` | Octal Integer | Permission bits for directories. (default: 0775) | 144 | | `file-mode` | Octal Integer | Permission bits for files. (default: 0664) | 145 | | `gid` | Integer | GID owner of all inodes. (default: 63147) | 146 | | `uid` | Integer | UID owner of all inodes. (default: -1) | 147 | | `implicit-dirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 148 | | `billing-project` | Text | Project to use for billing when accessing requester pays buckets. | 149 | | `limit-bytes-per-sec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 150 | | `limit-ops-per-sec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 151 | | `stat-cache-ttl` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 152 | | `type-cache-ttl` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 153 | | `fuse-mount-option` | Text | Additional comma-separated system-specific [mount option][fuse-mount-options]. Be careful! | 154 | 155 | 1. ??? info "**PersistentVolume.spec.csi.nodePublishSecretRef**" 156 | | Option | Type | Description | 157 | | --- | --- | --- | 158 | | `dirMode` | Octal Integer | Permission bits for directories, in octal. (default: 0775) | 159 | | `fileMode` | Octal Integer | Permission bits for files, in octal. (default: 0664) | 160 | | `gid` | Integer | GID owner of all inodes. (default: 63147) | 161 | | `uid` | Integer | UID owner of all inodes. (default: -1) | 162 | | `implicitDirs` | Flag | [Implicitly][gcsfuse-implicit-dirs] define directories based on content. | 163 | | `billingProject` | Text | Project to use for billing when accessing requester pays buckets. | 164 | | `limitBytesPerSec` | Integer | Bandwidth limit for reading data, measured over a 30-second window. The default is -1 (no limit). | 165 | | `limitOpsPerSec` | Integer | Operations per second limit, measured over a 30-second window. The default is 5. Use -1 for no limit. | 166 | | `statCacheTTL` | Text | How long to cache StatObject results and inode attributes e.g. `1h`. | 167 | | `typeCacheTTL` | Text | How long to cache name -> file/dir mappings in directory inodes e.g. `1h`. | 168 | | `fuseMountOptions` | Text[] | Additional comma-separated system-specific [mount options][fuse-mount-options]. Be careful! | 169 | 170 | ## Permission 171 | 172 | In order to access anything stored in GCS, you will need [service accounts][gcp-service-account] with 173 | appropriate IAM roles. If writes are needed, you will usually select `roles/storage.objectAdmin` scoped 174 | to the desired buckets. 175 | 176 | The [easiest way][gcp-create-sa-key] to create service account keys, if you don't yet 177 | have any, is to run: 178 | 179 | ```console 180 | gcloud iam service-accounts list 181 | ``` 182 | 183 | to find the email of a desired service account, then run: 184 | 185 | ```console 186 | gcloud iam service-accounts keys create .json --iam-account 187 | ``` 188 | 189 | to create a key file. 190 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ----- 4 | 5 | ## Early warnings from pods 6 | 7 | Warnings, like the one below, can be seen from pods scheduled on newly started nodes. 8 | 9 | ``` 10 | MountVolume.MountDevice failed for volume "xxxx" : kubernetes.io/csi: attacher.MountDevice failed to create newCsiDriverClient: driver name gcs.csi.ofek.dev not found in the list of registered CSI drivers 11 | ``` 12 | 13 | Those warnings are temporary and reflect that the driver is still starting. Kubernetes will retry until the driver is ready. The problem is often encountered in clusters with auto-scaler as nodes come and go. 14 | 15 | This is a known issue of kubernetes (see [kubernetes#75890](https://github.com/kubernetes/kubernetes/issues/75890)). 16 | 17 | A possible workaround is to taint all nodes running the `csi-gcs` driver like `/driver-ready=false:NoSchedule` and use, as suggested in [this comment](https://github.com/kubernetes/kubernetes/issues/75890#issuecomment-725792993), a custom controller like [wish/nodetaint](https://github.com/wish/nodetaint) to remove the taint once the `csi-gcs` pod is ready. 18 | 19 | This workaround will ensure pods are repelled from nodes until the `csi-gcs` driver is ready without interfering with other components like the cluster auto-scaler. 20 | 21 | By default, `` is `gcs.csi.ofek.dev`. 22 | 23 | !!! warning 24 | The driver labels the node with `/driver-ready=true` to reflect its readiness state. It's possible to use a node selector to select nodes with a ready `csi-gcs` node driver. However, it doesn't work with clusters using cluster-autoscaler as the auto-scaler will never find a node with matching `/driver-ready=true` label. 25 | -------------------------------------------------------------------------------- /examples/dynamic/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: csi-gcs-test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: writer 10 | image: busybox 11 | command: 12 | - sleep 13 | - infinity 14 | volumeMounts: 15 | - name: csi-gcs-pvc 16 | mountPath: /data 17 | - name: reader 18 | image: busybox 19 | command: 20 | - sleep 21 | - infinity 22 | volumeMounts: 23 | - name: csi-gcs-pvc 24 | mountPath: /data 25 | readOnly: true 26 | volumes: 27 | - name: csi-gcs-pvc 28 | persistentVolumeClaim: 29 | claimName: csi-gcs-pvc 30 | -------------------------------------------------------------------------------- /examples/dynamic/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | commonLabels: 4 | app: csi-gcs-test 5 | resources: 6 | - sc.yaml 7 | - pvc.yaml 8 | - deployment.yaml 9 | -------------------------------------------------------------------------------- /examples/dynamic/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: csi-gcs-pvc 5 | spec: 6 | storageClassName: csi-gcs 7 | accessModes: 8 | - ReadWriteOnce 9 | resources: 10 | requests: 11 | storage: 5Gi 12 | -------------------------------------------------------------------------------- /examples/dynamic/sc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1 2 | kind: StorageClass 3 | metadata: 4 | name: csi-gcs 5 | provisioner: gcs.csi.ofek.dev 6 | volumeBindingMode: Immediate 7 | allowVolumeExpansion: true 8 | reclaimPolicy: Delete 9 | parameters: 10 | csi.storage.k8s.io/node-publish-secret-name: csi-gcs-secret-mounter 11 | csi.storage.k8s.io/node-publish-secret-namespace: default 12 | csi.storage.k8s.io/provisioner-secret-name: csi-gcs-secret-creator 13 | csi.storage.k8s.io/provisioner-secret-namespace: default 14 | csi.storage.k8s.io/controller-expand-secret-name: csi-gcs-secret-creator 15 | csi.storage.k8s.io/controller-expand-secret-namespace: default 16 | -------------------------------------------------------------------------------- /examples/static/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: csi-gcs-test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: writer 10 | image: busybox 11 | command: 12 | - sleep 13 | - infinity 14 | volumeMounts: 15 | - name: csi-gcs-pvc 16 | mountPath: /data 17 | - name: reader 18 | image: busybox 19 | command: 20 | - sleep 21 | - infinity 22 | volumeMounts: 23 | - name: csi-gcs-pvc 24 | mountPath: /data 25 | readOnly: true 26 | volumes: 27 | - name: csi-gcs-pvc 28 | persistentVolumeClaim: 29 | claimName: csi-gcs-pvc 30 | -------------------------------------------------------------------------------- /examples/static/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | commonLabels: 4 | app: csi-gcs-test 5 | resources: 6 | - pv.yaml 7 | - pvc.yaml 8 | - deployment.yaml 9 | -------------------------------------------------------------------------------- /examples/static/pv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: csi-gcs-pv 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | capacity: 9 | storage: 5Gi 10 | persistentVolumeReclaimPolicy: Retain 11 | storageClassName: csi-gcs-test-sc 12 | csi: 13 | driver: gcs.csi.ofek.dev 14 | volumeHandle: csi-gcs 15 | nodePublishSecretRef: 16 | name: csi-gcs-secret 17 | namespace: default 18 | -------------------------------------------------------------------------------- /examples/static/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: csi-gcs-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 5Gi 11 | storageClassName: csi-gcs-test-sc 12 | -------------------------------------------------------------------------------- /examples/static/sc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1 2 | kind: StorageClass 3 | metadata: 4 | name: csi-gcs-test-sc 5 | provisioner: gcs.csi.ofek.dev 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ofek/csi-gcs 2 | 3 | go 1.18 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.30.1 7 | github.com/container-storage-interface/spec v1.7.0 8 | github.com/kubernetes-csi/csi-lib-utils v0.12.0 9 | github.com/kubernetes-csi/csi-test/v3 v3.1.1 10 | github.com/onsi/ginkgo v1.16.5 11 | github.com/onsi/gomega v1.27.6 12 | golang.org/x/oauth2 v0.6.0 13 | google.golang.org/api v0.114.0 14 | google.golang.org/grpc v1.54.0 15 | k8s.io/apimachinery v0.26.3 16 | k8s.io/client-go v0.26.0 17 | k8s.io/klog v1.0.0 18 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d 19 | ) 20 | 21 | require ( 22 | cloud.google.com/go v0.110.0 // indirect 23 | cloud.google.com/go/compute v1.18.0 // indirect 24 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 25 | cloud.google.com/go/iam v0.12.0 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 28 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 29 | github.com/fsnotify/fsnotify v1.4.9 // indirect 30 | github.com/go-logr/logr v1.2.3 // indirect 31 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 32 | github.com/go-openapi/jsonreference v0.20.0 // indirect 33 | github.com/go-openapi/swag v0.21.1 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 36 | github.com/golang/protobuf v1.5.3 // indirect 37 | github.com/google/gnostic v0.6.9 // indirect 38 | github.com/google/go-cmp v0.5.9 // indirect 39 | github.com/google/gofuzz v1.1.0 // indirect 40 | github.com/google/uuid v1.3.0 // indirect 41 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 42 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 43 | github.com/josharian/intern v1.0.0 // indirect 44 | github.com/json-iterator/go v1.1.12 // indirect 45 | github.com/mailru/easyjson v0.7.7 // indirect 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 47 | github.com/modern-go/reflect2 v1.0.2 // indirect 48 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 49 | github.com/nxadm/tail v1.4.8 // indirect 50 | github.com/pkg/errors v0.9.1 // indirect 51 | go.opencensus.io v0.24.0 // indirect 52 | golang.org/x/net v0.8.0 // indirect 53 | golang.org/x/sys v0.6.0 // indirect 54 | golang.org/x/term v0.6.0 // indirect 55 | golang.org/x/text v0.8.0 // indirect 56 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 57 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 58 | google.golang.org/appengine v1.6.7 // indirect 59 | google.golang.org/genproto v0.0.0-20230320184635-7606e756e683 // indirect 60 | google.golang.org/protobuf v1.29.1 // indirect 61 | gopkg.in/inf.v0 v0.9.1 // indirect 62 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | k8s.io/api v0.26.0 // indirect 66 | k8s.io/klog/v2 v2.80.1 // indirect 67 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 68 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 69 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 70 | sigs.k8s.io/yaml v1.3.0 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | 3 | apk add --update --no-cache bash 4 | go get -u k8s.io/code-generator@v0.24.0 5 | 6 | CLIENTSET_NAME_VERSIONED=clientset \ 7 | CLIENTSET_PKG_NAME=clientset \ 8 | bash "/go/pkg/mod/k8s.io/code-generator@v0.24.0/generate-groups.sh" deepcopy,client \ 9 | github.com/ofek/csi-gcs/pkg/client github.com/ofek/csi-gcs/pkg/apis \ 10 | "published-volume:v1beta1" 11 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: csi-gcs 2 | site_description: Kubernetes CSI driver for Google Cloud Storage 3 | site_author: Ofek Lev 4 | site_url: https://ofek.dev/csi-gcs/ 5 | repo_name: ofek/csi-gcs 6 | repo_url: https://github.com/ofek/csi-gcs 7 | edit_uri: blob/master/docs 8 | copyright: 'Copyright © Ofek Lev 2020-present' 9 | 10 | docs_dir: docs 11 | site_dir: site 12 | theme: 13 | name: material 14 | language: en 15 | features: 16 | - navigation.sections 17 | - navigation.tabs 18 | palette: 19 | - media: "(prefers-color-scheme: dark)" 20 | scheme: slate 21 | primary: blue 22 | accent: blue 23 | toggle: 24 | icon: material/weather-night 25 | name: Switch to light mode 26 | - media: "(prefers-color-scheme: light)" 27 | scheme: default 28 | primary: blue 29 | accent: blue 30 | toggle: 31 | icon: material/weather-sunny 32 | name: Switch to dark mode 33 | font: 34 | text: Roboto 35 | code: Roboto Mono 36 | icon: 37 | logo: material/book-open-page-variant 38 | repo: fontawesome/brands/github-alt 39 | favicon: assets/images/favicon.ico 40 | 41 | nav: 42 | - Home: 43 | - About: index.md 44 | - Getting started: getting_started.md 45 | - Static provisioning: static_provisioning.md 46 | - Dynamic provisioning: dynamic_provisioning.md 47 | - CSI Compatibility: csi_compatibility.md 48 | - Troubleshooting: troubleshooting.md 49 | - Contributing: 50 | - Setup: contributing/setup.md 51 | - Authors: contributing/authors.md 52 | 53 | plugins: 54 | # Built-in 55 | - search: 56 | # Extra 57 | - minify: 58 | minify_html: true 59 | - git-revision-date-localized: 60 | type: date 61 | 62 | markdown_extensions: 63 | # Built-in 64 | - markdown.extensions.abbr: 65 | - markdown.extensions.admonition: 66 | - markdown.extensions.attr_list: 67 | - markdown.extensions.footnotes: 68 | - markdown.extensions.meta: 69 | - markdown.extensions.tables: 70 | - markdown.extensions.toc: 71 | permalink: true 72 | toc_depth: "2-6" 73 | # Extra 74 | - mkpatcher: 75 | location: docs/.scripts 76 | - pymdownx.arithmatex: 77 | - pymdownx.betterem: 78 | smart_enable: all 79 | - pymdownx.caret: 80 | - pymdownx.critic: 81 | - pymdownx.details: 82 | - pymdownx.emoji: 83 | # https://github.com/twitter/twemoji 84 | # https://raw.githubusercontent.com/facelessuser/pymdown-extensions/master/pymdownx/twemoji_db.py 85 | emoji_index: !!python/name:materialx.emoji.twemoji 86 | emoji_generator: !!python/name:materialx.emoji.to_svg 87 | - pymdownx.highlight: 88 | guess_lang: false 89 | linenums_style: pymdownx-inline 90 | use_pygments: true 91 | - pymdownx.inlinehilite: 92 | - pymdownx.keys: 93 | - pymdownx.magiclink: 94 | repo_url_shortener: true 95 | repo_url_shorthand: true 96 | social_url_shortener: true 97 | social_url_shorthand: true 98 | normalize_issue_symbols: true 99 | provider: github 100 | user: ofek 101 | repo: csi-gcs 102 | - pymdownx.mark: 103 | - pymdownx.progressbar: 104 | - pymdownx.saneheaders: 105 | - pymdownx.smartsymbols: 106 | - pymdownx.snippets: 107 | check_paths: true 108 | base_path: 109 | - docs/.snippets 110 | - pymdownx.superfences: 111 | - pymdownx.tabbed: 112 | alternate_style: true 113 | - pymdownx.tasklist: 114 | custom_checkbox: true 115 | - pymdownx.tilde: 116 | 117 | extra: 118 | social: 119 | - icon: fontawesome/brands/docker 120 | link: https://hub.docker.com/r/ofekmeister/csi-gcs 121 | - icon: fontawesome/brands/github-alt 122 | link: https://github.com/ofek 123 | - icon: fontawesome/solid/blog 124 | link: https://ofek.dev/words/ 125 | - icon: fontawesome/brands/twitter 126 | link: https://twitter.com/Ofekmeister 127 | - icon: fontawesome/brands/linkedin 128 | link: https://www.linkedin.com/in/ofeklev/ 129 | extra_css: 130 | - assets/css/custom.css 131 | -------------------------------------------------------------------------------- /pkg/apis/published-volume/v1beta1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | // +groupName=gcs.csi.ofek.dev 4 | 5 | package v1beta1 6 | -------------------------------------------------------------------------------- /pkg/apis/published-volume/v1beta1/register.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | // Define your schema name and the version 10 | var SchemeGroupVersion = schema.GroupVersion{ 11 | Group: "gcs.csi.ofek.dev", 12 | Version: "v1beta1", 13 | } 14 | 15 | var ( 16 | SchemeBuilder runtime.SchemeBuilder 17 | localSchemeBuilder = &SchemeBuilder 18 | AddToScheme = localSchemeBuilder.AddToScheme 19 | ) 20 | 21 | func init() { 22 | // We only register manually written functions here. The registration of the 23 | // generated functions takes place in the generated files. The separation 24 | // makes the code compile even when the generated files are missing. 25 | localSchemeBuilder.Register(addKnownTypes) 26 | } 27 | 28 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 29 | func Resource(resource string) schema.GroupResource { 30 | return SchemeGroupVersion.WithResource(resource).GroupResource() 31 | } 32 | 33 | // Adds the list of known types to the given scheme. 34 | func addKnownTypes(scheme *runtime.Scheme) error { 35 | scheme.AddKnownTypes( 36 | SchemeGroupVersion, 37 | &PublishedVolume{}, 38 | &PublishedVolumeList{}, 39 | ) 40 | 41 | scheme.AddKnownTypes( 42 | SchemeGroupVersion, 43 | &metav1.Status{}, 44 | ) 45 | 46 | metav1.AddToGroupVersion( 47 | scheme, 48 | SchemeGroupVersion, 49 | ) 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/apis/published-volume/v1beta1/types.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | // +genclient 6 | // +genclient:nonNamespaced 7 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 8 | 9 | // PublishedVolume is a top-level type 10 | type PublishedVolume struct { 11 | metav1.TypeMeta `json:",inline"` 12 | // +optional 13 | metav1.ObjectMeta `json:"metadata,omitempty"` 14 | 15 | Spec PublishedVolumeSpec `json:"spec"` 16 | } 17 | 18 | type PublishedVolumeSpec struct { 19 | Node string `json:"node"` 20 | TargetPath string `json:"targetPath"` 21 | VolumeHandle string `json:"volumeHandle"` 22 | Options map[string]string `json:"options"` 23 | Pod PublishedVolumeSpecPod `json:"pod"` 24 | } 25 | 26 | type PublishedVolumeSpecPod struct { 27 | Name string `json:"name"` 28 | Namespace string `json:"namespace"` 29 | } 30 | 31 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 32 | 33 | type PublishedVolumeList struct { 34 | metav1.TypeMeta `json:",inline"` 35 | // +optional 36 | metav1.ListMeta `son:"metadata,omitempty"` 37 | 38 | Items []PublishedVolume `json:"items"` 39 | } 40 | -------------------------------------------------------------------------------- /pkg/apis/published-volume/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by deepcopy-gen. DO NOT EDIT. 21 | 22 | package v1beta1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *PublishedVolume) DeepCopyInto(out *PublishedVolume) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishedVolume. 38 | func (in *PublishedVolume) DeepCopy() *PublishedVolume { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(PublishedVolume) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *PublishedVolume) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *PublishedVolumeList) DeepCopyInto(out *PublishedVolumeList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]PublishedVolume, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishedVolumeList. 71 | func (in *PublishedVolumeList) DeepCopy() *PublishedVolumeList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(PublishedVolumeList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *PublishedVolumeList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *PublishedVolumeSpec) DeepCopyInto(out *PublishedVolumeSpec) { 90 | *out = *in 91 | if in.Options != nil { 92 | in, out := &in.Options, &out.Options 93 | *out = make(map[string]string, len(*in)) 94 | for key, val := range *in { 95 | (*out)[key] = val 96 | } 97 | } 98 | out.Pod = in.Pod 99 | return 100 | } 101 | 102 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishedVolumeSpec. 103 | func (in *PublishedVolumeSpec) DeepCopy() *PublishedVolumeSpec { 104 | if in == nil { 105 | return nil 106 | } 107 | out := new(PublishedVolumeSpec) 108 | in.DeepCopyInto(out) 109 | return out 110 | } 111 | 112 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 113 | func (in *PublishedVolumeSpecPod) DeepCopyInto(out *PublishedVolumeSpecPod) { 114 | *out = *in 115 | return 116 | } 117 | 118 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishedVolumeSpecPod. 119 | func (in *PublishedVolumeSpecPod) DeepCopy() *PublishedVolumeSpecPod { 120 | if in == nil { 121 | return nil 122 | } 123 | out := new(PublishedVolumeSpecPod) 124 | in.DeepCopyInto(out) 125 | return out 126 | } 127 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package clientset 20 | 21 | import ( 22 | "fmt" 23 | "net/http" 24 | 25 | gcsv1beta1 "github.com/ofek/csi-gcs/pkg/client/clientset/clientset/typed/published-volume/v1beta1" 26 | discovery "k8s.io/client-go/discovery" 27 | rest "k8s.io/client-go/rest" 28 | flowcontrol "k8s.io/client-go/util/flowcontrol" 29 | ) 30 | 31 | type Interface interface { 32 | Discovery() discovery.DiscoveryInterface 33 | GcsV1beta1() gcsv1beta1.GcsV1beta1Interface 34 | } 35 | 36 | // Clientset contains the clients for groups. Each group has exactly one 37 | // version included in a Clientset. 38 | type Clientset struct { 39 | *discovery.DiscoveryClient 40 | gcsV1beta1 *gcsv1beta1.GcsV1beta1Client 41 | } 42 | 43 | // GcsV1beta1 retrieves the GcsV1beta1Client 44 | func (c *Clientset) GcsV1beta1() gcsv1beta1.GcsV1beta1Interface { 45 | return c.gcsV1beta1 46 | } 47 | 48 | // Discovery retrieves the DiscoveryClient 49 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 50 | if c == nil { 51 | return nil 52 | } 53 | return c.DiscoveryClient 54 | } 55 | 56 | // NewForConfig creates a new Clientset for the given config. 57 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 58 | // NewForConfig will generate a rate-limiter in configShallowCopy. 59 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 60 | // where httpClient was generated with rest.HTTPClientFor(c). 61 | func NewForConfig(c *rest.Config) (*Clientset, error) { 62 | configShallowCopy := *c 63 | 64 | if configShallowCopy.UserAgent == "" { 65 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() 66 | } 67 | 68 | // share the transport between all clients 69 | httpClient, err := rest.HTTPClientFor(&configShallowCopy) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return NewForConfigAndClient(&configShallowCopy, httpClient) 75 | } 76 | 77 | // NewForConfigAndClient creates a new Clientset for the given config and http client. 78 | // Note the http client provided takes precedence over the configured transport values. 79 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 80 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. 81 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { 82 | configShallowCopy := *c 83 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 84 | if configShallowCopy.Burst <= 0 { 85 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 86 | } 87 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 88 | } 89 | 90 | var cs Clientset 91 | var err error 92 | cs.gcsV1beta1, err = gcsv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return &cs, nil 102 | } 103 | 104 | // NewForConfigOrDie creates a new Clientset for the given config and 105 | // panics if there is an error in the config. 106 | func NewForConfigOrDie(c *rest.Config) *Clientset { 107 | cs, err := NewForConfig(c) 108 | if err != nil { 109 | panic(err) 110 | } 111 | return cs 112 | } 113 | 114 | // New creates a new Clientset for the given RESTClient. 115 | func New(c rest.Interface) *Clientset { 116 | var cs Clientset 117 | cs.gcsV1beta1 = gcsv1beta1.New(c) 118 | 119 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 120 | return &cs 121 | } 122 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated clientset. 20 | package clientset 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | clientset "github.com/ofek/csi-gcs/pkg/client/clientset/clientset" 23 | gcsv1beta1 "github.com/ofek/csi-gcs/pkg/client/clientset/clientset/typed/published-volume/v1beta1" 24 | fakegcsv1beta1 "github.com/ofek/csi-gcs/pkg/client/clientset/clientset/typed/published-volume/v1beta1/fake" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/watch" 27 | "k8s.io/client-go/discovery" 28 | fakediscovery "k8s.io/client-go/discovery/fake" 29 | "k8s.io/client-go/testing" 30 | ) 31 | 32 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 33 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 34 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 35 | // for a real clientset and is mostly useful in simple unit tests. 36 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 37 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 38 | for _, obj := range objects { 39 | if err := o.Add(obj); err != nil { 40 | panic(err) 41 | } 42 | } 43 | 44 | cs := &Clientset{tracker: o} 45 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 46 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 47 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 48 | gvr := action.GetResource() 49 | ns := action.GetNamespace() 50 | watch, err := o.Watch(gvr, ns) 51 | if err != nil { 52 | return false, nil, err 53 | } 54 | return true, watch, nil 55 | }) 56 | 57 | return cs 58 | } 59 | 60 | // Clientset implements clientset.Interface. Meant to be embedded into a 61 | // struct to get a default implementation. This makes faking out just the method 62 | // you want to test easier. 63 | type Clientset struct { 64 | testing.Fake 65 | discovery *fakediscovery.FakeDiscovery 66 | tracker testing.ObjectTracker 67 | } 68 | 69 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 70 | return c.discovery 71 | } 72 | 73 | func (c *Clientset) Tracker() testing.ObjectTracker { 74 | return c.tracker 75 | } 76 | 77 | var ( 78 | _ clientset.Interface = &Clientset{} 79 | _ testing.FakeClient = &Clientset{} 80 | ) 81 | 82 | // GcsV1beta1 retrieves the GcsV1beta1Client 83 | func (c *Clientset) GcsV1beta1() gcsv1beta1.GcsV1beta1Interface { 84 | return &fakegcsv1beta1.FakeGcsV1beta1{Fake: &c.Fake} 85 | } 86 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | gcsv1beta1 "github.com/ofek/csi-gcs/pkg/apis/published-volume/v1beta1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var scheme = runtime.NewScheme() 31 | var codecs = serializer.NewCodecFactory(scheme) 32 | 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | gcsv1beta1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package scheme 20 | 21 | import ( 22 | gcsv1beta1 "github.com/ofek/csi-gcs/pkg/apis/published-volume/v1beta1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var Scheme = runtime.NewScheme() 31 | var Codecs = serializer.NewCodecFactory(Scheme) 32 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | gcsv1beta1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(Scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1beta1 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/fake/fake_published-volume_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1beta1 "github.com/ofek/csi-gcs/pkg/client/clientset/clientset/typed/published-volume/v1beta1" 23 | rest "k8s.io/client-go/rest" 24 | testing "k8s.io/client-go/testing" 25 | ) 26 | 27 | type FakeGcsV1beta1 struct { 28 | *testing.Fake 29 | } 30 | 31 | func (c *FakeGcsV1beta1) PublishedVolumes() v1beta1.PublishedVolumeInterface { 32 | return &FakePublishedVolumes{c} 33 | } 34 | 35 | // RESTClient returns a RESTClient that is used to communicate 36 | // with API server by this client implementation. 37 | func (c *FakeGcsV1beta1) RESTClient() rest.Interface { 38 | var ret *rest.RESTClient 39 | return ret 40 | } 41 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/fake/fake_publishedvolume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | 24 | v1beta1 "github.com/ofek/csi-gcs/pkg/apis/published-volume/v1beta1" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | schema "k8s.io/apimachinery/pkg/runtime/schema" 28 | types "k8s.io/apimachinery/pkg/types" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | testing "k8s.io/client-go/testing" 31 | ) 32 | 33 | // FakePublishedVolumes implements PublishedVolumeInterface 34 | type FakePublishedVolumes struct { 35 | Fake *FakeGcsV1beta1 36 | } 37 | 38 | var publishedvolumesResource = schema.GroupVersionResource{Group: "gcs.csi.ofek.dev", Version: "v1beta1", Resource: "publishedvolumes"} 39 | 40 | var publishedvolumesKind = schema.GroupVersionKind{Group: "gcs.csi.ofek.dev", Version: "v1beta1", Kind: "PublishedVolume"} 41 | 42 | // Get takes name of the publishedVolume, and returns the corresponding publishedVolume object, and an error if there is any. 43 | func (c *FakePublishedVolumes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.PublishedVolume, err error) { 44 | obj, err := c.Fake. 45 | Invokes(testing.NewRootGetAction(publishedvolumesResource, name), &v1beta1.PublishedVolume{}) 46 | if obj == nil { 47 | return nil, err 48 | } 49 | return obj.(*v1beta1.PublishedVolume), err 50 | } 51 | 52 | // List takes label and field selectors, and returns the list of PublishedVolumes that match those selectors. 53 | func (c *FakePublishedVolumes) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.PublishedVolumeList, err error) { 54 | obj, err := c.Fake. 55 | Invokes(testing.NewRootListAction(publishedvolumesResource, publishedvolumesKind, opts), &v1beta1.PublishedVolumeList{}) 56 | if obj == nil { 57 | return nil, err 58 | } 59 | 60 | label, _, _ := testing.ExtractFromListOptions(opts) 61 | if label == nil { 62 | label = labels.Everything() 63 | } 64 | list := &v1beta1.PublishedVolumeList{ListMeta: obj.(*v1beta1.PublishedVolumeList).ListMeta} 65 | for _, item := range obj.(*v1beta1.PublishedVolumeList).Items { 66 | if label.Matches(labels.Set(item.Labels)) { 67 | list.Items = append(list.Items, item) 68 | } 69 | } 70 | return list, err 71 | } 72 | 73 | // Watch returns a watch.Interface that watches the requested publishedVolumes. 74 | func (c *FakePublishedVolumes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 75 | return c.Fake. 76 | InvokesWatch(testing.NewRootWatchAction(publishedvolumesResource, opts)) 77 | } 78 | 79 | // Create takes the representation of a publishedVolume and creates it. Returns the server's representation of the publishedVolume, and an error, if there is any. 80 | func (c *FakePublishedVolumes) Create(ctx context.Context, publishedVolume *v1beta1.PublishedVolume, opts v1.CreateOptions) (result *v1beta1.PublishedVolume, err error) { 81 | obj, err := c.Fake. 82 | Invokes(testing.NewRootCreateAction(publishedvolumesResource, publishedVolume), &v1beta1.PublishedVolume{}) 83 | if obj == nil { 84 | return nil, err 85 | } 86 | return obj.(*v1beta1.PublishedVolume), err 87 | } 88 | 89 | // Update takes the representation of a publishedVolume and updates it. Returns the server's representation of the publishedVolume, and an error, if there is any. 90 | func (c *FakePublishedVolumes) Update(ctx context.Context, publishedVolume *v1beta1.PublishedVolume, opts v1.UpdateOptions) (result *v1beta1.PublishedVolume, err error) { 91 | obj, err := c.Fake. 92 | Invokes(testing.NewRootUpdateAction(publishedvolumesResource, publishedVolume), &v1beta1.PublishedVolume{}) 93 | if obj == nil { 94 | return nil, err 95 | } 96 | return obj.(*v1beta1.PublishedVolume), err 97 | } 98 | 99 | // Delete takes name of the publishedVolume and deletes it. Returns an error if one occurs. 100 | func (c *FakePublishedVolumes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 101 | _, err := c.Fake. 102 | Invokes(testing.NewRootDeleteActionWithOptions(publishedvolumesResource, name, opts), &v1beta1.PublishedVolume{}) 103 | return err 104 | } 105 | 106 | // DeleteCollection deletes a collection of objects. 107 | func (c *FakePublishedVolumes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 108 | action := testing.NewRootDeleteCollectionAction(publishedvolumesResource, listOpts) 109 | 110 | _, err := c.Fake.Invokes(action, &v1beta1.PublishedVolumeList{}) 111 | return err 112 | } 113 | 114 | // Patch applies the patch and returns the patched publishedVolume. 115 | func (c *FakePublishedVolumes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.PublishedVolume, err error) { 116 | obj, err := c.Fake. 117 | Invokes(testing.NewRootPatchSubresourceAction(publishedvolumesResource, name, pt, data, subresources...), &v1beta1.PublishedVolume{}) 118 | if obj == nil { 119 | return nil, err 120 | } 121 | return obj.(*v1beta1.PublishedVolume), err 122 | } 123 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1beta1 20 | 21 | type PublishedVolumeExpansion interface{} 22 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/published-volume_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1beta1 20 | 21 | import ( 22 | "net/http" 23 | 24 | v1beta1 "github.com/ofek/csi-gcs/pkg/apis/published-volume/v1beta1" 25 | "github.com/ofek/csi-gcs/pkg/client/clientset/clientset/scheme" 26 | rest "k8s.io/client-go/rest" 27 | ) 28 | 29 | type GcsV1beta1Interface interface { 30 | RESTClient() rest.Interface 31 | PublishedVolumesGetter 32 | } 33 | 34 | // GcsV1beta1Client is used to interact with features provided by the gcs.csi.ofek.dev group. 35 | type GcsV1beta1Client struct { 36 | restClient rest.Interface 37 | } 38 | 39 | func (c *GcsV1beta1Client) PublishedVolumes() PublishedVolumeInterface { 40 | return newPublishedVolumes(c) 41 | } 42 | 43 | // NewForConfig creates a new GcsV1beta1Client for the given config. 44 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 45 | // where httpClient was generated with rest.HTTPClientFor(c). 46 | func NewForConfig(c *rest.Config) (*GcsV1beta1Client, error) { 47 | config := *c 48 | if err := setConfigDefaults(&config); err != nil { 49 | return nil, err 50 | } 51 | httpClient, err := rest.HTTPClientFor(&config) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return NewForConfigAndClient(&config, httpClient) 56 | } 57 | 58 | // NewForConfigAndClient creates a new GcsV1beta1Client for the given config and http client. 59 | // Note the http client provided takes precedence over the configured transport values. 60 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*GcsV1beta1Client, error) { 61 | config := *c 62 | if err := setConfigDefaults(&config); err != nil { 63 | return nil, err 64 | } 65 | client, err := rest.RESTClientForConfigAndClient(&config, h) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &GcsV1beta1Client{client}, nil 70 | } 71 | 72 | // NewForConfigOrDie creates a new GcsV1beta1Client for the given config and 73 | // panics if there is an error in the config. 74 | func NewForConfigOrDie(c *rest.Config) *GcsV1beta1Client { 75 | client, err := NewForConfig(c) 76 | if err != nil { 77 | panic(err) 78 | } 79 | return client 80 | } 81 | 82 | // New creates a new GcsV1beta1Client for the given RESTClient. 83 | func New(c rest.Interface) *GcsV1beta1Client { 84 | return &GcsV1beta1Client{c} 85 | } 86 | 87 | func setConfigDefaults(config *rest.Config) error { 88 | gv := v1beta1.SchemeGroupVersion 89 | config.GroupVersion = &gv 90 | config.APIPath = "/apis" 91 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 92 | 93 | if config.UserAgent == "" { 94 | config.UserAgent = rest.DefaultKubernetesUserAgent() 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // RESTClient returns a RESTClient that is used to communicate 101 | // with API server by this client implementation. 102 | func (c *GcsV1beta1Client) RESTClient() rest.Interface { 103 | if c == nil { 104 | return nil 105 | } 106 | return c.restClient 107 | } 108 | -------------------------------------------------------------------------------- /pkg/client/clientset/clientset/typed/published-volume/v1beta1/publishedvolume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1beta1 20 | 21 | import ( 22 | "context" 23 | "time" 24 | 25 | v1beta1 "github.com/ofek/csi-gcs/pkg/apis/published-volume/v1beta1" 26 | scheme "github.com/ofek/csi-gcs/pkg/client/clientset/clientset/scheme" 27 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | types "k8s.io/apimachinery/pkg/types" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | rest "k8s.io/client-go/rest" 31 | ) 32 | 33 | // PublishedVolumesGetter has a method to return a PublishedVolumeInterface. 34 | // A group's client should implement this interface. 35 | type PublishedVolumesGetter interface { 36 | PublishedVolumes() PublishedVolumeInterface 37 | } 38 | 39 | // PublishedVolumeInterface has methods to work with PublishedVolume resources. 40 | type PublishedVolumeInterface interface { 41 | Create(ctx context.Context, publishedVolume *v1beta1.PublishedVolume, opts v1.CreateOptions) (*v1beta1.PublishedVolume, error) 42 | Update(ctx context.Context, publishedVolume *v1beta1.PublishedVolume, opts v1.UpdateOptions) (*v1beta1.PublishedVolume, error) 43 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 44 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 45 | Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.PublishedVolume, error) 46 | List(ctx context.Context, opts v1.ListOptions) (*v1beta1.PublishedVolumeList, error) 47 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 48 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.PublishedVolume, err error) 49 | PublishedVolumeExpansion 50 | } 51 | 52 | // publishedVolumes implements PublishedVolumeInterface 53 | type publishedVolumes struct { 54 | client rest.Interface 55 | } 56 | 57 | // newPublishedVolumes returns a PublishedVolumes 58 | func newPublishedVolumes(c *GcsV1beta1Client) *publishedVolumes { 59 | return &publishedVolumes{ 60 | client: c.RESTClient(), 61 | } 62 | } 63 | 64 | // Get takes name of the publishedVolume, and returns the corresponding publishedVolume object, and an error if there is any. 65 | func (c *publishedVolumes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.PublishedVolume, err error) { 66 | result = &v1beta1.PublishedVolume{} 67 | err = c.client.Get(). 68 | Resource("publishedvolumes"). 69 | Name(name). 70 | VersionedParams(&options, scheme.ParameterCodec). 71 | Do(ctx). 72 | Into(result) 73 | return 74 | } 75 | 76 | // List takes label and field selectors, and returns the list of PublishedVolumes that match those selectors. 77 | func (c *publishedVolumes) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.PublishedVolumeList, err error) { 78 | var timeout time.Duration 79 | if opts.TimeoutSeconds != nil { 80 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 81 | } 82 | result = &v1beta1.PublishedVolumeList{} 83 | err = c.client.Get(). 84 | Resource("publishedvolumes"). 85 | VersionedParams(&opts, scheme.ParameterCodec). 86 | Timeout(timeout). 87 | Do(ctx). 88 | Into(result) 89 | return 90 | } 91 | 92 | // Watch returns a watch.Interface that watches the requested publishedVolumes. 93 | func (c *publishedVolumes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 94 | var timeout time.Duration 95 | if opts.TimeoutSeconds != nil { 96 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 97 | } 98 | opts.Watch = true 99 | return c.client.Get(). 100 | Resource("publishedvolumes"). 101 | VersionedParams(&opts, scheme.ParameterCodec). 102 | Timeout(timeout). 103 | Watch(ctx) 104 | } 105 | 106 | // Create takes the representation of a publishedVolume and creates it. Returns the server's representation of the publishedVolume, and an error, if there is any. 107 | func (c *publishedVolumes) Create(ctx context.Context, publishedVolume *v1beta1.PublishedVolume, opts v1.CreateOptions) (result *v1beta1.PublishedVolume, err error) { 108 | result = &v1beta1.PublishedVolume{} 109 | err = c.client.Post(). 110 | Resource("publishedvolumes"). 111 | VersionedParams(&opts, scheme.ParameterCodec). 112 | Body(publishedVolume). 113 | Do(ctx). 114 | Into(result) 115 | return 116 | } 117 | 118 | // Update takes the representation of a publishedVolume and updates it. Returns the server's representation of the publishedVolume, and an error, if there is any. 119 | func (c *publishedVolumes) Update(ctx context.Context, publishedVolume *v1beta1.PublishedVolume, opts v1.UpdateOptions) (result *v1beta1.PublishedVolume, err error) { 120 | result = &v1beta1.PublishedVolume{} 121 | err = c.client.Put(). 122 | Resource("publishedvolumes"). 123 | Name(publishedVolume.Name). 124 | VersionedParams(&opts, scheme.ParameterCodec). 125 | Body(publishedVolume). 126 | Do(ctx). 127 | Into(result) 128 | return 129 | } 130 | 131 | // Delete takes name of the publishedVolume and deletes it. Returns an error if one occurs. 132 | func (c *publishedVolumes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 133 | return c.client.Delete(). 134 | Resource("publishedvolumes"). 135 | Name(name). 136 | Body(&opts). 137 | Do(ctx). 138 | Error() 139 | } 140 | 141 | // DeleteCollection deletes a collection of objects. 142 | func (c *publishedVolumes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 143 | var timeout time.Duration 144 | if listOpts.TimeoutSeconds != nil { 145 | timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second 146 | } 147 | return c.client.Delete(). 148 | Resource("publishedvolumes"). 149 | VersionedParams(&listOpts, scheme.ParameterCodec). 150 | Timeout(timeout). 151 | Body(&opts). 152 | Do(ctx). 153 | Error() 154 | } 155 | 156 | // Patch applies the patch and returns the patched publishedVolume. 157 | func (c *publishedVolumes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.PublishedVolume, err error) { 158 | result = &v1beta1.PublishedVolume{} 159 | err = c.client.Patch(pt). 160 | Resource("publishedvolumes"). 161 | Name(name). 162 | SubResource(subresources...). 163 | VersionedParams(&opts, scheme.ParameterCodec). 164 | Body(data). 165 | Do(ctx). 166 | Into(result) 167 | return 168 | } 169 | -------------------------------------------------------------------------------- /pkg/driver/constants.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | const ( 4 | CSIDriverName = "gcs.csi.ofek.dev" 5 | BucketMountPath = "/var/lib/kubelet/pods" 6 | KeyStoragePath = "/tmp/keys" 7 | DefaultGid = 63147 8 | DefaultDirMode = 0775 9 | DefaultFileMode = 0664 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/driver/controller.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "cloud.google.com/go/storage" 8 | "github.com/container-storage-interface/spec/lib/go/csi" 9 | "github.com/kubernetes-csi/csi-lib-utils/protosanitizer" 10 | "github.com/ofek/csi-gcs/pkg/flags" 11 | "github.com/ofek/csi-gcs/pkg/util" 12 | "golang.org/x/oauth2/google" 13 | "google.golang.org/api/option" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | "k8s.io/klog" 17 | ) 18 | 19 | func (d *GCSDriver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { 20 | klog.V(4).Infof("Method CreateVolume called with: %s", protosanitizer.StripSecrets(req)) 21 | 22 | if req.Name == "" { 23 | return nil, status.Error(codes.InvalidArgument, "missing name") 24 | } 25 | if len(req.VolumeCapabilities) == 0 { 26 | return nil, status.Error(codes.InvalidArgument, "missing volume capabilities") 27 | } 28 | 29 | for _, capability := range req.GetVolumeCapabilities() { 30 | if capability.GetMount() != nil && capability.GetBlock() == nil { 31 | continue 32 | } 33 | return nil, status.Error(codes.InvalidArgument, "Only volumeMode Filesystem is supported") 34 | } 35 | 36 | // Default Options 37 | var options = map[string]string{ 38 | "bucket": util.BucketName(req.Name), 39 | "location": "US", 40 | "kmsKeyId": "", 41 | } 42 | 43 | // Merge Secret Options 44 | options = flags.MergeSecret(options, req.Secrets) 45 | 46 | // Merge MountFlag Options 47 | for _, capability := range req.GetVolumeCapabilities() { 48 | options = flags.MergeMountOptions(options, capability.GetMount().GetMountFlags()) 49 | } 50 | 51 | // Merge PVC Annotation Options 52 | pvcName, pvcNameSelected := req.Parameters["csi.storage.k8s.io/pvc/name"] 53 | pvcNamespace, pvcNamespaceSelected := req.Parameters["csi.storage.k8s.io/pvc/namespace"] 54 | 55 | var pvcAnnotations = map[string]string{} 56 | 57 | if pvcNameSelected && pvcNamespaceSelected { 58 | loadedPvcAnnotations, err := util.GetPvcAnnotations(ctx, pvcName, pvcNamespace) 59 | if err != nil { 60 | return nil, status.Errorf(codes.Internal, "Failed to load PersistentVolumeClaim: %v", err) 61 | } 62 | 63 | pvcAnnotations = loadedPvcAnnotations 64 | } 65 | options = flags.MergeAnnotations(options, pvcAnnotations) 66 | 67 | // Merge Context 68 | if req.Parameters != nil { 69 | options = flags.MergeAnnotations(options, req.Parameters) 70 | } 71 | 72 | var clientOpt option.ClientOption 73 | if len(req.Secrets) == 0 { 74 | // Find default credentials 75 | creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadOnly) 76 | if err != nil { 77 | return nil, err 78 | } 79 | clientOpt = option.WithCredentials(creds) 80 | } else { 81 | // Retrieve Secret Key 82 | keyFile, err := util.GetKey(req.Secrets, KeyStoragePath) 83 | if err != nil { 84 | return nil, err 85 | } 86 | clientOpt = option.WithCredentialsFile(keyFile) 87 | defer util.CleanupKey(keyFile, KeyStoragePath) 88 | } 89 | 90 | // Creates a client. 91 | client, err := storage.NewClient(ctx, clientOpt) 92 | if err != nil { 93 | return nil, status.Errorf(codes.Internal, "Failed to create client: %v", err) 94 | } 95 | 96 | // Creates a Bucket instance. 97 | bucket := client.Bucket(options[flags.FLAG_BUCKET]) 98 | 99 | // Check if Bucket Exists 100 | _, err = bucket.Attrs(ctx) 101 | if err == nil { 102 | klog.V(2).Infof("Bucket '%s' exists", options[flags.FLAG_BUCKET]) 103 | } else { 104 | klog.V(2).Infof("Bucket '%s' does not exist, creating", options[flags.FLAG_BUCKET]) 105 | 106 | projectId, projectIdExists := options[flags.FLAG_PROJECT_ID] 107 | if !projectIdExists { 108 | return nil, status.Errorf(codes.InvalidArgument, "Project Id not provided, bucket can't be created: %s", options[flags.FLAG_BUCKET]) 109 | } 110 | if err := bucket.Create(ctx, projectId, &storage.BucketAttrs{Location: options[flags.FLAG_LOCATION], 111 | Encryption: &storage.BucketEncryption{DefaultKMSKeyName: options[flags.FLAG_KMS_KEY_ID]}}); err != nil { 112 | return nil, status.Errorf(codes.Internal, "Failed to create bucket: %v", err) 113 | } 114 | } 115 | 116 | // Get Capacity 117 | bucketAttrs, err := bucket.Attrs(ctx) 118 | if err != nil { 119 | return nil, status.Errorf(codes.Internal, "Failed to get bucket attrs: %v", err) 120 | } 121 | 122 | existingCapacity, err := util.BucketCapacity(bucketAttrs) 123 | if err != nil { 124 | return nil, status.Errorf(codes.Internal, "Failed to get bucket capacity: %v", err) 125 | } 126 | 127 | // Check / Set Capacity 128 | newCapacity := int64(req.GetCapacityRange().GetRequiredBytes()) 129 | if existingCapacity == 0 { 130 | _, err = util.SetBucketCapacity(ctx, bucket, newCapacity) 131 | if err != nil { 132 | return nil, status.Errorf(codes.Internal, "Failed to set bucket capacity: %v", err) 133 | } 134 | } else if existingCapacity < newCapacity { 135 | return nil, status.Error(codes.AlreadyExists, fmt.Sprintf("Volume with the same name: %s but with smaller size already exist", options[flags.FLAG_BUCKET])) 136 | } 137 | 138 | return &csi.CreateVolumeResponse{ 139 | Volume: &csi.Volume{ 140 | VolumeId: options[flags.FLAG_BUCKET], 141 | VolumeContext: options, 142 | CapacityBytes: newCapacity, 143 | }, 144 | }, nil 145 | } 146 | 147 | func (d *GCSDriver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { 148 | klog.V(4).Infof("Method DeleteVolume called with: %s", protosanitizer.StripSecrets(req)) 149 | 150 | if req.VolumeId == "" { 151 | return nil, status.Error(codes.InvalidArgument, "missing volume id") 152 | } 153 | 154 | var clientOpt option.ClientOption 155 | if len(req.Secrets) == 0 { 156 | // Find default credentials 157 | creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadOnly) 158 | if err != nil { 159 | return nil, err 160 | } 161 | clientOpt = option.WithCredentials(creds) 162 | } else { 163 | // Retrieve Secret Key 164 | keyFile, err := util.GetKey(req.Secrets, KeyStoragePath) 165 | if err != nil { 166 | return nil, err 167 | } 168 | clientOpt = option.WithCredentialsFile(keyFile) 169 | defer util.CleanupKey(keyFile, KeyStoragePath) 170 | } 171 | 172 | // Creates a client. 173 | client, err := storage.NewClient(ctx, clientOpt) 174 | if err != nil { 175 | return nil, status.Errorf(codes.Internal, "Failed to create client: %v", err) 176 | } 177 | 178 | // Creates a Bucket instance. 179 | bucket := client.Bucket(req.VolumeId) 180 | 181 | _, err = bucket.Attrs(ctx) 182 | if err == nil { 183 | if err := bucket.Delete(ctx); err != nil { 184 | return nil, status.Errorf(codes.Internal, "Error deleting bucket %s, %v", req.VolumeId, err) 185 | } 186 | } else { 187 | klog.V(2).Infof("Bucket '%s' does not exist, not deleting", req.VolumeId) 188 | } 189 | 190 | return &csi.DeleteVolumeResponse{}, nil 191 | } 192 | 193 | func (d *GCSDriver) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) { 194 | klog.V(4).Infof("Method ControllerGetCapabilities called with: %s", protosanitizer.StripSecrets(req)) 195 | 196 | return &csi.ControllerGetCapabilitiesResponse{ 197 | Capabilities: []*csi.ControllerServiceCapability{ 198 | { 199 | Type: &csi.ControllerServiceCapability_Rpc{ 200 | Rpc: &csi.ControllerServiceCapability_RPC{ 201 | Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, 202 | }, 203 | }, 204 | }, 205 | { 206 | Type: &csi.ControllerServiceCapability_Rpc{ 207 | Rpc: &csi.ControllerServiceCapability_RPC{ 208 | Type: csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, 209 | }, 210 | }, 211 | }, 212 | }, 213 | }, nil 214 | } 215 | 216 | func (d *GCSDriver) ValidateVolumeCapabilities(ctx context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) { 217 | klog.V(4).Infof("Method ValidateVolumeCapabilities called with: %s", protosanitizer.StripSecrets(req)) 218 | 219 | if req.VolumeId == "" { 220 | return nil, status.Error(codes.InvalidArgument, "missing volume id") 221 | } 222 | if len(req.VolumeCapabilities) == 0 { 223 | return nil, status.Error(codes.InvalidArgument, "missing volume capabilities") 224 | } 225 | 226 | bucketName := req.VolumeId 227 | 228 | var clientOpt option.ClientOption 229 | if len(req.Secrets) == 0 { 230 | // Find default credentials 231 | creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadOnly) 232 | if err != nil { 233 | return nil, err 234 | } 235 | clientOpt = option.WithCredentials(creds) 236 | } else { 237 | // Retrieve Secret Key 238 | keyFile, err := util.GetKey(req.Secrets, KeyStoragePath) 239 | if err != nil { 240 | return nil, err 241 | } 242 | clientOpt = option.WithCredentialsFile(keyFile) 243 | defer util.CleanupKey(keyFile, KeyStoragePath) 244 | } 245 | 246 | // Creates a client. 247 | client, err := storage.NewClient(ctx, clientOpt) 248 | if err != nil { 249 | return nil, status.Errorf(codes.Internal, "Failed to create client: %v", err) 250 | } 251 | 252 | // Creates a Bucket instance. 253 | bucket := client.Bucket(bucketName) 254 | 255 | _, err = bucket.Attrs(ctx) 256 | 257 | if err != nil { 258 | return nil, status.Error(codes.NotFound, "volume does not exist") 259 | } 260 | 261 | for _, capability := range req.GetVolumeCapabilities() { 262 | if capability.GetMount() != nil && capability.GetBlock() == nil { 263 | continue 264 | } 265 | return &csi.ValidateVolumeCapabilitiesResponse{Message: "Only volumeMode Filesystem is supported"}, nil 266 | } 267 | 268 | return &csi.ValidateVolumeCapabilitiesResponse{ 269 | Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ 270 | 271 | VolumeContext: req.GetVolumeContext(), 272 | VolumeCapabilities: req.GetVolumeCapabilities(), 273 | Parameters: req.GetParameters(), 274 | }, 275 | }, nil 276 | } 277 | 278 | func (d *GCSDriver) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { 279 | klog.V(4).Infof("Method ControllerPublishVolume called with: %s", protosanitizer.StripSecrets(req)) 280 | 281 | return nil, status.Error(codes.Unimplemented, "") 282 | } 283 | 284 | func (d *GCSDriver) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) { 285 | klog.V(4).Infof("Method ControllerUnpublishVolume called with: %s", protosanitizer.StripSecrets(req)) 286 | 287 | return nil, status.Error(codes.Unimplemented, "") 288 | } 289 | 290 | func (d *GCSDriver) GetCapacity(ctx context.Context, req *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) { 291 | klog.V(4).Infof("Method GetCapacity called with: %s", protosanitizer.StripSecrets(req)) 292 | 293 | return nil, status.Error(codes.Unimplemented, "") 294 | } 295 | 296 | func (d *GCSDriver) ListVolumes(ctx context.Context, req *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) { 297 | klog.V(4).Infof("Method ListVolumes called with: %s", protosanitizer.StripSecrets(req)) 298 | 299 | return nil, status.Error(codes.Unimplemented, "") 300 | } 301 | 302 | func (d *GCSDriver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { 303 | klog.V(4).Infof("Method CreateSnapshot called with: %s", protosanitizer.StripSecrets(req)) 304 | 305 | return nil, status.Error(codes.Unimplemented, "") 306 | } 307 | 308 | func (d *GCSDriver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { 309 | klog.V(4).Infof("Method DeleteSnapshot called with: %s", protosanitizer.StripSecrets(req)) 310 | 311 | return nil, status.Error(codes.Unimplemented, "") 312 | } 313 | 314 | func (d *GCSDriver) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { 315 | klog.V(4).Infof("Method ListSnapshots called with: %s", protosanitizer.StripSecrets(req)) 316 | 317 | return nil, status.Error(codes.Unimplemented, "") 318 | } 319 | 320 | func (d *GCSDriver) ControllerGetVolume(ctx context.Context, req *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) { 321 | klog.V(4).Infof("Method ControllerGetVolume called with: %s", protosanitizer.StripSecrets(req)) 322 | 323 | return nil, status.Error(codes.Unimplemented, "") 324 | } 325 | 326 | func (d *GCSDriver) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { 327 | klog.V(4).Infof("Method ControllerExpandVolume called with: %s", protosanitizer.StripSecrets(req)) 328 | 329 | if req.VolumeId == "" { 330 | return nil, status.Error(codes.InvalidArgument, "missing volume id") 331 | } 332 | 333 | var clientOpt option.ClientOption 334 | if len(req.Secrets) == 0 { 335 | // Find default credentials 336 | creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadOnly) 337 | if err != nil { 338 | return nil, err 339 | } 340 | clientOpt = option.WithCredentials(creds) 341 | } else { 342 | // Retrieve Secret Key 343 | keyFile, err := util.GetKey(req.Secrets, KeyStoragePath) 344 | if err != nil { 345 | return nil, err 346 | } 347 | clientOpt = option.WithCredentialsFile(keyFile) 348 | defer util.CleanupKey(keyFile, KeyStoragePath) 349 | } 350 | 351 | // Creates a client. 352 | client, err := storage.NewClient(ctx, clientOpt) 353 | if err != nil { 354 | return nil, status.Errorf(codes.Internal, "Failed to create client: %v", err) 355 | } 356 | 357 | // Creates a Bucket instance. 358 | bucket := client.Bucket(req.VolumeId) 359 | 360 | // Check if Bucket Exists 361 | _, err = bucket.Attrs(ctx) 362 | if err == nil { 363 | klog.V(2).Infof("Bucket '%s' exists", req.VolumeId) 364 | } else { 365 | return nil, status.Errorf(codes.NotFound, "Bucket '%s' does not exist", req.VolumeId) 366 | } 367 | 368 | // Get Capacity 369 | bucketAttrs, err := bucket.Attrs(ctx) 370 | if err != nil { 371 | return nil, status.Errorf(codes.Internal, "Failed to get bucket attrs: %v", err) 372 | } 373 | 374 | existingCapacity, err := util.BucketCapacity(bucketAttrs) 375 | if err != nil { 376 | return nil, status.Errorf(codes.Internal, "Failed to get bucket capacity: %v", err) 377 | } 378 | 379 | // Check / Set Capacity 380 | newCapacity := int64(req.GetCapacityRange().GetRequiredBytes()) 381 | if newCapacity > existingCapacity { 382 | _, err = util.SetBucketCapacity(ctx, bucket, newCapacity) 383 | if err != nil { 384 | return nil, status.Errorf(codes.Internal, "Failed to set bucket capacity: %v", err) 385 | } 386 | } 387 | 388 | return &csi.ControllerExpandVolumeResponse{ 389 | CapacityBytes: newCapacity, 390 | NodeExpansionRequired: false, 391 | }, nil 392 | } 393 | -------------------------------------------------------------------------------- /pkg/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | 8 | "github.com/container-storage-interface/spec/lib/go/csi" 9 | "google.golang.org/grpc" 10 | "k8s.io/klog" 11 | 12 | "github.com/ofek/csi-gcs/pkg/util" 13 | 14 | "k8s.io/utils/mount" 15 | ) 16 | 17 | type GCSDriver struct { 18 | name string 19 | nodeName string 20 | endpoint string 21 | mountPoint string 22 | version string 23 | server *grpc.Server 24 | mounter mount.Interface 25 | deleteOrphanedPods bool 26 | } 27 | 28 | func NewGCSDriver(name, node, endpoint string, version string, deleteOrphanedPods bool) (*GCSDriver, error) { 29 | return &GCSDriver{ 30 | name: name, 31 | nodeName: node, 32 | endpoint: endpoint, 33 | mountPoint: BucketMountPath, 34 | version: version, 35 | mounter: mount.New(""), 36 | deleteOrphanedPods: deleteOrphanedPods, 37 | }, nil 38 | } 39 | 40 | func (d *GCSDriver) Run() error { 41 | ctx := context.TODO() 42 | 43 | // set the driver-ready label to false at the beginning to handle edge-case where the controller didn't terminated gracefully 44 | if err := util.SetDriverReadyLabel(ctx, d.name, d.nodeName, false); err != nil { 45 | klog.Warningf("Unable to set driver-ready=false label on the node, error: %v", err) 46 | } 47 | 48 | if len(d.mountPoint) == 0 { 49 | return errors.New("--bucket-mount-path is required") 50 | } 51 | 52 | scheme, address, err := util.ParseEndpoint(d.endpoint) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | listener, err := net.Listen(scheme, address) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | logHandler := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 63 | resp, err := handler(ctx, req) 64 | if err == nil { 65 | klog.V(4).Infof("Method %s completed", info.FullMethod) 66 | } else { 67 | klog.Errorf("Method %s failed with error: %v", info.FullMethod, err) 68 | } 69 | return resp, err 70 | } 71 | 72 | if d.deleteOrphanedPods { 73 | err = d.RunPodCleanup() 74 | 75 | if err != nil { 76 | klog.Errorf("RunPodCleanup failed with error: %v", err) 77 | } 78 | } 79 | 80 | klog.V(1).Infof("Starting Google Cloud Storage CSI Driver - driver: `%s`, version: `%s`, gRPC socket: `%s`", d.name, d.version, d.endpoint) 81 | d.server = grpc.NewServer(grpc.UnaryInterceptor(logHandler)) 82 | csi.RegisterIdentityServer(d.server, d) 83 | csi.RegisterNodeServer(d.server, d) 84 | csi.RegisterControllerServer(d.server, d) 85 | if err = util.SetDriverReadyLabel(ctx, d.name, d.nodeName, true); err != nil { 86 | klog.Warningf("unable to set driver-ready=true label on the node, error: %v", err) 87 | } 88 | return d.server.Serve(listener) 89 | } 90 | 91 | func (d *GCSDriver) stop() { 92 | ctx := context.TODO() 93 | 94 | d.server.Stop() 95 | if err := util.SetDriverReadyLabel(ctx, d.name, d.nodeName, false); err != nil { 96 | klog.Warningf("Unable to set driver-ready=false label on the node, error: %v", err) 97 | } 98 | klog.V(1).Info("CSI driver stopped") 99 | } 100 | 101 | func (d *GCSDriver) RunPodCleanup() (err error) { 102 | ctx := context.TODO() 103 | 104 | publishedVolumes, err := util.GetRegisteredMounts(ctx, d.nodeName) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | for _, publishedVolume := range publishedVolumes.Items { 110 | // Killing Pod because its Volume is no longer mounted 111 | err = util.DeletePod(ctx, publishedVolume.Spec.Pod.Namespace, publishedVolume.Spec.Pod.Name) 112 | if err == nil { 113 | klog.V(4).Infof("Deleted Pod %s/%s because its volume was no longer mounted", publishedVolume.Spec.Pod.Namespace, publishedVolume.Spec.Pod.Name) 114 | } else { 115 | klog.Errorf("Could not delete pod %s/%s because it was no longer mounted because of error: %v", publishedVolume.Spec.Pod.Namespace, publishedVolume.Spec.Pod.Name, err) 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/driver/driver_suite_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestDriver(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Driver Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/driver/identity.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/container-storage-interface/spec/lib/go/csi" 7 | "k8s.io/klog" 8 | ) 9 | 10 | func (d *GCSDriver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { 11 | klog.V(4).Infof("Method GetPluginInfo called with: %+v", req) 12 | 13 | return &csi.GetPluginInfoResponse{ 14 | Name: d.name, 15 | VendorVersion: driverVersion, 16 | }, nil 17 | } 18 | 19 | func (d *GCSDriver) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { 20 | klog.V(4).Infof("Method GetPluginCapabilities called with: %+v", req) 21 | 22 | return &csi.GetPluginCapabilitiesResponse{ 23 | Capabilities: []*csi.PluginCapability{ 24 | { 25 | Type: &csi.PluginCapability_Service_{ 26 | Service: &csi.PluginCapability_Service{ 27 | Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, nil 33 | } 34 | 35 | func (d *GCSDriver) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) { 36 | klog.V(4).Infof("Method Probe called with: %+v", req) 37 | 38 | return &csi.ProbeResponse{}, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/driver/node.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "cloud.google.com/go/storage" 11 | "github.com/container-storage-interface/spec/lib/go/csi" 12 | "github.com/kubernetes-csi/csi-lib-utils/protosanitizer" 13 | "github.com/ofek/csi-gcs/pkg/flags" 14 | "github.com/ofek/csi-gcs/pkg/util" 15 | "golang.org/x/oauth2/google" 16 | "google.golang.org/api/option" 17 | "google.golang.org/grpc/codes" 18 | "google.golang.org/grpc/status" 19 | "k8s.io/klog" 20 | "k8s.io/utils/mount" 21 | ) 22 | 23 | func (driver *GCSDriver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { 24 | klog.V(4).Infof("Method NodePublishVolume called with: %s", protosanitizer.StripSecrets(req)) 25 | 26 | if req.GetVolumeId() == "" { 27 | return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") 28 | } 29 | 30 | if req.TargetPath == "" { 31 | return nil, status.Error(codes.InvalidArgument, "Target path missing in request") 32 | } 33 | 34 | if req.VolumeCapability == nil { 35 | return nil, status.Error(codes.InvalidArgument, "NodePublishVolume Volume Capability must be provided") 36 | } 37 | 38 | if req.VolumeCapability.GetMount() == nil || req.VolumeCapability.GetBlock() != nil { 39 | return nil, status.Error(codes.InvalidArgument, "Only volumeMode Filesystem is supported") 40 | } 41 | 42 | // Default Options 43 | var options = map[string]string{ 44 | "bucket": req.GetVolumeId(), 45 | "gid": strconv.FormatInt(DefaultGid, 10), 46 | "dirMode": "0" + strconv.FormatInt(DefaultDirMode, 8), 47 | "fileMode": "0" + strconv.FormatInt(DefaultFileMode, 8), 48 | } 49 | 50 | // Merge Secret Options 51 | options = flags.MergeSecret(options, req.Secrets) 52 | 53 | // Merge MountFlag Options 54 | options = flags.MergeMountOptions(options, req.GetVolumeCapability().GetMount().GetMountFlags()) 55 | 56 | // Merge Volume Context 57 | if req.VolumeContext != nil { 58 | options = flags.MergeFlags(options, req.VolumeContext) 59 | } 60 | 61 | var clientOpt option.ClientOption 62 | keyFile := "" 63 | if len(req.Secrets) == 0 { 64 | // Find default credentials 65 | creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadOnly) 66 | if err != nil { 67 | return nil, err 68 | } 69 | clientOpt = option.WithCredentials(creds) 70 | } else { 71 | // Retrieve Secret Key 72 | var err error 73 | keyFile, err = util.GetKey(req.Secrets, KeyStoragePath) 74 | if err != nil { 75 | return nil, err 76 | } 77 | clientOpt = option.WithCredentialsFile(keyFile) 78 | } 79 | 80 | // Creates a client. 81 | client, err := storage.NewClient(ctx, clientOpt) 82 | if err != nil { 83 | return nil, status.Errorf(codes.Internal, "Failed to create client: %v", err) 84 | } 85 | 86 | // Creates a Bucket instance. 87 | bucket := client.Bucket(options[flags.FLAG_BUCKET]) 88 | 89 | bucketExists, err := util.BucketExists(ctx, bucket) 90 | if err != nil { 91 | return nil, status.Errorf(codes.Internal, "Failed to check if bucket exists: %v", err) 92 | } 93 | if !bucketExists { 94 | return nil, status.Errorf(codes.NotFound, "Bucket %s does not exist", options[flags.FLAG_BUCKET]) 95 | } 96 | 97 | notMnt, err := driver.mounter.IsLikelyNotMountPoint(req.TargetPath) 98 | if err != nil { 99 | if os.IsNotExist(err) { 100 | if err := os.MkdirAll(req.TargetPath, 0750); err != nil { 101 | return nil, status.Error(codes.Internal, err.Error()) 102 | } 103 | notMnt = true 104 | } else { 105 | return nil, status.Error(codes.Internal, err.Error()) 106 | } 107 | } 108 | 109 | if !notMnt { 110 | return &csi.NodePublishVolumeResponse{}, nil 111 | } 112 | 113 | mountOptions := []string{"allow_other"} 114 | if keyFile != "" { 115 | mountOptions = append(mountOptions, fmt.Sprintf("key_file=%s", keyFile)) 116 | } 117 | mountOptions = append(mountOptions, flags.ExtraFlags(options)...) 118 | if req.GetReadonly() { 119 | mountOptions = append(mountOptions, "ro") 120 | } 121 | 122 | err = driver.mounter.Mount(options[flags.FLAG_BUCKET], req.TargetPath, "gcsfuse", mountOptions) 123 | if err != nil { 124 | if os.IsPermission(err) { 125 | return nil, status.Error(codes.PermissionDenied, err.Error()) 126 | } 127 | if strings.Contains(err.Error(), "invalid argument") { 128 | return nil, status.Error(codes.InvalidArgument, err.Error()) 129 | } 130 | return nil, status.Error(codes.Internal, err.Error()) 131 | } 132 | 133 | if driver.deleteOrphanedPods { 134 | err = util.RegisterMount( 135 | ctx, 136 | req.VolumeId, 137 | req.TargetPath, 138 | driver.nodeName, 139 | req.VolumeContext["csi.storage.k8s.io/pod.namespace"], 140 | req.VolumeContext["csi.storage.k8s.io/pod.name"], 141 | options, 142 | ) 143 | if err != nil { 144 | return nil, err 145 | } 146 | } 147 | 148 | return &csi.NodePublishVolumeResponse{}, nil 149 | } 150 | 151 | func (driver *GCSDriver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (response *csi.NodeUnpublishVolumeResponse, err error) { 152 | klog.V(4).Infof("Method NodeUnpublishVolume called with: %s", protosanitizer.StripSecrets(req)) 153 | 154 | // Check arguments 155 | if len(req.GetVolumeId()) == 0 { 156 | return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") 157 | } 158 | if len(req.GetTargetPath()) == 0 { 159 | return nil, status.Error(codes.InvalidArgument, "Target path missing in request") 160 | } 161 | 162 | notMnt, err := driver.mounter.IsLikelyNotMountPoint(req.TargetPath) 163 | 164 | if err != nil { 165 | if os.IsNotExist(err) { 166 | return &csi.NodeUnpublishVolumeResponse{}, nil 167 | } 168 | // This error happens when the node container is restarted and the connection is lost 169 | if strings.Contains(err.Error(), "transport endpoint is not connected") { 170 | notMnt = false 171 | } else { 172 | return nil, status.Error(codes.Internal, err.Error()) 173 | } 174 | } 175 | if notMnt { 176 | return &csi.NodeUnpublishVolumeResponse{}, nil 177 | } 178 | 179 | err = mount.CleanupMountPoint(req.GetTargetPath(), driver.mounter, false) 180 | if err != nil { 181 | return nil, status.Error(codes.Internal, err.Error()) 182 | } 183 | 184 | if driver.deleteOrphanedPods { 185 | err = util.UnregisterMount(ctx, req.VolumeId, req.TargetPath, driver.nodeName) 186 | if err != nil { 187 | klog.Error(err) 188 | } 189 | } 190 | 191 | return &csi.NodeUnpublishVolumeResponse{}, nil 192 | } 193 | 194 | func (driver *GCSDriver) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { 195 | klog.V(4).Infof("Method NodeGetInfo called with: %s", protosanitizer.StripSecrets(req)) 196 | 197 | return &csi.NodeGetInfoResponse{NodeId: driver.nodeName}, nil 198 | } 199 | 200 | func (driver *GCSDriver) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { 201 | klog.V(4).Infof("Method NodeGetCapabilities called with: %s", protosanitizer.StripSecrets(req)) 202 | 203 | return &csi.NodeGetCapabilitiesResponse{Capabilities: []*csi.NodeServiceCapability{ 204 | { 205 | Type: &csi.NodeServiceCapability_Rpc{ 206 | Rpc: &csi.NodeServiceCapability_RPC{ 207 | Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, 208 | }, 209 | }, 210 | }, 211 | }}, nil 212 | } 213 | 214 | func (driver *GCSDriver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { 215 | klog.V(4).Infof("Method NodeStageVolume called with: %s", protosanitizer.StripSecrets(req)) 216 | 217 | return nil, status.Errorf(codes.Unimplemented, "NodeStageVolume: not implemented by %s", driver.name) 218 | } 219 | 220 | func (driver *GCSDriver) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { 221 | klog.V(4).Infof("Method NodeUnstageVolume called with: %s", protosanitizer.StripSecrets(req)) 222 | 223 | return nil, status.Errorf(codes.Unimplemented, "NodeUnstageVolume: not implemented by %s", driver.name) 224 | } 225 | 226 | func (driver *GCSDriver) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { 227 | klog.V(4).Infof("Method NodeGetVolumeStats called with: %s", protosanitizer.StripSecrets(req)) 228 | 229 | return nil, status.Errorf(codes.Unimplemented, "NodeGetVolumeStats: not implemented by %s", driver.name) 230 | } 231 | 232 | func (driver *GCSDriver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { 233 | klog.V(4).Infof("Method NodeExpandVolume called with: %s", protosanitizer.StripSecrets(req)) 234 | 235 | // Check arguments 236 | if len(req.GetVolumeId()) == 0 { 237 | return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") 238 | } 239 | if len(req.GetVolumePath()) == 0 { 240 | return nil, status.Error(codes.InvalidArgument, "Volume path missing in request") 241 | } 242 | 243 | notMnt, err := driver.mounter.IsLikelyNotMountPoint(req.GetVolumePath()) 244 | 245 | if err != nil { 246 | if os.IsNotExist(err) { 247 | return nil, status.Error(codes.NotFound, "Targetpath not found") 248 | } else { 249 | return nil, status.Error(codes.Internal, err.Error()) 250 | } 251 | } 252 | if notMnt { 253 | return nil, status.Error(codes.NotFound, "Volume not mounted") 254 | } 255 | 256 | return &csi.NodeExpandVolumeResponse{}, nil 257 | } 258 | -------------------------------------------------------------------------------- /pkg/driver/version.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | ) 8 | 9 | // These will be set at build time. 10 | var ( 11 | driverVersion string 12 | ) 13 | 14 | type VersionInfo struct { 15 | DriverVersion string `json:"driverVersion"` 16 | GoVersion string `json:"goVersion"` 17 | Compiler string `json:"compiler"` 18 | Platform string `json:"platform"` 19 | } 20 | 21 | func GetVersion() VersionInfo { 22 | return VersionInfo{ 23 | DriverVersion: driverVersion, 24 | GoVersion: runtime.Version(), 25 | Compiler: runtime.Compiler, 26 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 27 | } 28 | } 29 | 30 | func GetVersionJSON() (string, error) { 31 | versionInfo := GetVersion() 32 | 33 | marshalled, err := json.MarshalIndent(&versionInfo, "", " ") 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | return string(marshalled), nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/flags/flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "flag" 5 | "strconv" 6 | "strings" 7 | 8 | "k8s.io/klog" 9 | ) 10 | 11 | const ( 12 | FLAG_BUCKET = "bucket" 13 | FLAG_PROJECT_ID = "projectId" 14 | FLAG_KMS_KEY_ID = "kmsKeyId" 15 | FLAG_LOCATION = "location" 16 | FLAG_FUSE_MOUNT_OPTION = "fuseMountOptions" 17 | FLAG_DIR_MODE = "dirMode" 18 | FLAG_FILE_MODE = "fileMode" 19 | FLAG_UID = "uid" 20 | FLAG_GID = "gid" 21 | FLAG_IMPLICIT_DIRS = "implicitDirs" 22 | FLAG_BILLING_PROJECT = "billingProject" 23 | FLAG_LIMIT_BYTES_PER_SEC = "limitBytesPerSec" 24 | FLAG_LIMIT_OPS_PER_SEC = "limitOpsPerSec" 25 | FLAG_STAT_CACHE_TTL = "statCacheTTL" 26 | FLAG_TYPE_CACHE_TTL = "typeCacheTTL" 27 | FLAG_MAX_RETRY_SLEEP = "maxRetrySleep" 28 | 29 | ANNOTATION_PREFIX = "gcs.csi.ofek.dev/" 30 | 31 | ANNOTATION_BUCKET = "gcs.csi.ofek.dev/bucket" 32 | ANNOTATION_PROJECT_ID = "gcs.csi.ofek.dev/project-id" 33 | ANNOTATION_KMS_KEY_ID = "gcs.csi.ofek.dev/kms-key-id" 34 | ANNOTATION_LOCATION = "gcs.csi.ofek.dev/location" 35 | ANNOTATION_FUSE_MOUNT_OPTION = "gcs.csi.ofek.dev/fuse-mount-options" 36 | ANNOTATION_DIR_MODE = "gcs.csi.ofek.dev/dir-mode" 37 | ANNOTATION_FILE_MODE = "gcs.csi.ofek.dev/file-mode" 38 | ANNOTATION_UID = "gcs.csi.ofek.dev/uid" 39 | ANNOTATION_GID = "gcs.csi.ofek.dev/gid" 40 | ANNOTATION_IMPLICIT_DIRS = "gcs.csi.ofek.dev/implicit-dirs" 41 | ANNOTATION_BILLING_PROJECT = "gcs.csi.ofek.dev/billing-project" 42 | ANNOTATION_LIMIT_BYTES_PER_SEC = "gcs.csi.ofek.dev/limit-bytes-per-sec" 43 | ANNOTATION_LIMIT_OPS_PER_SEC = "gcs.csi.ofek.dev/limit-ops-per-sec" 44 | ANNOTATION_STAT_CACHE_TTL = "gcs.csi.ofek.dev/stat-cache-ttl" 45 | ANNOTATION_TYPE_CACHE_TTL = "gcs.csi.ofek.dev/type-cache-ttl" 46 | ANNOTATION_MAX_RETRY_SLEEP = "gcs.csi.ofek.dev/max-retry-sleep" 47 | 48 | MOUNT_OPTION_BUCKET = "bucket" 49 | MOUNT_OPTION_PROJECT_ID = "project-id" 50 | MOUNT_OPTION_KMS_KEY_ID = "kms-key-id" 51 | MOUNT_OPTION_LOCATION = "location" 52 | MOUNT_OPTION_FUSE_MOUNT_OPTION = "fuse-mount-option" 53 | MOUNT_OPTION_DIR_MODE = "dir-mode" 54 | MOUNT_OPTION_FILE_MODE = "file-mode" 55 | MOUNT_OPTION_UID = "uid" 56 | MOUNT_OPTION_GID = "gid" 57 | MOUNT_OPTION_IMPLICIT_DIRS = "implicit-dirs" 58 | MOUNT_OPTION_BILLING_PROJECT = "billing-project" 59 | MOUNT_OPTION_LIMIT_BYTES_PER_SEC = "limit-bytes-per-sec" 60 | MOUNT_OPTION_LIMIT_OPS_PER_SEC = "limit-ops-per-sec" 61 | MOUNT_OPTION_STAT_CACHE_TTL = "stat-cache-ttl" 62 | MOUNT_OPTION_TYPE_CACHE_TTL = "type-cache-ttl" 63 | MOUNT_OPTION_MAX_RETRY_SLEEP = "max-retry-sleep" 64 | ) 65 | 66 | func IsFlag(flag string) bool { 67 | switch flag { 68 | case FLAG_BUCKET: 69 | return true 70 | case FLAG_PROJECT_ID: 71 | return true 72 | case FLAG_KMS_KEY_ID: 73 | return true 74 | case FLAG_LOCATION: 75 | return true 76 | case FLAG_FUSE_MOUNT_OPTION: 77 | return true 78 | case FLAG_DIR_MODE: 79 | return true 80 | case FLAG_FILE_MODE: 81 | return true 82 | case FLAG_UID: 83 | return true 84 | case FLAG_GID: 85 | return true 86 | case FLAG_IMPLICIT_DIRS: 87 | return true 88 | case FLAG_BILLING_PROJECT: 89 | return true 90 | case FLAG_LIMIT_BYTES_PER_SEC: 91 | return true 92 | case FLAG_LIMIT_OPS_PER_SEC: 93 | return true 94 | case FLAG_STAT_CACHE_TTL: 95 | return true 96 | case FLAG_TYPE_CACHE_TTL: 97 | return true 98 | case FLAG_MAX_RETRY_SLEEP: 99 | return true 100 | } 101 | return false 102 | } 103 | 104 | func FlagNameFromAnnotation(annotation string) string { 105 | switch annotation { 106 | case ANNOTATION_BUCKET: 107 | return FLAG_BUCKET 108 | case ANNOTATION_PROJECT_ID: 109 | return FLAG_PROJECT_ID 110 | case ANNOTATION_KMS_KEY_ID: 111 | return FLAG_KMS_KEY_ID 112 | case ANNOTATION_LOCATION: 113 | return FLAG_LOCATION 114 | case ANNOTATION_FUSE_MOUNT_OPTION: 115 | return FLAG_FUSE_MOUNT_OPTION 116 | case ANNOTATION_DIR_MODE: 117 | return FLAG_DIR_MODE 118 | case ANNOTATION_FILE_MODE: 119 | return FLAG_FILE_MODE 120 | case ANNOTATION_UID: 121 | return FLAG_UID 122 | case ANNOTATION_GID: 123 | return FLAG_GID 124 | case ANNOTATION_IMPLICIT_DIRS: 125 | return FLAG_IMPLICIT_DIRS 126 | case ANNOTATION_BILLING_PROJECT: 127 | return FLAG_BILLING_PROJECT 128 | case ANNOTATION_LIMIT_BYTES_PER_SEC: 129 | return FLAG_LIMIT_BYTES_PER_SEC 130 | case ANNOTATION_LIMIT_OPS_PER_SEC: 131 | return FLAG_LIMIT_OPS_PER_SEC 132 | case ANNOTATION_STAT_CACHE_TTL: 133 | return FLAG_STAT_CACHE_TTL 134 | case ANNOTATION_TYPE_CACHE_TTL: 135 | return FLAG_TYPE_CACHE_TTL 136 | case ANNOTATION_MAX_RETRY_SLEEP: 137 | return FLAG_MAX_RETRY_SLEEP 138 | } 139 | return "" 140 | } 141 | 142 | func IsOwnAnnotation(annotation string) bool { 143 | return strings.HasPrefix(annotation, ANNOTATION_PREFIX) 144 | } 145 | 146 | func IsAnnotation(annotation string) bool { 147 | return FlagNameFromAnnotation(annotation) != "" 148 | } 149 | 150 | func FlagNameFromMountOption(cmd string) string { 151 | switch cmd { 152 | case MOUNT_OPTION_BUCKET: 153 | return FLAG_BUCKET 154 | case MOUNT_OPTION_PROJECT_ID: 155 | return FLAG_PROJECT_ID 156 | case MOUNT_OPTION_KMS_KEY_ID: 157 | return FLAG_KMS_KEY_ID 158 | case MOUNT_OPTION_LOCATION: 159 | return FLAG_LOCATION 160 | case MOUNT_OPTION_FUSE_MOUNT_OPTION: 161 | return FLAG_FUSE_MOUNT_OPTION 162 | case MOUNT_OPTION_DIR_MODE: 163 | return FLAG_DIR_MODE 164 | case MOUNT_OPTION_FILE_MODE: 165 | return FLAG_FILE_MODE 166 | case MOUNT_OPTION_UID: 167 | return FLAG_UID 168 | case MOUNT_OPTION_GID: 169 | return FLAG_GID 170 | case MOUNT_OPTION_IMPLICIT_DIRS: 171 | return FLAG_IMPLICIT_DIRS 172 | case MOUNT_OPTION_BILLING_PROJECT: 173 | return FLAG_BILLING_PROJECT 174 | case MOUNT_OPTION_LIMIT_BYTES_PER_SEC: 175 | return FLAG_LIMIT_BYTES_PER_SEC 176 | case MOUNT_OPTION_LIMIT_OPS_PER_SEC: 177 | return FLAG_LIMIT_OPS_PER_SEC 178 | case MOUNT_OPTION_STAT_CACHE_TTL: 179 | return FLAG_STAT_CACHE_TTL 180 | case MOUNT_OPTION_TYPE_CACHE_TTL: 181 | return FLAG_TYPE_CACHE_TTL 182 | case MOUNT_OPTION_MAX_RETRY_SLEEP: 183 | return FLAG_MAX_RETRY_SLEEP 184 | } 185 | return "" 186 | } 187 | 188 | func IsMountOption(cmd string) bool { 189 | return FlagNameFromMountOption(cmd) != "" 190 | } 191 | 192 | func MergeFlags(a map[string]string, b map[string]string) (result map[string]string) { 193 | result = a 194 | 195 | for k, v := range b { 196 | if !IsFlag(k) { 197 | klog.Warningf("Flag %s unknown", k) 198 | continue 199 | } 200 | result[k] = v 201 | } 202 | 203 | return result 204 | } 205 | 206 | func MergeSecret(a map[string]string, b map[string]string) (result map[string]string) { 207 | result = a 208 | 209 | for k, v := range b { 210 | if !IsFlag(k) { 211 | continue 212 | } 213 | result[k] = v 214 | } 215 | 216 | return result 217 | } 218 | 219 | func MergeAnnotations(a map[string]string, b map[string]string) (result map[string]string) { 220 | result = a 221 | 222 | for k, v := range b { 223 | if !IsOwnAnnotation(k) { 224 | continue 225 | } 226 | if !IsAnnotation(k) { 227 | klog.Warningf("Annotation %s unknown", k) 228 | continue 229 | } 230 | result[FlagNameFromAnnotation(k)] = v 231 | } 232 | 233 | return result 234 | } 235 | 236 | type fuseMountOptions []string 237 | 238 | func (i *fuseMountOptions) String() string { 239 | return "" 240 | } 241 | 242 | func (i *fuseMountOptions) Set(value string) error { 243 | *i = append(*i, value) 244 | return nil 245 | } 246 | 247 | type octalInt int64 248 | 249 | func (i *octalInt) String() string { 250 | return "" 251 | } 252 | 253 | func (i *octalInt) Set(value string) error { 254 | parsedInt, err := strconv.ParseInt(value, 8, 64) 255 | 256 | *i = octalInt(parsedInt) 257 | 258 | return err 259 | } 260 | 261 | func MergeMountOptions(a map[string]string, b []string) (result map[string]string) { 262 | var ( 263 | args = flag.NewFlagSet("csi-gcs", flag.ContinueOnError) 264 | bucket string 265 | projectId string 266 | kmsKeyId string 267 | location string 268 | fuseMountOptions fuseMountOptions 269 | dirMode octalInt = -1 270 | fileMode octalInt = -1 271 | uid int64 272 | gid int64 273 | implicitDirs bool 274 | billingProject string 275 | limitBytesPerSec int64 276 | limitOpsPerSec int64 277 | statCacheTTL string 278 | typeCacheTTL string 279 | maxRetrySleepMin int64 280 | ) 281 | 282 | args.StringVar(&bucket, MOUNT_OPTION_BUCKET, "", "Bucket Name") 283 | args.StringVar(&projectId, MOUNT_OPTION_PROJECT_ID, "", "Project ID of Bucket") 284 | args.StringVar(&kmsKeyId, MOUNT_OPTION_KMS_KEY_ID, "", "KMS encryption key ID. (projects/my-pet-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key)") 285 | args.StringVar(&location, MOUNT_OPTION_LOCATION, "", "Bucket Location") 286 | args.Var(&fuseMountOptions, MOUNT_OPTION_FUSE_MOUNT_OPTION, "") 287 | args.Var(&dirMode, MOUNT_OPTION_DIR_MODE, "Permission bits for directories, in octal. (default: 0775)") 288 | args.Var(&fileMode, MOUNT_OPTION_FILE_MODE, "Permission bits for files, in octal. (default: 0664)") 289 | args.Int64Var(&uid, MOUNT_OPTION_UID, -1, "UID owner of all inodes. (default: -1)") 290 | args.Int64Var(&gid, MOUNT_OPTION_GID, -1, "GID owner of all inodes. (default: -1)") 291 | args.BoolVar(&implicitDirs, MOUNT_OPTION_IMPLICIT_DIRS, false, "Implicitly define directories based on content.") 292 | args.StringVar(&billingProject, MOUNT_OPTION_BILLING_PROJECT, "", "Project to use for billing when accessing requester pays buckets.") 293 | args.Int64Var(&limitBytesPerSec, MOUNT_OPTION_LIMIT_BYTES_PER_SEC, -1, "Bandwidth limit for reading data, measured over a 30-second window.") 294 | args.Int64Var(&limitOpsPerSec, MOUNT_OPTION_LIMIT_OPS_PER_SEC, -1, "Operations per second limit, measured over a 30-second window.") 295 | args.StringVar(&statCacheTTL, MOUNT_OPTION_STAT_CACHE_TTL, "", "How long to cache StatObject results and inode attributes.") 296 | args.StringVar(&typeCacheTTL, MOUNT_OPTION_TYPE_CACHE_TTL, "", "How long to cache name -> file/dir mappings in directory inodes.") 297 | args.Int64Var(&maxRetrySleepMin, MOUNT_OPTION_MAX_RETRY_SLEEP, -1, "The maximum duration allowed to sleep in a retry loop with exponential backoff for failed requests to GCS backend. Once the backoff duration exceeds this limit, the retry stops. The default is 1 minute. A value of 0 disables retries.") 298 | 299 | err := args.Parse(b) 300 | if err != nil { 301 | klog.Warningf("%s", err) 302 | } 303 | 304 | result = a 305 | 306 | if bucket != "" { 307 | result[FLAG_BUCKET] = bucket 308 | } 309 | 310 | if projectId != "" { 311 | result[FLAG_PROJECT_ID] = projectId 312 | } 313 | 314 | if kmsKeyId != "" { 315 | result[FLAG_KMS_KEY_ID] = kmsKeyId 316 | } 317 | 318 | if location != "" { 319 | result[FLAG_LOCATION] = location 320 | } 321 | 322 | if len(fuseMountOptions) != 0 { 323 | result[FLAG_FUSE_MOUNT_OPTION] = strings.Join(fuseMountOptions, ",") 324 | } 325 | 326 | if dirMode != -1 { 327 | result[FLAG_DIR_MODE] = "0" + strconv.FormatInt(int64(dirMode), 8) 328 | } 329 | 330 | if fileMode != -1 { 331 | result[FLAG_FILE_MODE] = "0" + strconv.FormatInt(int64(fileMode), 8) 332 | } 333 | 334 | if uid != -1 { 335 | result[FLAG_UID] = strconv.FormatInt(uid, 10) 336 | } 337 | 338 | if gid != -1 { 339 | result[FLAG_GID] = strconv.FormatInt(gid, 10) 340 | } 341 | 342 | if implicitDirs { 343 | result[FLAG_IMPLICIT_DIRS] = "true" 344 | } 345 | 346 | if billingProject != "" { 347 | result[FLAG_BILLING_PROJECT] = billingProject 348 | } 349 | 350 | if limitBytesPerSec != -1 { 351 | result[FLAG_LIMIT_BYTES_PER_SEC] = strconv.FormatInt(limitBytesPerSec, 10) 352 | } 353 | 354 | if limitOpsPerSec != -1 { 355 | result[FLAG_LIMIT_OPS_PER_SEC] = strconv.FormatInt(limitOpsPerSec, 10) 356 | } 357 | 358 | if statCacheTTL != "" { 359 | result[FLAG_STAT_CACHE_TTL] = statCacheTTL 360 | } 361 | 362 | if typeCacheTTL != "" { 363 | result[FLAG_TYPE_CACHE_TTL] = typeCacheTTL 364 | } 365 | 366 | if maxRetrySleepMin != -1 { 367 | result[FLAG_MAX_RETRY_SLEEP] = strconv.FormatInt(maxRetrySleepMin, 10) 368 | } 369 | 370 | return result 371 | } 372 | 373 | func FlagNameToGcsfuseOption(flag string) string { 374 | switch flag { 375 | case FLAG_DIR_MODE: 376 | return "dir_mode" 377 | case FLAG_FILE_MODE: 378 | return "file_mode" 379 | case FLAG_UID: 380 | return "uid" 381 | case FLAG_GID: 382 | return "gid" 383 | case FLAG_IMPLICIT_DIRS: 384 | return "implicit_dirs" 385 | case FLAG_BILLING_PROJECT: 386 | return "billing_project" 387 | case FLAG_LIMIT_BYTES_PER_SEC: 388 | return "limit_bytes_per_sec" 389 | case FLAG_LIMIT_OPS_PER_SEC: 390 | return "limit_ops_per_sec" 391 | case FLAG_STAT_CACHE_TTL: 392 | return "stat_cache_ttl" 393 | case FLAG_TYPE_CACHE_TTL: 394 | return "type_cache_ttl" 395 | case FLAG_MAX_RETRY_SLEEP: 396 | return "max_retry_sleep" 397 | } 398 | return "" 399 | } 400 | 401 | func MaybeAddFlag(result []string, flags map[string]string, name string) []string { 402 | argName := FlagNameToGcsfuseOption(name) 403 | 404 | value, found := flags[name] 405 | if found { 406 | result = append(result, argName+"="+value) 407 | } 408 | return result 409 | } 410 | func MaybeAddBooleanFlag(result []string, flags map[string]string, name string) []string { 411 | argName := FlagNameToGcsfuseOption(name) 412 | 413 | value, found := flags[name] 414 | if found && value == "true" { 415 | result = append(result, argName) 416 | } 417 | return result 418 | } 419 | 420 | func MaybeAddDirectFlag(result []string, flags map[string]string, name string) []string { 421 | value, found := flags[name] 422 | if found { 423 | result = append(result, strings.Split(value, ",")...) 424 | } 425 | return result 426 | } 427 | 428 | func ExtraFlags(flags map[string]string) (result []string) { 429 | result = []string{} 430 | 431 | result = MaybeAddDirectFlag(result, flags, FLAG_FUSE_MOUNT_OPTION) 432 | result = MaybeAddFlag(result, flags, FLAG_DIR_MODE) 433 | result = MaybeAddFlag(result, flags, FLAG_FILE_MODE) 434 | result = MaybeAddFlag(result, flags, FLAG_UID) 435 | result = MaybeAddFlag(result, flags, FLAG_GID) 436 | result = MaybeAddBooleanFlag(result, flags, FLAG_IMPLICIT_DIRS) 437 | result = MaybeAddFlag(result, flags, FLAG_LIMIT_BYTES_PER_SEC) 438 | result = MaybeAddFlag(result, flags, FLAG_LIMIT_OPS_PER_SEC) 439 | result = MaybeAddFlag(result, flags, FLAG_STAT_CACHE_TTL) 440 | result = MaybeAddFlag(result, flags, FLAG_TYPE_CACHE_TTL) 441 | result = MaybeAddFlag(result, flags, FLAG_MAX_RETRY_SLEEP) 442 | 443 | return result 444 | } 445 | -------------------------------------------------------------------------------- /pkg/flags/flags_suite_test.go: -------------------------------------------------------------------------------- 1 | package flags_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFlags(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Flags Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/flags/flags_test.go: -------------------------------------------------------------------------------- 1 | package flags_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/ofek/csi-gcs/pkg/flags" 8 | ) 9 | 10 | var _ = Describe("Flags", func() { 11 | 12 | Describe("MergeFlags", func() { 13 | It("Should Merge", func() { 14 | Expect( 15 | MergeFlags( 16 | map[string]string{ 17 | "bucket": "test", 18 | "location": "US", 19 | }, 20 | map[string]string{ 21 | "bucket": "test2", 22 | "projectId": "csi-gcs", 23 | "foo": "bar", 24 | }, 25 | ), 26 | ).To(Equal(map[string]string{ 27 | "bucket": "test2", 28 | "location": "US", 29 | "projectId": "csi-gcs", 30 | })) 31 | }) 32 | }) 33 | Describe("MergeAnnotations", func() { 34 | It("Should Merge", func() { 35 | Expect( 36 | MergeAnnotations( 37 | map[string]string{ 38 | "bucket": "test", 39 | "location": "US", 40 | }, 41 | map[string]string{ 42 | "gcs.csi.ofek.dev/bucket": "test2", 43 | "gcs.csi.ofek.dev/project-id": "csi-gcs", 44 | "gcs.csi.ofek.dev/foo": "bar", 45 | }, 46 | ), 47 | ).To(Equal(map[string]string{ 48 | "bucket": "test2", 49 | "location": "US", 50 | "projectId": "csi-gcs", 51 | })) 52 | }) 53 | }) 54 | Describe("MergeMountOptions", func() { 55 | It("Should Merge", func() { 56 | Expect( 57 | MergeMountOptions( 58 | map[string]string{ 59 | "bucket": "test", 60 | "location": "US", 61 | }, 62 | []string{"--bucket=test2", 63 | "--project-id=csi-gcs", 64 | "--implicit-dirs", 65 | "--dir-mode=0600", 66 | "--file-mode=600", 67 | "--fuse-mount-option=foo,bar", 68 | "--fuse-mount-option=baz", 69 | }, 70 | ), 71 | ).To(Equal(map[string]string{ 72 | "bucket": "test2", 73 | "implicitDirs": "true", 74 | "dirMode": "0600", 75 | "fileMode": "0600", 76 | "location": "US", 77 | "fuseMountOptions": "foo,bar,baz", 78 | "projectId": "csi-gcs", 79 | })) 80 | }) 81 | }) 82 | Describe("ExtraFlags", func() { 83 | It("Should Merge", func() { 84 | Expect( 85 | ExtraFlags( 86 | map[string]string{ 87 | "bucket": "test", 88 | "location": "US", 89 | "fuseMountOptions": "foo,bar,baz", 90 | "implicitDirs": "true", 91 | "dirMode": "0600", 92 | }, 93 | ), 94 | ).To(Equal([]string{"foo", "bar", "baz", "dir_mode=0600", "implicit_dirs"})) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /pkg/util/common.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "hash/crc32" 8 | "io/ioutil" 9 | "net/url" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | 16 | "cloud.google.com/go/storage" 17 | "github.com/ofek/csi-gcs/pkg/apis/published-volume/v1beta1" 18 | gcs "github.com/ofek/csi-gcs/pkg/client/clientset/clientset" 19 | "google.golang.org/api/iterator" 20 | "google.golang.org/grpc/codes" 21 | "google.golang.org/grpc/status" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/apimachinery/pkg/types" 25 | "k8s.io/client-go/kubernetes" 26 | "k8s.io/client-go/rest" 27 | "k8s.io/klog" 28 | ) 29 | 30 | func ParseEndpoint(endpoint string) (string, string, error) { 31 | u, err := url.Parse(endpoint) 32 | if err != nil { 33 | return "", "", fmt.Errorf("could not parse endpoint: %v", err) 34 | } 35 | 36 | var address string 37 | if len(u.Host) == 0 { 38 | address = filepath.FromSlash(u.Path) 39 | } else { 40 | address = path.Join(u.Host, filepath.FromSlash(u.Path)) 41 | } 42 | 43 | scheme := strings.ToLower(u.Scheme) 44 | if scheme == "unix" { 45 | if err := os.Remove(address); err != nil && !os.IsNotExist(err) { 46 | return "", "", fmt.Errorf("could not remove unix socket %q: %v", address, err) 47 | } 48 | } else { 49 | return "", "", fmt.Errorf("unsupported protocol: %s", scheme) 50 | } 51 | 52 | return scheme, address, nil 53 | } 54 | 55 | func CreateFile(path, contents string) (string, error) { 56 | tmpFile, err := ioutil.TempFile(path, "") 57 | if err != nil { 58 | return "", fmt.Errorf("error creating file: %s", err) 59 | } 60 | 61 | filePath := tmpFile.Name() 62 | fileContents := []byte(contents) 63 | 64 | if _, err := tmpFile.Write(fileContents); err != nil { 65 | return "", fmt.Errorf("error writing to file %s: %s", filePath, err) 66 | } 67 | 68 | if err := tmpFile.Close(); err != nil { 69 | return "", fmt.Errorf("error closing file %s: %s", filePath, err) 70 | } 71 | 72 | return filePath, nil 73 | } 74 | 75 | func CreateDir(d string) error { 76 | stat, err := os.Lstat(d) 77 | 78 | if os.IsNotExist(err) { 79 | if err := os.MkdirAll(d, os.ModePerm); err != nil { 80 | return err 81 | } 82 | } else if err != nil { 83 | return err 84 | } else if stat != nil && !stat.IsDir() { 85 | return fmt.Errorf("%s already exists and is not a directory", d) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func GetKey(secrets map[string]string, keyStoragePath string) (string, error) { 92 | if _, err := os.Stat(keyStoragePath); os.IsNotExist(err) { 93 | os.Mkdir(keyStoragePath, 0700) 94 | } 95 | 96 | keyContents, keyNameExists := secrets["key"] 97 | if !keyNameExists { 98 | keyContents, keyNameExists = secrets["key.json"] 99 | if !keyNameExists { 100 | return "", status.Errorf(codes.Internal, "Secret has no keys named '%s' or '%s'", "key", "key.json") 101 | } 102 | } 103 | 104 | klog.V(5).Info("Saving key contents to a temporary location") 105 | keyFile, err := CreateFile(keyStoragePath, keyContents) 106 | if err != nil { 107 | return "", status.Errorf(codes.Internal, "Unable to save secret 'key' / 'key.json' to %s", keyStoragePath) 108 | } 109 | 110 | return keyFile, nil 111 | } 112 | 113 | func CleanupKey(keyFile string, keyStoragePath string) { 114 | location := filepath.Dir(keyFile) 115 | if location == keyStoragePath { 116 | if err := os.Remove(keyFile); err != nil { 117 | klog.Warningf("Error removing temporary key file %s: %s", keyFile, err) 118 | } 119 | } 120 | } 121 | 122 | func BucketName(volumeId string) string { 123 | // return volumeId 124 | var crc32Hash = crc32.ChecksumIEEE([]byte(volumeId)) 125 | 126 | if len(volumeId) > 48 { 127 | volumeId = volumeId[0:48] 128 | } 129 | return fmt.Sprintf("%s-%x", strings.ToLower(volumeId), crc32Hash) 130 | } 131 | 132 | func BucketCapacity(attrs *storage.BucketAttrs) (int64, error) { 133 | for labelName, labelValue := range attrs.Labels { 134 | if labelName != "capacity" { 135 | continue 136 | } 137 | 138 | capacity, err := strconv.ParseInt(labelValue, 10, 64) 139 | if err != nil { 140 | return 0, status.Errorf(codes.Internal, "Failed to parse bucket capacity: %v", labelValue) 141 | } 142 | 143 | return capacity, nil 144 | } 145 | 146 | return 0, nil 147 | } 148 | 149 | func SetBucketCapacity(ctx context.Context, bucket *storage.BucketHandle, capacity int64) (attrs *storage.BucketAttrs, err error) { 150 | var uattrs = storage.BucketAttrsToUpdate{} 151 | 152 | uattrs.SetLabel("capacity", strconv.FormatInt(capacity, 10)) 153 | 154 | return bucket.Update(ctx, uattrs) 155 | } 156 | 157 | func BucketExists(ctx context.Context, bucket *storage.BucketHandle) (exists bool, err error) { 158 | query := &storage.Query{Prefix: ""} 159 | 160 | it := bucket.Objects(ctx, query) 161 | _, err = it.Next() 162 | 163 | if err == iterator.Done { 164 | return true, nil 165 | } else if err != nil && err.Error() == "storage: bucket doesn't exist" { 166 | return false, nil 167 | } else if err != nil { 168 | return false, err 169 | } 170 | 171 | return true, nil 172 | } 173 | 174 | func GetPvcAnnotations(ctx context.Context, pvcName string, pvcNamespace string) (annotations map[string]string, err error) { 175 | config, err := rest.InClusterConfig() 176 | if err != nil { 177 | return nil, err 178 | } 179 | // creates the clientset 180 | clientset, err := kubernetes.NewForConfig(config) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | pvc, err := clientset.CoreV1().PersistentVolumeClaims(pvcNamespace).Get(ctx, pvcName, metav1.GetOptions{}) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | return pvc.ObjectMeta.Annotations, nil 191 | } 192 | 193 | // DriverReadyLabel returns the driver-ready label according to the driver name. 194 | func DriverReadyLabel(driverName string) string { 195 | return driverName + "/driver-ready" 196 | } 197 | 198 | // DriverReadyLabelJSONPatchEscaped returns the driver-ready label according to the driver name but espcaed to be used in a JSONPatch path. 199 | func DriverReadyLabelJSONPatchEscaped(driverName string) string { 200 | return strings.ReplaceAll(DriverReadyLabel(driverName), "/", "~1") 201 | } 202 | 203 | // SetDriverReadyLabel set the label /driver-ready= on the given node. 204 | func SetDriverReadyLabel(ctx context.Context, driverName string, nodeName string, isReady bool) (err error) { 205 | config, err := rest.InClusterConfig() 206 | if err != nil { 207 | return err 208 | } 209 | clientset, err := kubernetes.NewForConfig(config) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | patch := []struct { 215 | Op string `json:"op"` 216 | Path string `json:"path"` 217 | Value string `json:"value"` 218 | }{{ 219 | Op: "add", 220 | Path: "/metadata/labels/" + DriverReadyLabelJSONPatchEscaped(driverName), 221 | Value: strconv.FormatBool(isReady), 222 | }} 223 | patchBytes, err := json.Marshal(patch) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | _, err = clientset.CoreV1().Nodes().Patch(ctx, nodeName, types.JSONPatchType, patchBytes, metav1.PatchOptions{}) 229 | if err != nil { 230 | return err 231 | } 232 | return nil 233 | } 234 | 235 | func DeletePod(ctx context.Context, namespace string, name string) (err error) { 236 | config, err := rest.InClusterConfig() 237 | if err != nil { 238 | return err 239 | } 240 | // creates the clientset 241 | clientset, err := kubernetes.NewForConfig(config) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | return clientset.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{}) 247 | } 248 | 249 | func GetRegisteredMounts(ctx context.Context, node string) (list *v1beta1.PublishedVolumeList, err error) { 250 | config, err := rest.InClusterConfig() 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | // creates the clientset 256 | clientset, err := gcs.NewForConfig(config) 257 | if err != nil { 258 | return nil, err 259 | } 260 | 261 | return clientset.GcsV1beta1().PublishedVolumes().List(ctx, metav1.ListOptions{ 262 | LabelSelector: labels.Set(map[string]string{ 263 | "gcs.csi.ofek.dev/node": node, 264 | }).String(), 265 | }) 266 | } 267 | 268 | func RegisterMount(ctx context.Context, volumeID string, targetPath string, node string, podNamespace string, podName string, options map[string]string) (err error) { 269 | config, err := rest.InClusterConfig() 270 | if err != nil { 271 | return err 272 | } 273 | 274 | // creates the clientsets 275 | clientset, err := gcs.NewForConfig(config) 276 | if err != nil { 277 | return err 278 | } 279 | coreClientset, err := kubernetes.NewForConfig(config) 280 | if err != nil { 281 | return err 282 | } 283 | 284 | name := strconv.FormatUint(uint64(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%s-%s-%s", volumeID, targetPath, node)))), 16) 285 | 286 | nodeResource, err := coreClientset.CoreV1().Nodes().Get(ctx, node, metav1.GetOptions{}) 287 | if err != nil { 288 | return err 289 | } 290 | 291 | _, err = clientset.GcsV1beta1().PublishedVolumes().Create(ctx, &v1beta1.PublishedVolume{ 292 | ObjectMeta: metav1.ObjectMeta{ 293 | Name: name, 294 | Labels: map[string]string{ 295 | "gcs.csi.ofek.dev/node": node, 296 | }, 297 | OwnerReferences: []metav1.OwnerReference{ 298 | { 299 | APIVersion: "v1", 300 | Kind: "Node", 301 | Name: node, 302 | UID: nodeResource.GetUID(), 303 | }, 304 | }, 305 | }, 306 | Spec: v1beta1.PublishedVolumeSpec{ 307 | Node: node, 308 | TargetPath: targetPath, 309 | VolumeHandle: volumeID, 310 | Options: options, 311 | Pod: v1beta1.PublishedVolumeSpecPod{ 312 | Namespace: podNamespace, 313 | Name: podName, 314 | }, 315 | }, 316 | }, metav1.CreateOptions{}) 317 | if err != nil { 318 | return err 319 | } 320 | 321 | return nil 322 | } 323 | 324 | func UnregisterMount(ctx context.Context, volumeID string, targetPath string, node string) (err error) { 325 | config, err := rest.InClusterConfig() 326 | if err != nil { 327 | return err 328 | } 329 | 330 | // creates the clientset 331 | clientset, err := gcs.NewForConfig(config) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | name := strconv.FormatUint(uint64(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%s-%s-%s", volumeID, targetPath, node)))), 16) 337 | 338 | delPropPolicy := metav1.DeletePropagationForeground 339 | err = clientset.GcsV1beta1().PublishedVolumes().Delete(ctx, name, metav1.DeleteOptions{ 340 | PropagationPolicy: &delPropPolicy, 341 | }) 342 | if err != nil { 343 | return err 344 | } 345 | 346 | return nil 347 | } 348 | -------------------------------------------------------------------------------- /pkg/util/common_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | . "github.com/ofek/csi-gcs/pkg/util" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Common", func() { 13 | Describe("GetKey", func() { 14 | It("should error when neither 'key' nor 'key.json' are present", func() { 15 | sec := map[string]string{} 16 | 17 | _, err := GetKey(sec, "") 18 | Expect(err).ShouldNot(Equal("Secret has no keys named 'key' or 'key.json'")) 19 | }) 20 | 21 | It("should use 'key' first", func() { 22 | sec := map[string]string{ 23 | "key": "Content of key", 24 | "key.json": "Content of key.json", 25 | } 26 | 27 | dir, err := ioutil.TempDir("", "test") 28 | Expect(err).ShouldNot(HaveOccurred()) 29 | defer os.RemoveAll(dir) 30 | 31 | s, err := GetKey(sec, dir) 32 | Expect(err).ShouldNot(HaveOccurred()) 33 | Expect(s).To(BeARegularFile()) 34 | 35 | f, err := os.Open(s) 36 | Expect(err).ShouldNot(HaveOccurred()) 37 | Expect(ioutil.ReadAll(f)).To(BeEquivalentTo("Content of key")) 38 | }) 39 | 40 | It("should fallback to 'key.json'", func() { 41 | sec := map[string]string{ 42 | "key.json": "Content of key.json", 43 | } 44 | 45 | dir, err := ioutil.TempDir("", "test") 46 | Expect(err).ShouldNot(HaveOccurred()) 47 | defer os.RemoveAll(dir) 48 | 49 | s, err := GetKey(sec, dir) 50 | Expect(err).ShouldNot(HaveOccurred()) 51 | Expect(s).To(BeARegularFile()) 52 | 53 | f, err := os.Open(s) 54 | Expect(err).ShouldNot(HaveOccurred()) 55 | Expect(ioutil.ReadAll(f)).To(BeEquivalentTo("Content of key.json")) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /pkg/util/util_suite_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUtil(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Util Suite") 13 | } 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | invoke 2 | tox 3 | -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from invoke import Collection 2 | 3 | from . import docs 4 | from . import image 5 | from . import test 6 | from . import env 7 | from . import build 8 | from . import codegen 9 | from .utils import set_root 10 | 11 | ns = Collection() 12 | ns.add_collection(Collection.from_module(image)) 13 | ns.add_collection(Collection.from_module(docs)) 14 | ns.add_collection(Collection.from_module(test)) 15 | ns.add_collection(Collection.from_module(env)) 16 | ns.add_collection(Collection.from_module(build)) 17 | ns.add_collection(Collection.from_module(codegen)) 18 | 19 | set_root() 20 | -------------------------------------------------------------------------------- /tasks/build.py: -------------------------------------------------------------------------------- 1 | from invoke import task 2 | 3 | from .utils import EnvVars, get_version 4 | 5 | 6 | @task( 7 | help={ 8 | 'release': 'Build a release image', 9 | }, 10 | default=True, 11 | ) 12 | def build(ctx, release=False): 13 | if release: 14 | global_ldflags = '-s -w' 15 | else: 16 | global_ldflags = '' 17 | 18 | with EnvVars({'CGO_ENABLED': '0', 'GOOS': 'linux', 'GOARCH': 'amd64'}): 19 | ctx.run( 20 | f'go build ' 21 | f'-o bin/driver ' 22 | f'-ldflags "all={global_ldflags}" ' 23 | f'-ldflags "-X github.com/ofek/csi-gcs/pkg/driver.driverVersion={get_version()} {global_ldflags}" ' 24 | f'./cmd', 25 | echo=True, 26 | ) 27 | -------------------------------------------------------------------------------- /tasks/codegen.py: -------------------------------------------------------------------------------- 1 | from invoke import task 2 | 3 | from .utils import get_root 4 | 5 | 6 | @task( 7 | default=True, 8 | ) 9 | def codegen(ctx): 10 | mount_dir = '/go/src/github.com/ofek/csi-gcs' 11 | ctx.run( 12 | f'docker run ' 13 | f'--rm ' 14 | f'-v "{get_root()}:{mount_dir}" ' 15 | f'-w {mount_dir} ' 16 | f'golang:1.18.2-alpine3.15 ' 17 | f'./hack/update-codegen.sh', 18 | echo=True, 19 | ) 20 | -------------------------------------------------------------------------------- /tasks/constants.py: -------------------------------------------------------------------------------- 1 | GCSFUSE_VERSION = '0.41.1' 2 | 3 | REPO = 'ofekmeister' 4 | IMAGE = 'csi-gcs' 5 | DRIVER_NAME = f'{REPO}/{IMAGE}' 6 | -------------------------------------------------------------------------------- /tasks/docs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import webbrowser 4 | from tempfile import TemporaryDirectory 5 | 6 | from invoke import Exit, task 7 | 8 | from .utils import create_file, get_git_email, get_git_user, get_latest_commit_hash 9 | 10 | 11 | def insert_verbosity_flag(command, verbosity): 12 | # One level is no tox flag 13 | if verbosity: 14 | verbosity -= 1 15 | # By default hide deps stage and success text 16 | else: 17 | verbosity -= 2 18 | 19 | if verbosity < 0: 20 | command.insert(1, f"-{'q' * abs(verbosity)}") 21 | elif verbosity > 0: 22 | command.insert(1, f"-{'v' * abs(verbosity)}") 23 | 24 | 25 | @task( 26 | help={'verbose': 'Increase verbosity (can be used additively)'}, 27 | incrementable=['verbose'], 28 | ) 29 | def build(ctx, verbose=False): 30 | """Build documentation""" 31 | command = ['tox', '-e', 'docs', '--', 'build', '--clean'] 32 | insert_verbosity_flag(command, verbose) 33 | 34 | print('Building documentation...') 35 | ctx.run(' '.join(command)) 36 | 37 | 38 | @task( 39 | default=True, 40 | pre=[build], 41 | help={ 42 | 'no-open': 'Do not open the documentation in a web browser', 43 | 'verbose': 'Increase verbosity (can be used additively)', 44 | }, 45 | incrementable=['verbose'], 46 | ) 47 | def serve(ctx, no_open=False, verbose=False): 48 | """Serve and view documentation in a web browser""" 49 | 50 | command = ['tox', '-e', 'docs', '--', 'serve', '--livereload', '--dev-addr', '0.0.0.0:8765'] 51 | insert_verbosity_flag(command, verbose) 52 | 53 | if not no_open: 54 | webbrowser.open_new_tab(f'http://localhost:8765') 55 | 56 | ctx.run(' '.join(command)) 57 | -------------------------------------------------------------------------------- /tasks/env.py: -------------------------------------------------------------------------------- 1 | from invoke import task 2 | 3 | from .constants import DRIVER_NAME, GCSFUSE_VERSION 4 | from .utils import get_root 5 | 6 | 7 | @task 8 | def create(ctx): 9 | ctx.run( 10 | 'docker volume create csi-gcs-go', 11 | echo=True, 12 | ) 13 | ctx.run( 14 | f'docker build . ' 15 | f'--tag {DRIVER_NAME}-env ' 16 | f'-f dev-env.Dockerfile ' 17 | f'--build-arg gcsfuse_version="{GCSFUSE_VERSION}"', 18 | echo=True, 19 | ) 20 | 21 | 22 | @task 23 | def delete(ctx): 24 | ctx.run( 25 | 'docker volume rm csi-gcs-go', 26 | echo=True, 27 | ) 28 | ctx.run( 29 | f'docker image rm {DRIVER_NAME}-env', 30 | echo=True, 31 | ) 32 | 33 | 34 | @task( 35 | pre=[create], 36 | default=True, 37 | ) 38 | def run(ctx, command='echo Provide Command'): 39 | ctx.run( 40 | f'docker run ' 41 | f'--rm ' 42 | f'-v {get_root()}:/driver ' 43 | f'-v csi-gcs-go:/go ' 44 | f'--cap-add SYS_ADMIN ' 45 | f'--device /dev/fuse ' 46 | f'--privileged ' 47 | f'-v /tmp/csi:/tmp/csi:rw ' 48 | f'-v /var/run/docker.sock:/var/run/docker.sock ' 49 | f'{DRIVER_NAME}-env ' 50 | f'{command}', 51 | echo=True, 52 | ) 53 | -------------------------------------------------------------------------------- /tasks/image.py: -------------------------------------------------------------------------------- 1 | from invoke import task 2 | 3 | from .constants import DRIVER_NAME, GCSFUSE_VERSION 4 | from .utils import image_name, image_tags 5 | 6 | @task( 7 | help={ 8 | 'release': 'Build a release image', 9 | 'gcsfuse': f'The version or commit hash of gcsfuse (default: {GCSFUSE_VERSION})', 10 | }, 11 | default=True, 12 | ) 13 | def build(ctx, release=False, gcsfuse=GCSFUSE_VERSION): 14 | if release: 15 | global_ldflags = '-s -w' 16 | docker_build_args = '--no-cache' 17 | else: 18 | global_ldflags = '' 19 | docker_build_args = '' 20 | 21 | image = image_name() 22 | 23 | ctx.run( 24 | f'docker build . --tag {image} ' 25 | f'--build-arg global_ldflags="{global_ldflags}" ' 26 | f'--build-arg gcsfuse_version="{gcsfuse}" ' 27 | f'{docker_build_args}', 28 | echo=True, 29 | ) 30 | 31 | for tag in image_tags(): 32 | ctx.run(f'docker tag {image} {image_name(tag)}', echo=True) 33 | 34 | @task 35 | def deploy(ctx): 36 | ctx.run(f'docker push {image_name()}', echo=True) 37 | for tag in image_tags(): 38 | if tag != 'dev': 39 | ctx.run(f'docker tag {image_name()} {image_name(tag)}', echo=True) 40 | ctx.run(f'docker push {image_name(tag)}', echo=True) 41 | -------------------------------------------------------------------------------- /tasks/test.py: -------------------------------------------------------------------------------- 1 | 2 | from invoke import task 3 | 4 | @task 5 | def sanity(ctx): 6 | ctx.run(f'go test ./test', echo=True) 7 | 8 | @task 9 | def unit_driver(ctx): 10 | ctx.run(f'go test ./pkg/driver', echo=True) 11 | 12 | @task 13 | def unit_flags(ctx): 14 | ctx.run(f'go test ./pkg/flags', echo=True) 15 | 16 | @task 17 | def unit_util(ctx): 18 | ctx.run(f'go test ./pkg/util', echo=True) 19 | 20 | @task(pre=[unit_flags, unit_driver, unit_util]) 21 | def unit(ctx): pass 22 | 23 | @task( 24 | pre=[unit, sanity], 25 | default=True, 26 | ) 27 | def all(ctx): pass -------------------------------------------------------------------------------- /tasks/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from .constants import DRIVER_NAME 4 | 5 | def get_root(): 6 | return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | def set_root(): 9 | os.chdir(get_root()) 10 | 11 | def get_latest_commit_hash(ctx): 12 | result = ctx.run('git rev-parse HEAD', hide=True) 13 | return result.stdout.strip() 14 | 15 | def get_git_user(ctx): 16 | user = os.getenv('GH_USER') 17 | if user is not None: 18 | return user 19 | 20 | result = ctx.run('git config --get user.name', hide=True) 21 | return result.stdout.strip() 22 | 23 | def get_git_email(ctx): 24 | email = os.getenv('GH_EMAIL') 25 | if email is not None: 26 | return email 27 | 28 | result = ctx.run('git config --get user.email', hide=True) 29 | return result.stdout.strip() 30 | 31 | def create_file(f): 32 | with open(f, 'a'): 33 | os.utime(f, None) 34 | 35 | def get_version(): 36 | version = subprocess.run(['git', 'describe', '--long', '--tags', '--match=v*', '--dirty'], stdout=subprocess.PIPE, cwd=get_root()) 37 | if version.returncode == 0: 38 | return version.stdout.decode('utf-8').strip() 39 | 40 | current_ref = subprocess.run(['git', 'rev-list', '-n1', 'HEAD'], stdout=subprocess.PIPE, cwd=get_root()) 41 | return current_ref.stdout.decode('utf-8').strip() 42 | 43 | def image_name(version=False): 44 | if not version: 45 | version = get_version() 46 | return f'{DRIVER_NAME}:{version}' 47 | 48 | def image_tags(): 49 | last_tag = subprocess.run(['git', 'describe', '--tags', '--match=v*', '--abbrev=0'], stdout=subprocess.PIPE, cwd=get_root()) 50 | 51 | if last_tag.returncode != 0: 52 | return ['dev'] 53 | 54 | current_ref = subprocess.run(['git', 'rev-list', '-n1', 'HEAD'], stdout=subprocess.PIPE, cwd=get_root()) 55 | last_tag_ref = subprocess.run(['git', 'rev-list', '-n1', last_tag.stdout.decode('utf-8').strip()], stdout=subprocess.PIPE, cwd=get_root()) 56 | 57 | if last_tag_ref.stdout.decode('utf-8').strip() == current_ref.stdout.decode('utf-8').strip(): 58 | return [last_tag.stdout.decode('utf-8').strip(), 'latest', 'dev'] 59 | 60 | return ['dev'] 61 | 62 | 63 | class EnvVars(dict): 64 | def __init__(self, env_vars=None, ignore=None): 65 | super(EnvVars, self).__init__(os.environ) 66 | self.old_env = dict(self) 67 | 68 | if env_vars is not None: 69 | self.update(env_vars) 70 | 71 | if ignore is not None: 72 | for env_var in ignore: 73 | self.pop(env_var, None) 74 | 75 | def __enter__(self): 76 | os.environ.clear() 77 | os.environ.update(self) 78 | 79 | def __exit__(self, exc_type, exc_value, traceback): 80 | os.environ.clear() 81 | os.environ.update(self.old_env) 82 | -------------------------------------------------------------------------------- /test/sanity_test.go: -------------------------------------------------------------------------------- 1 | package sanity_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/kubernetes-csi/csi-test/v3/pkg/sanity" 9 | "github.com/ofek/csi-gcs/pkg/driver" 10 | "k8s.io/klog" 11 | ) 12 | 13 | func TestCsiGcs(t *testing.T) { 14 | endpointFile, err := ioutil.TempFile("", "csi-gcs.*.sock") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | defer os.Remove(endpointFile.Name()) 20 | 21 | stagingPath, err := ioutil.TempDir("", "csi-gcs-staging") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | os.Remove(stagingPath) 27 | 28 | targetPath, err := ioutil.TempDir("", "csi-gcs-target") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | os.Remove(targetPath) 34 | 35 | var endpoint = "unix://" 36 | endpoint += endpointFile.Name() 37 | 38 | d, err := driver.NewGCSDriver(driver.CSIDriverName, "test-node", endpoint, "development", false) 39 | if err != nil { 40 | klog.Error(err.Error()) 41 | os.Exit(1) 42 | } 43 | 44 | go func() { 45 | if err = d.Run(); err != nil { 46 | t.Fatal(err) 47 | } 48 | }() 49 | 50 | config := sanity.NewTestConfig() 51 | // Set configuration options as needed 52 | config.Address = endpoint 53 | config.SecretsFile = "./secret.yaml" 54 | config.StagingPath = stagingPath 55 | config.TargetPath = targetPath 56 | config.RemoveTargetPath = func(patargetPathth string) error { 57 | return os.RemoveAll(targetPath) 58 | } 59 | 60 | // Now call the test suite 61 | sanity.Test(t, config) 62 | } 63 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | docs 4 | 5 | [testenv] 6 | passenv = * 7 | basepython = python3 8 | skip_install = true 9 | 10 | [testenv:docs] 11 | deps = 12 | mkdocs~=1.3.0 13 | ; theme 14 | mkdocs-material~=8.2.8 15 | ; plugins 16 | mkdocs-minify-plugin~=0.5.0 17 | mkdocs-git-revision-date-localized-plugin~=1.0.0 18 | ; Extensions 19 | pymdown-extensions~=9.3.0 20 | mkdocs-material-extensions~=1.0.3 21 | mkpatcher~=1.0.2 22 | ; Necessary for syntax highlighting in code blocks 23 | Pygments~=2.11.2 24 | commands = 25 | python -m mkdocs {posargs} 26 | --------------------------------------------------------------------------------