├── .ci ├── actor.yml ├── interface.yml └── provider.yml ├── LICENSE ├── README.md ├── applier ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── Makefile ├── README.md ├── provider.mk ├── provider_test_config.toml ├── src │ └── main.rs └── tests │ └── applier_test.rs ├── interface ├── .gitignore ├── Makefile ├── README.md ├── codegen.toml ├── html │ ├── org_wasmcloud_core.html │ ├── org_wasmcloud_interface_factorial.html │ └── org_wasmcloud_model.html ├── interface.mk ├── kubernetes_applier.smithy └── rust │ ├── Cargo.toml │ ├── build.rs │ └── src │ ├── kubernetes_applier.rs │ └── lib.rs └── service-applier ├── .cargo └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── actor.mk └── src └── lib.rs /.ci/actor.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - "main" 5 | tags: 6 | include: 7 | - "kubernetes-service-applier-v*" 8 | paths: 9 | include: 10 | - "service-applier/" 11 | - ".ci/actor.yml" 12 | pr: 13 | branches: 14 | include: 15 | - "main" 16 | paths: 17 | include: 18 | - "service-applier/" 19 | - ".ci/actor.yml" 20 | 21 | pool: 22 | vmImage: ubuntu-latest 23 | 24 | resources: 25 | repositories: 26 | - repository: public-templates 27 | type: github 28 | endpoint: cosmonic 29 | name: cosmonic/ado-common 30 | 31 | variables: 32 | - group: "Common Rust Vars" 33 | - group: "Cosmonic Release Keys" 34 | - name: working-directory 35 | value: ./service-applier 36 | - name: WASH_ISSUER_KEY 37 | value: $[variables.COSMONIC_ACCOUNT_OFFICIAL] 38 | - name: WASH_SUBJECT_KEY 39 | value: $[variables.SERVICE_APPLIER_KEY] 40 | - name: PUSH_USER 41 | value: $[variables.WASMCLOUD_AZURECR_PUSH_USER] 42 | - name: PUSH_PASSWORD 43 | value: $[variables.WASMCLOUD_AZURECR_PUSH_PASSWORD] 44 | 45 | stages: 46 | - stage: build_and_check 47 | jobs: 48 | - job: build_and_check 49 | steps: 50 | - template: steps/rust-setup.yml@public-templates 51 | parameters: 52 | components: 53 | - clippy 54 | - rustfmt 55 | targets: 56 | - wasm32-unknown-unknown 57 | 58 | - template: steps/rust-caching.yml@public-templates 59 | parameters: 60 | projectName: service-applier-actor 61 | workingDirectory: $(working-directory) 62 | 63 | - template: steps/rust-fmt-clippy.yml@public-templates 64 | parameters: 65 | workingDirectory: $(working-directory) 66 | 67 | - stage: release 68 | dependsOn: 69 | - build_and_check 70 | # Only do this stage if the ref is a tag and the previous stage succeeded 71 | condition: and(succeeded('build_and_check'), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 72 | jobs: 73 | - job: release 74 | steps: 75 | - template: steps/install-wash.yml@public-templates 76 | 77 | - template: steps/rust-setup.yml@public-templates 78 | parameters: 79 | targets: 80 | - wasm32-unknown-unknown 81 | - template: steps/rust-caching.yml@public-templates 82 | parameters: 83 | projectName: service-applier-actor 84 | workingDirectory: $(working-directory) 85 | cacheTarget: false 86 | 87 | - bash: make 88 | workingDirectory: $(working-directory) 89 | displayName: Build and sign actor 90 | env: 91 | WASH_ISSUER_KEY: $(WASH_ISSUER_KEY) 92 | WASH_SUBJECT_KEY: $(WASH_SUBJECT_KEY) 93 | 94 | - bash: | 95 | echo "##vso[task.setvariable variable=oci-repository]$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name' | sed 's/-/_/g' )" 96 | echo "##vso[task.setvariable variable=oci-version]$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].version')" 97 | displayName: Determine artifact metadata 98 | workingDirectory: $(working-directory) 99 | 100 | - template: steps/oci-release.yml@public-templates 101 | parameters: 102 | artifactPath: $(working-directory)/build/$(oci-repository)_s.wasm 103 | ociUrl: wasmcloud.azurecr.io 104 | ociRepository: $(oci-repository) 105 | ociVersion: $(oci-version) 106 | ociUsername: $(PUSH_USER) 107 | ociPassword: $(PUSH_PASSWORD) 108 | -------------------------------------------------------------------------------- /.ci/interface.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - "main" 5 | tags: 6 | include: 7 | - "kubernetes-applier-interface-v*" 8 | paths: 9 | include: 10 | - "interface/" 11 | - ".ci/interface.yml" 12 | pr: 13 | branches: 14 | include: 15 | - "main" 16 | paths: 17 | include: 18 | - "interface/" 19 | - ".ci/interface.yml" 20 | 21 | pool: 22 | vmImage: ubuntu-latest 23 | 24 | resources: 25 | repositories: 26 | - repository: public-templates 27 | type: github 28 | endpoint: cosmonic 29 | name: cosmonic/ado-common 30 | 31 | variables: 32 | - group: "Common Rust Vars" 33 | - group: "Cosmonic Release Keys" 34 | - name: working-directory 35 | value: ./interface/rust 36 | - name: CRATES_TOKEN 37 | value: $[variables.CRATES_PUBLISH_TOKEN] 38 | 39 | stages: 40 | - stage: build_and_check 41 | jobs: 42 | - job: build_and_check 43 | steps: 44 | - template: steps/rust-setup.yml@public-templates 45 | parameters: 46 | components: 47 | - clippy 48 | - rustfmt 49 | - template: steps/rust-caching.yml@public-templates 50 | parameters: 51 | projectName: service-applier-interface 52 | workingDirectory: $(working-directory) 53 | 54 | - template: steps/rust-test.yml@public-templates 55 | parameters: 56 | workingDirectory: $(working-directory) 57 | runDocTests: false 58 | 59 | - stage: release 60 | dependsOn: 61 | - build_and_check 62 | # Only do this stage if the ref is a tag and the previous stage succeeded 63 | condition: and(succeeded('build_and_check'), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 64 | jobs: 65 | - job: release 66 | steps: 67 | - template: steps/rust-setup.yml@public-templates 68 | - template: steps/rust-caching.yml@public-templates 69 | parameters: 70 | projectName: service-applier-interface 71 | workingDirectory: $(working-directory) 72 | cacheTarget: false 73 | 74 | - template: steps/crate-release.yml@public-templates 75 | parameters: 76 | workingDirectory: $(working-directory) 77 | cratesToken: $(CRATES_TOKEN) 78 | -------------------------------------------------------------------------------- /.ci/provider.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - "main" 5 | tags: 6 | include: 7 | - "kubernetes-applier-provider-v*" 8 | paths: 9 | include: 10 | - "applier/" 11 | - ".ci/provider.yml" 12 | pr: 13 | branches: 14 | include: 15 | - "main" 16 | paths: 17 | include: 18 | - "applier/" 19 | - ".ci/provider.yml" 20 | 21 | pool: 22 | vmImage: ubuntu-latest 23 | 24 | resources: 25 | repositories: 26 | - repository: public-templates 27 | type: github 28 | endpoint: cosmonic 29 | name: cosmonic/ado-common 30 | 31 | variables: 32 | - group: "Common Rust Vars" 33 | - group: "Cosmonic Release Keys" 34 | - name: working-directory 35 | value: ./applier 36 | - name: WASH_ISSUER_KEY 37 | value: $[variables.COSMONIC_ACCOUNT_OFFICIAL] 38 | - name: WASH_SUBJECT_KEY 39 | value: $[variables.COSMONIC_KUBERNETES_APPLIER_KEY] 40 | - name: PUSH_USER 41 | value: $[variables.WASMCLOUD_AZURECR_PUSH_USER] 42 | - name: PUSH_PASSWORD 43 | value: $[variables.WASMCLOUD_AZURECR_PUSH_PASSWORD] 44 | 45 | stages: 46 | - stage: build_and_check 47 | jobs: 48 | - job: build_and_check 49 | strategy: 50 | matrix: 51 | linux: 52 | vmImage: ubuntu-latest 53 | windows: 54 | vmImage: windows-latest 55 | macos: 56 | vmImage: macOS-latest 57 | pool: 58 | vmImage: $(vmImage) 59 | steps: 60 | - template: steps/rust-setup.yml@public-templates 61 | parameters: 62 | components: 63 | - clippy 64 | - rustfmt 65 | - template: steps/rust-caching.yml@public-templates 66 | parameters: 67 | projectName: service-applier-provider 68 | workingDirectory: $(working-directory) 69 | 70 | # No tests needed here as they are all integration tests. So just run a check and clippy 71 | - template: steps/rust-fmt-clippy.yml@public-templates 72 | parameters: 73 | workingDirectory: $(working-directory) 74 | 75 | - job: integration 76 | steps: 77 | - template: steps/rust-setup.yml@public-templates 78 | parameters: 79 | components: 80 | - clippy 81 | - rustfmt 82 | 83 | - bash: | 84 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.14.0/kind-linux-amd64 85 | chmod +x ./kind 86 | mv ./kind "${BINARY_LOCATION}/" 87 | echo "##vso[task.setvariable variable=PATH]$PATH:${BINARY_LOCATION}/" 88 | displayName: Install kind 89 | env: 90 | BINARY_LOCATION: $(Agent.TempDirectory) 91 | 92 | - bash: kind create cluster 93 | displayName: Start kind cluster 94 | 95 | # Because we are using rustls-tls, the credentials for the kubernetes cluster can't be a bare IP 96 | - bash: sed -i 's/127.0.0.1/localhost/g' ~/.kube/config 97 | displayName: Use localhost for kubeconfig 98 | 99 | - template: steps/rust-caching.yml@public-templates 100 | parameters: 101 | projectName: service-applier-provider-integration 102 | workingDirectory: $(working-directory) 103 | 104 | - script: make test 105 | displayName: Run integration tests 106 | workingDirectory: $(working-directory) 107 | 108 | - stage: release 109 | dependsOn: 110 | - build_and_check 111 | # Only do this stage if the ref is a tag and the previous stage succeeded 112 | condition: and(succeeded('build_and_check'), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 113 | jobs: 114 | - job: release 115 | steps: 116 | - template: steps/install-wash.yml@public-templates 117 | - template: steps/rust-setup.yml@public-templates 118 | - template: steps/rust-caching.yml@public-templates 119 | parameters: 120 | projectName: service-applier-provider 121 | workingDirectory: $(working-directory) 122 | cacheTarget: false 123 | 124 | - bash: cargo install --git https://github.com/brooksmtownsend/cross --branch add-darwin-target --force 125 | displayName: Install Cross 126 | 127 | - bash: make par-full 128 | workingDirectory: $(working-directory) 129 | displayName: Build provider archive 130 | env: 131 | WASH_ISSUER_KEY: $(WASH_ISSUER_KEY) 132 | WASH_SUBJECT_KEY: $(WASH_SUBJECT_KEY) 133 | 134 | - bash: | 135 | echo "##vso[task.setvariable variable=oci-repository]$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name')" 136 | echo "##vso[task.setvariable variable=oci-version]$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].version')" 137 | displayName: Determine artifact metadata 138 | workingDirectory: $(working-directory) 139 | 140 | - template: steps/oci-release.yml@public-templates 141 | parameters: 142 | artifactPath: $(working-directory)/build/$(oci-repository).par.gz 143 | ociUrl: wasmcloud.azurecr.io 144 | ociRepository: $(oci-repository) 145 | ociVersion: $(oci-version) 146 | ociUsername: $(PUSH_USER) 147 | ociPassword: $(PUSH_PASSWORD) 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022, Cosmonic Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | This repository is **deprecated**. The functionality provided by Kubernetes Applier has been moved to the [`wasmcloud-operator`](https://github.com/wasmcloud/wasmcloud-operator), we strongly recommend you use the [wasmCloud Operator](https://github.com/wasmcloud/wasmcloud-operator) instead. 3 | 4 | # Kubernetes applier 5 | 6 | This repo contains the interface, provider, and basic service actor for a Kubernetes applier. This 7 | is mainly intended for use with those who need to connect existing Kubernetes services to services 8 | in wasmCloud. Please see each individual directory for more information 9 | -------------------------------------------------------------------------------- /applier/.gitignore: -------------------------------------------------------------------------------- 1 | # This file lists build byproducts, 2 | # IDE-specific files (unless shared by your team) 3 | 4 | # 5 | 6 | ## Build 7 | /build 8 | /dist/ 9 | /target 10 | **target 11 | 12 | ## File system 13 | .DS_Store 14 | desktop.ini 15 | 16 | ## Editor 17 | *.swp 18 | *.swo 19 | Session.vim 20 | .cproject 21 | .idea 22 | *.iml 23 | .vscode 24 | .project 25 | .favorites.json 26 | .settings/ 27 | 28 | ## Temporary files 29 | *~ 30 | \#* 31 | \#*\# 32 | .#* 33 | 34 | ## Python 35 | __pycache__/ 36 | *.py[cod] 37 | *$py.class 38 | 39 | ## Node 40 | **node_modules 41 | **package-lock.json 42 | 43 | -------------------------------------------------------------------------------- /applier/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "applier" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Cosmonic Inc"] 6 | 7 | [dependencies] 8 | base64 = "0.13" 9 | tracing = { version = "0.1", features = ["log"] } 10 | tokio = { version = "1", features = ["full"] } 11 | kubernetes-applier-interface = "0.3" 12 | wasmbus-rpc = "0.9.2" 13 | k8s-openapi = { version = "0.15", default-features = false, features = ["v1_22"] } 14 | kube = { version = "0.74", default-features = false, features = ["rustls-tls", "config", "client"] } 15 | serde_yaml = "0.8" 16 | atty = "0.2" 17 | 18 | # test dependencies 19 | [dev-dependencies] 20 | wasmcloud-test-util = "0.4" 21 | 22 | [[bin]] 23 | name = "applier" 24 | path = "src/main.rs" 25 | -------------------------------------------------------------------------------- /applier/Cross.toml: -------------------------------------------------------------------------------- 1 | [target.armv7-unknown-linux-gnueabihf] 2 | image = "wasmcloud/cross:armv7-unknown-linux-gnueabihf" 3 | 4 | [target.aarch64-unknown-linux-gnu] 5 | image = "wasmcloud/cross:aarch64-unknown-linux-gnu" 6 | 7 | [target.x86_64-apple-darwin] 8 | image = "wasmcloud/cross:x86_64-apple-darwin" 9 | 10 | [target.aarch64-apple-darwin] 11 | image = "wasmcloud/cross:aarch64-apple-darwin" 12 | 13 | [target.x86_64-unknown-linux-gnu] 14 | image = "wasmcloud/cross:x86_64-unknown-linux-gnu" 15 | -------------------------------------------------------------------------------- /applier/Makefile: -------------------------------------------------------------------------------- 1 | # applier Makefile 2 | 3 | CAPABILITY_ID = "cosmonic:kubernetes_applier" 4 | NAME = "applier" 5 | VENDOR = "cosmonic" 6 | PROJECT = applier 7 | VERSION = 0.1.0 8 | REVISION = 0 9 | 10 | include ./provider.mk 11 | 12 | test-cleanup: 13 | docker stop applier-nats || true 14 | ps -ax | grep applier | grep -i -v make | awk '{print $$1}' | xargs kill -9 || true 15 | kubectl delete svc -l wasmcloud.dev/test || true 16 | 17 | test:: build test-cleanup 18 | cargo clippy --all-targets --all-features 19 | docker run --rm -d -p 4222:4222 --name applier-nats nats:2 20 | cargo test --tests 21 | $(MAKE) test-cleanup 22 | -------------------------------------------------------------------------------- /applier/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Applier Capability Provider 2 | 3 | This is a capability provider implementation of the `cosmonic:kubernetes_applier` contract. Its 4 | purpose is to take arbitrary manifests from an actor and do the equivalent of a `kubectl apply` to 5 | create the object. 6 | 7 | ## Using the provider 8 | 9 | TODO: Put in the OCI reference here once we push 10 | 11 | The only configuration required for linking to this provider is a valid kubeconfig. There are 3 ways 12 | of doing this: 13 | 14 | - The default (if no config is specified) will attempt to infer the kubeconfig from the default 15 | location (e.g. `$HOME/.kube/config`) or, if it is running in a pod, from the environment variables 16 | in the pod. This option is great for local testing and for running this provider within a host 17 | running in a pod. 18 | - The `config_b64` key: The value of this key should be the base64 encoded kubeconfig the provider 19 | should use. Please note that this kubeconfig should have all certs and tokens embedded within the 20 | kubeconfig (i.e. `client-certificate-data`). If any file paths are used, the link will be 21 | rejected. 22 | - The `config_file` key: A specific path where the kubeconfig should be loaded from. This option 23 | does allow file paths and is recommended when you have full control over the host and are storing 24 | the kubeconfig in a location other than the default 25 | 26 | ## Contributing 27 | 28 | We welcome all contributions! If you would like to submit changes, please open a [Pull 29 | Request](https://github.com/cosmonic/kubernetes-applier/pulls) and one of the maintainers will 30 | review it 31 | 32 | ### Prerequisites 33 | 34 | In order to build this module, you will need to have the following tools installed: 35 | 36 | - `make` 37 | - [`wash`](https://wasmcloud.dev/overview/installation/#install-wash) 38 | - `jq` 39 | 40 | ### Building 41 | 42 | To build the binary, simply run `make build`. To build and sign the provider for use with a 43 | wasmCloud host, run `make` 44 | 45 | ### Testing 46 | 47 | Before running the test, you need to have a valid kubeconfig pointing at a running Kubernetes 48 | cluster (we recommend using [kind](https://kind.sigs.k8s.io/)). 49 | 50 | For ease of testing, we use NATS in a docker image. The tests can be run manually by running `cargo 51 | test --tests` if you wish to setup your own NATS server. Otherwise, you can just run `make test` to 52 | run all tests. 53 | 54 | #### Troubleshooting 55 | 56 | For maximum compatibility, we use rustls for the TLS stack. However, this can cause issues with 57 | kubeconfigs that contain a server IP address rather than a FQDN (such as those created by `kind`). 58 | If you see an error about an unrecognized domain name, make sure the server entry in your kubeconfig 59 | is using a domain name (e.g. switching `127.0.0.1` to `localhost`). 60 | -------------------------------------------------------------------------------- /applier/provider.mk: -------------------------------------------------------------------------------- 1 | # provider.mk 2 | # 3 | # common rules for building capability providers 4 | # Some of these rules depend on GNUMakefile >= 4.0 5 | # 6 | # before including this, local project makefile should define the following 7 | # (to override defaults) 8 | # top_targets # list of targets that are applicable for this project 9 | # 10 | 11 | top_targets ?= all par par-full test clean 12 | 13 | platform_id = $(shell uname -s) 14 | platform = $$( \ 15 | case $(platform_id) in \ 16 | ( Linux ) echo $(platform_id) ;; \ 17 | ( Darwin ) echo $(platform_id) ;; \ 18 | ( * ) echo Unrecognized Platform;; \ 19 | esac ) 20 | 21 | machine_id = $(shell uname -m ) 22 | 23 | # name of compiled binary 24 | bin_name ?= $(PROJECT) 25 | dest_par ?= build/$(bin_name).par.gz 26 | link_name ?= default 27 | 28 | # If name is not defined, use project 29 | NAME ?= $(PROJECT) 30 | 31 | WASH ?= wash 32 | 33 | oci_url_base ?= localhost:5000/v2 34 | oci_url ?= $(oci_url_base)/$(bin_name):$(VERSION) 35 | ifeq ($(WASH_REG_USER),) 36 | oci_insecure := --insecure 37 | endif 38 | 39 | par_targets ?= \ 40 | x86_64-unknown-linux-gnu \ 41 | x86_64-apple-darwin \ 42 | aarch64-unknown-linux-gnu \ 43 | aarch64-apple-darwin \ 44 | armv7-unknown-linux-gnueabihf \ 45 | x86_64-pc-windows-gnu 46 | 47 | # Lookup table from rust target triple to wasmcloud architecture doubles 48 | # Thanks to https://stackoverflow.com/a/40919906 for the pointer to 49 | # "constructed macro names". 50 | ARCH_LOOKUP_x86_64-unknown-linux-gnu=x86_64-linux 51 | ARCH_LOOKUP_x86_64-apple-darwin=x86_64-macos 52 | ARCH_LOOKUP_armv7-unknown-linux-gnueabihf=arm-linux 53 | ARCH_LOOKUP_aarch64-unknown-linux-gnu=aarch64-linux 54 | ARCH_LOOKUP_aarch64-apple-darwin=aarch64-macos 55 | ARCH_LOOKUP_x86_64-pc-windows-gnu=x86_64-windows 56 | 57 | bin_targets = $(foreach target,$(par_targets),target/$(target)/release/$(bin_name)) 58 | 59 | # pick target0 for starting par based on default rust target 60 | par_target0 ?= $(shell rustup show | grep 'Default host' | sed "s/Default host: //") 61 | 62 | # the target of the current platform, as defined by cross 63 | cross_target0=target/$(par_target0)/release/$(bin_name) 64 | # bin_target0=$(cross_target0) 65 | bin_target0=target/release/$(bin_name) 66 | 67 | # traverse subdirs 68 | .ONESHELL: 69 | ifneq ($(subdirs),) 70 | $(top_targets):: 71 | for dir in $(subdirs); do \ 72 | $(MAKE) -C $$dir $@ ; \ 73 | done 74 | endif 75 | 76 | # default target 77 | all:: $(dest_par) 78 | 79 | par:: $(dest_par) 80 | 81 | # rebuild base par if target0 changes 82 | $(dest_par): $(bin_target0) Makefile Cargo.toml 83 | @mkdir -p $(dir $(dest_par)) 84 | $(WASH) par create \ 85 | --arch $(ARCH_LOOKUP_$(par_target0)) \ 86 | --binary $(bin_target0) \ 87 | --capid $(CAPABILITY_ID) \ 88 | --name $(NAME) \ 89 | --vendor $(VENDOR) \ 90 | --version $(VERSION) \ 91 | --revision $(REVISION) \ 92 | --destination $@ \ 93 | --compress 94 | @echo Created $@ 95 | 96 | # par-full adds all the other targets to the base par 97 | par-full: $(dest_par) $(bin_targets) 98 | for target in $(par_targets); do \ 99 | target_dest=target/$${target}/release/$(bin_name); \ 100 | if [ $$target = "x86_64-pc-windows-gnu" ]; then \ 101 | target_dest=$$target_dest.exe; \ 102 | fi; \ 103 | par_arch=`printf $$target | sed -E 's/([^-]+)-([^-]+)-([^-]+)(-gnu.*)?/\1-\3/' | sed 's/darwin/macos/'`; \ 104 | echo building $$par_arch; \ 105 | if [ $$target_dest != $(cross_target0) ] && [ -f $$target_dest ]; then \ 106 | $(WASH) par insert --arch $$par_arch --binary $$target_dest $(dest_par); \ 107 | fi; \ 108 | done 109 | 110 | # create rust build targets 111 | ifeq ($(wildcard ./Cargo.toml),./Cargo.toml) 112 | 113 | # rust dependencies 114 | RUST_DEPS += $(wildcard src/*.rs) $(wildcard target/*/deps/*) Cargo.toml Makefile 115 | 116 | target/release/$(bin_name): $(RUST_DEPS) 117 | cargo build --release 118 | 119 | target/debug/$(bin_name): $(RUST_DEPS) 120 | cargo build 121 | 122 | # cross-compile target, remove intermediate build artifacts before build 123 | target/%/release/$(bin_name): $(RUST_DEPS) 124 | tname=`printf $@ | sed -E 's_target/([^/]+)/release.*$$_\1_'` &&\ 125 | rm -rf target/release/build &&\ 126 | rm -rf target/release/deps &&\ 127 | cross build --release --target $$tname 128 | 129 | endif 130 | 131 | # rules to print file name and path of build target 132 | target-path: 133 | @echo $(dest_par) 134 | target-path-abs: 135 | @echo $(abspath $(dest_par)) 136 | target-file: 137 | @echo $(notdir $(dest_par)) 138 | 139 | 140 | # push par file to registry 141 | push: $(dest_par) 142 | $(WASH) reg push $(oci_insecure) $(oci_url) $(dest_par) 143 | 144 | # start provider 145 | start: 146 | $(WASH) ctl start provider $(oci_url) \ 147 | --host-id $(shell $(WASH) ctl get hosts -o json | jq -r ".hosts[0].id") \ 148 | --link-name $(link_name) \ 149 | --timeout-ms 4000 150 | 151 | # inspect claims on par file 152 | inspect: $(dest_par) 153 | $(WASH) par inspect $(dest_par) 154 | 155 | inventory: 156 | $(WASH) ctl get inventory $(shell $(WASH) ctl get hosts -o json | jq -r ".hosts[0].id") 157 | 158 | 159 | # clean: remove built par files, but don't clean if we're in top-level dir 160 | ifeq ($(wildcard build/makefiles),) 161 | clean:: 162 | rm -rf build/ 163 | endif 164 | 165 | 166 | ifeq ($(wildcard ./Cargo.toml),./Cargo.toml) 167 | build:: 168 | cargo build 169 | 170 | release:: 171 | cargo build --release 172 | 173 | clean:: 174 | cargo clean 175 | if command -v cross; then cross clean; fi 176 | 177 | endif 178 | 179 | 180 | install-cross: ## Helper function to install the proper `cross` version 181 | cargo install --git https://github.com/ChrisRx/cross --branch add-darwin-target --force 182 | 183 | 184 | # for debugging - show variables make is using 185 | make-vars: 186 | @echo "platform_id : $(platform_id)" 187 | @echo "platform : $(platform)" 188 | @echo "machine_id : $(machine_id)" 189 | @echo "default-par : $(par_target0)" 190 | @echo "project_dir : $(project_dir)" 191 | @echo "subdirs : $(subdirs)" 192 | @echo "top_targets : $(top_targets)" 193 | @echo "NAME : $(NAME)" 194 | @echo "VENDOR : $(VENDOR)" 195 | @echo "VERSION : $(VERSION)" 196 | @echo "REVISION : $(REVISION)" 197 | 198 | 199 | .PHONY: all par par-full test clean 200 | -------------------------------------------------------------------------------- /applier/provider_test_config.toml: -------------------------------------------------------------------------------- 1 | # configuration for applier test 2 | 3 | # name of compiled binary (usually project name unless overridden in [[bin]] 4 | # Required 5 | bin_path = "target/debug/applier" 6 | 7 | # set RUST_LOG environment variable (default "info") 8 | rust_log = "debug" 9 | 10 | # set RUST_BACKTRACE (default: 0) 11 | rust_backtrace = "1" 12 | 13 | # nats should be running. Uncomment to override the default url 14 | #nats_url = "0.0.0.0:4222" 15 | 16 | # lattice prefix (default "default") 17 | #lattice_rpc_prefix = "default" 18 | 19 | # link name (default: "default") 20 | #link_name = "default" 21 | 22 | # name of contract under test 23 | contract_id = "cosmonic:kubernetes_applier" 24 | -------------------------------------------------------------------------------- /applier/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Kubernetes applier capability provider 2 | //! 3 | //! 4 | use kube::{ 5 | api::{DeleteParams, DynamicObject, PatchParams, PostParams}, 6 | config::{KubeConfigOptions, Kubeconfig}, 7 | core::{params::Patch, ApiResource, GroupVersionKind}, 8 | Api, Client, Config, 9 | }; 10 | use kubernetes_applier_interface::{ 11 | DeleteRequest, KubernetesApplier, KubernetesApplierReceiver, OperationResponse, 12 | }; 13 | use tokio::sync::RwLock; 14 | use tracing::{debug, info, instrument, trace}; 15 | use wasmbus_rpc::provider::prelude::*; 16 | 17 | use std::collections::HashMap; 18 | use std::sync::Arc; 19 | 20 | /// Loading a kubeconfig from a file 21 | const CONFIG_FILE_KEY: &str = "config_file"; 22 | /// Passing a kubeconfig as a base64 encoding string. This config should contain embedded 23 | /// certificates rather than paths to certificates 24 | const CONFIG_B64_KEY: &str = "config_b64"; 25 | 26 | const CERT_PATH_ERROR: &str = 27 | "Certificate and key paths are not allowed for base64 encoded configs. Offending entry:"; 28 | const FIELD_MANAGER: &str = "kubernetes-applier-provider"; 29 | 30 | // main (via provider_main) initializes the threaded tokio executor, 31 | // listens to lattice rpcs, handles actor links, 32 | // and returns only when it receives a shutdown message 33 | // 34 | fn main() -> Result<(), Box> { 35 | info!("Starting provider process"); 36 | provider_main( 37 | ApplierProvider::default(), 38 | Some("Kubernetes Applier Provider".to_string()), 39 | )?; 40 | 41 | info!("Applier provider exiting"); 42 | Ok(()) 43 | } 44 | 45 | /// applier capability provider implementation 46 | #[derive(Default, Clone, Provider)] 47 | #[services(KubernetesApplier)] 48 | struct ApplierProvider { 49 | clients: Arc>>, 50 | } 51 | 52 | impl ProviderDispatch for ApplierProvider {} 53 | #[async_trait] 54 | impl ProviderHandler for ApplierProvider { 55 | #[instrument(level = "debug", skip(self, ld), fields(actor_id = %ld.actor_id))] 56 | async fn put_link(&self, ld: &LinkDefinition) -> Result { 57 | debug!("Got link request"); 58 | // Normalize keys to lowercase 59 | let values: HashMap = ld 60 | .values 61 | .iter() 62 | .map(|(k, v)| (k.to_lowercase(), v.to_owned())) 63 | .collect(); 64 | 65 | // Attempt to load the config. If nothing it passed attempt to infer it from the pod or the 66 | // default kubeconfig path 67 | let config = if let Some(p) = values.get(CONFIG_FILE_KEY) { 68 | let path = p.to_owned(); 69 | debug!(%path, "Loading kubeconfig from file"); 70 | let conf = tokio::task::spawn_blocking(move || Kubeconfig::read_from(path)) 71 | .await 72 | .map_err(|e| { 73 | RpcError::ProviderInit(format!( 74 | "Internal error occured while loading kubeconfig: {}", 75 | e 76 | )) 77 | })? 78 | .map_err(|e| format!("Invalid kubeconfig from file {}: {}", p, e))?; 79 | Config::from_custom_kubeconfig(conf, &KubeConfigOptions::default()) 80 | .await 81 | .map_err(|e| { 82 | RpcError::ProviderInit(format!("Invalid kubeconfig from file {}: {}", p, e)) 83 | })? 84 | } else if let Some(raw) = values.get(CONFIG_B64_KEY) { 85 | debug!("Loading config from base64 encoded string"); 86 | let decoded = base64::decode(raw).map_err(|e| { 87 | RpcError::ProviderInit(format!("Invalid base64 config given: {}", e)) 88 | })?; 89 | // NOTE: We do not support multiple yaml documents in the same file. We shouldn't need 90 | // this, but if we do, we can borrow some of the logic from the `kube` crate 91 | let conf: Kubeconfig = serde_yaml::from_slice(&decoded).map_err(|e| { 92 | RpcError::ProviderInit(format!("Invalid kubeconfig data given: {}", e)) 93 | })?; 94 | // Security: check that cert paths are not set as they could access certs on the host 95 | // runtime 96 | trace!("Ensuring base64 encoded config does not contain paths"); 97 | for cluster in conf.clusters.iter() { 98 | ensure_no_path( 99 | &cluster.cluster.certificate_authority, 100 | "cluster", 101 | &cluster.name, 102 | )?; 103 | } 104 | for user in conf.auth_infos.iter() { 105 | ensure_no_path( 106 | &user.auth_info.client_certificate, 107 | "client_certificate", 108 | &user.name, 109 | )?; 110 | ensure_no_path(&user.auth_info.client_key, "client_key", &user.name)?; 111 | ensure_no_path(&user.auth_info.token_file, "token_file", &user.name)?; 112 | } 113 | Config::from_custom_kubeconfig(conf, &KubeConfigOptions::default()) 114 | .await 115 | .map_err(|e| { 116 | RpcError::ProviderInit(format!("Invalid kubeconfig from base64: {}", e)) 117 | })? 118 | } else { 119 | debug!("No config given, inferring config from environment"); 120 | // If no config was manually specified we try to infer it from local pod variables or 121 | // the default kubeconfig path 122 | Config::infer().await.map_err(|e| RpcError::ProviderInit(format!("No config given and unable to infer config from environment or default config file: {}", e)))? 123 | }; 124 | 125 | tracing::trace!(?config, "Attempting to create client and connect to server"); 126 | // Now create the client and make sure it works 127 | let client = Client::try_from(config).map_err(|e| { 128 | RpcError::ProviderInit(format!( 129 | "Unable to create client from loaded kubeconfig: {}", 130 | e 131 | )) 132 | })?; 133 | 134 | // NOTE: In the future, we may want to improve this with a retry 135 | client.apiserver_version().await.map_err(|e| { 136 | RpcError::ProviderInit(format!( 137 | "Unable to connect to the Kubernetes API server: {}", 138 | e 139 | )) 140 | })?; 141 | tracing::trace!("Successfully connected to server"); 142 | 143 | let mut clients = self.clients.write().await; 144 | clients.insert(ld.actor_id.clone(), client); 145 | Ok(true) 146 | } 147 | 148 | async fn delete_link(&self, actor_id: &str) { 149 | self.clients.write().await.remove(actor_id); 150 | } 151 | } 152 | 153 | #[async_trait] 154 | impl KubernetesApplier for ApplierProvider { 155 | #[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, object_name = tracing::field::Empty))] 156 | async fn apply(&self, ctx: &Context, arg: &Vec) -> RpcResult { 157 | trace!(body_len = arg.len(), "Decoding object for apply"); 158 | let object: DynamicObject = serde_yaml::from_slice(arg).map_err(|e| { 159 | RpcError::InvalidParameter(format!("Unable to parse data as kubernetes object: {}", e)) 160 | })?; 161 | 162 | let obj_name = object 163 | .metadata 164 | .name 165 | .as_ref() 166 | .ok_or_else(|| { 167 | RpcError::InvalidParameter("The given object is missing a name".to_string()) 168 | })? 169 | .as_str(); 170 | 171 | tracing::span::Span::current().record("object_name", &tracing::field::display(obj_name)); 172 | 173 | let type_data = object.types.as_ref().ok_or_else(|| { 174 | RpcError::InvalidParameter( 175 | "The given manifest does not contain type information".to_string(), 176 | ) 177 | })?; 178 | // Decompose api_version into the parts we need to type the request 179 | let (group, version) = match type_data.api_version.split_once('/') { 180 | Some((g, v)) => (g.to_owned(), v.to_owned()), 181 | None => (String::new(), type_data.api_version.to_owned()), 182 | }; 183 | let gvk = GroupVersionKind { 184 | group, 185 | version, 186 | kind: type_data.kind.clone(), 187 | }; 188 | let resource = ApiResource::from_gvk(&gvk); 189 | 190 | trace!(?gvk, "Inferred object type from data"); 191 | 192 | let client = self.get_client(ctx).await?; 193 | 194 | let api: Api = if let Some(ns) = object.metadata.namespace.as_ref() { 195 | Api::namespaced_with(client, ns.as_str(), &resource) 196 | } else { 197 | Api::default_namespaced_with(client, &resource) 198 | }; 199 | 200 | debug!("Attempting to apply object to api"); 201 | 202 | trace!("Checking if object already exists"); 203 | let exists = match api.get(obj_name).await { 204 | Ok(_) => true, 205 | Err(kube::Error::Api(e)) if e.code == 404 => false, 206 | // TODO: retries in case of flakiness? 207 | Err(e) => { 208 | return Ok(OperationResponse { 209 | succeeded: false, 210 | error: Some(format!("Unable to fetch object from API: {}", e)), 211 | }) 212 | } 213 | }; 214 | 215 | let resp = if exists { 216 | trace!("Object already exists, attempting server-side apply"); 217 | api.patch( 218 | obj_name, 219 | &PatchParams { 220 | field_manager: Some(FIELD_MANAGER.to_string()), 221 | ..Default::default() 222 | }, 223 | &Patch::Apply(&object), 224 | ) 225 | .await 226 | } else { 227 | trace!("Object does not exist, creating"); 228 | api.create( 229 | &PostParams { 230 | field_manager: Some(FIELD_MANAGER.to_string()), 231 | ..Default::default() 232 | }, 233 | &object, 234 | ) 235 | .await 236 | }; 237 | 238 | if let Err(e) = resp { 239 | return Ok(OperationResponse { 240 | succeeded: false, 241 | error: Some(e.to_string()), 242 | }); 243 | } 244 | 245 | Ok(OperationResponse { 246 | succeeded: true, 247 | error: None, 248 | }) 249 | } 250 | 251 | #[instrument(level = "debug", skip(self, ctx), fields(actor_id = ?ctx.actor))] 252 | async fn delete(&self, ctx: &Context, arg: &DeleteRequest) -> RpcResult { 253 | let client = self.get_client(ctx).await?; 254 | 255 | let resource = ApiResource::from_gvk(&GroupVersionKind { 256 | group: arg.group.clone(), 257 | version: arg.version.clone(), 258 | kind: arg.kind.clone(), 259 | }); 260 | 261 | let api: Api = if let Some(ns) = arg.namespace.as_ref() { 262 | Api::namespaced_with(client, ns.as_str(), &resource) 263 | } else { 264 | Api::default_namespaced_with(client, &resource) 265 | }; 266 | debug!("Attempting to delete object"); 267 | match api 268 | .delete(arg.name.as_str(), &DeleteParams::default()) 269 | .await 270 | { 271 | // If it is ok or returns not found, that means we are ok 272 | Ok(_) => Ok(OperationResponse { 273 | succeeded: true, 274 | error: None, 275 | }), 276 | Err(kube::Error::Api(e)) if e.code == 404 => Ok(OperationResponse { 277 | succeeded: true, 278 | error: None, 279 | }), 280 | Err(e) => Ok(OperationResponse { 281 | succeeded: false, 282 | error: Some(e.to_string()), 283 | }), 284 | } 285 | } 286 | } 287 | 288 | impl ApplierProvider { 289 | async fn get_client(&self, ctx: &Context) -> RpcResult { 290 | let actor_id = ctx.actor.as_ref().ok_or_else(|| { 291 | RpcError::InvalidParameter("Actor ID does not exist on request".to_string()) 292 | })?; 293 | Ok(self 294 | .clients 295 | .read() 296 | .await 297 | .get(actor_id.as_str()) 298 | .ok_or_else(|| { 299 | RpcError::InvalidParameter(format!("No link registered for actor {}", actor_id)) 300 | })? 301 | .clone()) 302 | } 303 | } 304 | 305 | fn ensure_no_path(item: &Option, entity: &str, name: &str) -> Result<(), RpcError> { 306 | if item.is_some() { 307 | return Err(RpcError::ProviderInit(format!( 308 | "{} {} {}", 309 | CERT_PATH_ERROR, entity, name 310 | ))); 311 | } 312 | Ok(()) 313 | } 314 | -------------------------------------------------------------------------------- /applier/tests/applier_test.rs: -------------------------------------------------------------------------------- 1 | use k8s_openapi::api::core::v1::Service; 2 | use kube::{api::PostParams, Api}; 3 | use kubernetes_applier_interface::*; 4 | use wasmbus_rpc::{error::RpcError, provider::prelude::*}; 5 | use wasmcloud_test_util::{ 6 | check, 7 | cli::print_test_results, 8 | provider_test::test_provider, 9 | testing::{TestOptions, TestResult}, 10 | }; 11 | #[allow(unused_imports)] 12 | use wasmcloud_test_util::{run_selected, run_selected_spawn}; 13 | 14 | #[tokio::test] 15 | async fn run_all() { 16 | let opts = TestOptions::default(); 17 | let res = run_selected_spawn!( 18 | opts, 19 | health_check, 20 | create_update_delete_happy_path, 21 | invalid_create, 22 | invalid_update, 23 | nonexistent_delete 24 | ); 25 | print_test_results(&res); 26 | 27 | let passed = res.iter().filter(|tr| tr.passed).count(); 28 | let total = res.len(); 29 | assert_eq!(passed, total, "{} passed out of {}", passed, total); 30 | 31 | // try to let the provider shut down gracefully 32 | let provider = test_provider().await; 33 | let _ = provider.shutdown().await; 34 | } 35 | 36 | /// test that health check returns healthy 37 | async fn health_check(_opt: &TestOptions) -> RpcResult<()> { 38 | let prov = test_provider().await; 39 | 40 | // health check 41 | let hc = prov.health_check().await; 42 | check!(hc.is_ok())?; 43 | Ok(()) 44 | } 45 | 46 | const VALID_MANIFEST: &str = r#"apiVersion: v1 47 | kind: Service 48 | metadata: 49 | name: foo-applier-test-happy 50 | labels: 51 | wasmcloud.dev/test: "true" 52 | spec: 53 | selector: 54 | app.kubernetes.io/name: foo-applier 55 | ports: 56 | - protocol: TCP 57 | port: 8080 58 | targetPort: 8080"#; 59 | 60 | const VALID_MANIFEST_WITH_LABELS: &str = r#"apiVersion: v1 61 | kind: Service 62 | metadata: 63 | name: foo-applier-test-happy 64 | labels: 65 | wasmcloud.dev/test: "true" 66 | foo: happy 67 | spec: 68 | selector: 69 | app.kubernetes.io/name: foo-applier 70 | ports: 71 | - protocol: TCP 72 | port: 8080 73 | targetPort: 8080"#; 74 | 75 | /// Test the happy path of creating updating and deleting 76 | async fn create_update_delete_happy_path(_opt: &TestOptions) -> RpcResult<()> { 77 | let prov = test_provider().await; 78 | let svc_name = "foo-applier-test-happy"; 79 | 80 | let client = kube::Client::try_default() 81 | .await 82 | .expect("Unable to get client"); 83 | let api: Api = Api::default_namespaced(client); 84 | 85 | // The test scaffolding doesn't wait for an ack from the link, so wait for a bit 86 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 87 | 88 | let actor_id = prov.origin().public_key(); 89 | // create client and ctx 90 | let client = KubernetesApplierSender::via(prov); 91 | let ctx = Context { 92 | actor: Some(actor_id), 93 | ..Default::default() 94 | }; 95 | 96 | let resp = client 97 | .apply(&ctx, &VALID_MANIFEST.as_bytes().to_vec()) 98 | .await?; 99 | assert!(resp.succeeded, "Create should have succeeded"); 100 | 101 | // Validate service exists 102 | api.get(svc_name) 103 | .await 104 | .unwrap_or_else(|_| panic!("Service {} does not exist", svc_name)); 105 | 106 | let resp = client 107 | .apply(&ctx, &VALID_MANIFEST_WITH_LABELS.as_bytes().to_vec()) 108 | .await?; 109 | assert!(resp.succeeded, "Update should have succeeded"); 110 | 111 | let svc = api 112 | .get(svc_name) 113 | .await 114 | .unwrap_or_else(|_| panic!("Service {} does not exist", svc_name)); 115 | 116 | assert_eq!( 117 | svc.metadata 118 | .labels 119 | .expect("Should have labels present") 120 | .get("foo") 121 | .expect("foo label doesn't exist"), 122 | "happy", 123 | "Label value should be set correctly" 124 | ); 125 | 126 | let resp = client 127 | .delete( 128 | &ctx, 129 | &DeleteRequest { 130 | group: String::new(), 131 | kind: "Service".into(), 132 | version: "v1".into(), 133 | name: svc_name.into(), 134 | ..Default::default() 135 | }, 136 | ) 137 | .await?; 138 | assert!(resp.succeeded, "Delete should have succeeded"); 139 | if api.get(svc_name).await.is_ok() { 140 | panic!("Service {} should be deleted", svc_name) 141 | } 142 | Ok(()) 143 | } 144 | 145 | // TODO: Test base64 config and file path config once https://github.com/wasmCloud/wasmcloud-test/issues/6 is fixed 146 | 147 | const INVALID_MANIFEST: &str = r#"apiVersion: v1 148 | kind: NotReal 149 | metadata: 150 | name: foo-applier-test-invalid 151 | labels: 152 | wasmcloud.dev/test: "true" 153 | spec: 154 | selector: 155 | app.kubernetes.io/name: foo-applier 156 | ports: 157 | - protocol: TCP 158 | port: 8080 159 | targetPort: 8080 160 | totallyNotValid: bar"#; 161 | 162 | /// Test that an invalid create fails 163 | async fn invalid_create(_opt: &TestOptions) -> RpcResult<()> { 164 | let prov = test_provider().await; 165 | // The test scaffolding doesn't wait for an ack from the link, so wait for a bit 166 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 167 | 168 | let actor_id = prov.origin().public_key(); 169 | // create client and ctx 170 | let client = KubernetesApplierSender::via(prov); 171 | let ctx = Context { 172 | actor: Some(actor_id), 173 | ..Default::default() 174 | }; 175 | 176 | let resp = client 177 | .apply(&ctx, &INVALID_MANIFEST.as_bytes().to_vec()) 178 | .await?; 179 | assert!(!resp.succeeded, "Create should not have succeeded"); 180 | assert!(resp.error.is_some(), "Error message should be set"); 181 | 182 | Ok(()) 183 | } 184 | 185 | const INVALID_UPDATE_MANIFEST: &str = r#"apiVersion: v1 186 | kind: Service 187 | metadata: 188 | name: foo-applier-test-happy 189 | labels: 190 | wasmcloud.dev/test: "true" 191 | spec: 192 | selector: 193 | app.kubernetes.io/name: foo-applier 194 | ports: 195 | - protocol: TCP 196 | port: 8080 197 | targetPort: 8080 198 | totallyNotValid: bar"#; 199 | 200 | /// Test that an invalid update fails 201 | async fn invalid_update(_opt: &TestOptions) -> RpcResult<()> { 202 | let prov = test_provider().await; 203 | 204 | let client = kube::Client::try_default() 205 | .await 206 | .expect("Unable to get client"); 207 | let api: Api = Api::default_namespaced(client); 208 | 209 | let valid: Service = serde_yaml::from_str(VALID_MANIFEST).unwrap(); 210 | // Create a good service first 211 | api.create(&PostParams::default(), &valid) 212 | .await 213 | .expect("Should be able to create valid service"); 214 | 215 | // The test scaffolding doesn't wait for an ack from the link, so wait for a bit 216 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 217 | 218 | let actor_id = prov.origin().public_key(); 219 | // create client and ctx 220 | let client = KubernetesApplierSender::via(prov); 221 | let ctx = Context { 222 | actor: Some(actor_id), 223 | ..Default::default() 224 | }; 225 | 226 | let resp = client 227 | .apply(&ctx, &INVALID_UPDATE_MANIFEST.as_bytes().to_vec()) 228 | .await?; 229 | assert!(!resp.succeeded, "Update should not have succeeded"); 230 | assert!(resp.error.is_some(), "Error message should be set"); 231 | 232 | Ok(()) 233 | } 234 | 235 | /// Test that a non-existent delete succeeds 236 | async fn nonexistent_delete(_opt: &TestOptions) -> RpcResult<()> { 237 | let prov = test_provider().await; 238 | let svc_name = "foo-applier-test-noexist"; 239 | 240 | // The test scaffolding doesn't wait for an ack from the link, so wait for a bit 241 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 242 | 243 | let actor_id = prov.origin().public_key(); 244 | // create client and ctx 245 | let client = KubernetesApplierSender::via(prov); 246 | let ctx = Context { 247 | actor: Some(actor_id), 248 | ..Default::default() 249 | }; 250 | 251 | let resp = client 252 | .delete( 253 | &ctx, 254 | &DeleteRequest { 255 | group: String::new(), 256 | kind: "Service".into(), 257 | version: "v1".into(), 258 | name: svc_name.into(), 259 | ..Default::default() 260 | }, 261 | ) 262 | .await?; 263 | assert!(resp.succeeded, "Delete should have succeeded"); 264 | 265 | Ok(()) 266 | } 267 | -------------------------------------------------------------------------------- /interface/.gitignore: -------------------------------------------------------------------------------- 1 | # This file lists build byproducts, 2 | # IDE-specific files (unless shared by your team) 3 | 4 | # 5 | # Cargo.lock is not included for interface libraries 6 | Cargo.lock 7 | # uncomment the following line if you don't want to check in generated html docs 8 | #html 9 | # 10 | 11 | ## Build 12 | /build 13 | /dist/ 14 | /target 15 | **target 16 | 17 | ## File system 18 | .DS_Store 19 | desktop.ini 20 | 21 | ## Editor 22 | *.swp 23 | *.swo 24 | Session.vim 25 | .cproject 26 | .idea 27 | *.iml 28 | .vscode 29 | .project 30 | .favorites.json 31 | .settings/ 32 | 33 | ## Temporary files 34 | *~ 35 | \#* 36 | \#*\# 37 | .#* 38 | 39 | ## Python 40 | __pycache__/ 41 | *.py[cod] 42 | *$py.class 43 | 44 | ## Node 45 | **node_modules 46 | **package-lock.json 47 | 48 | -------------------------------------------------------------------------------- /interface/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for interface kubernetes-applier 2 | 3 | include ./interface.mk 4 | 5 | -------------------------------------------------------------------------------- /interface/README.md: -------------------------------------------------------------------------------- 1 | # Interface for the Factorial service, wasmcloud:example:factorial 2 | 3 | This is an interface for a simple service that calculates 4 | the fatorial of a whole number. 5 | -------------------------------------------------------------------------------- /interface/codegen.toml: -------------------------------------------------------------------------------- 1 | # codegen.toml 2 | 3 | # `models` contains a list of smithy model file(s), 4 | # and folder(s) containing .smithy files, used for input to code generators, 5 | # documentation generator, and linting and validation. 6 | # Dependencies of the model should also be included in the `models` list 7 | # because they will improve documentation and validation. 8 | # 9 | # The namespaces(s) that will be generated for this library are indicated 10 | # in the per-language file settings later in this file. 11 | [[models]] 12 | path = "." 13 | files = [ "kubernetes_applier.smithy" ] 14 | 15 | 16 | [[models]] 17 | # Location of dependencies may be either a path to a directory, or a url prefix 18 | # If a relative path is used, it is relative to the location of this codegen.toml 19 | #path = "/path/to/my-interfaces" 20 | url = "https://wasmcloud.github.io/interfaces/idl/org.wasmcloud" 21 | files = [ "wasmcloud-core.smithy", "wasmcloud-model.smithy" ] 22 | 23 | 24 | ## 25 | ## HTML documentation output 26 | ## 27 | [html] 28 | 29 | # (optional) template dir to scan (overrides compiled-in templates) 30 | #templates = "docgen/templates" 31 | # Top-level output directory for html generated files 32 | output_dir = "html" 33 | 34 | # Additional parameters for html generation 35 | [html.parameters] 36 | 37 | # name of template for page generation (default: 'namespace_doc') 38 | #doc_template = "namespace_doc" 39 | 40 | # whether to use minified tailwind.css (default false) 41 | minified = true 42 | 43 | 44 | ## 45 | ## Rust language output 46 | ## 47 | [rust] 48 | 49 | # top-level output directory for rust files. 50 | output_dir = "rust" 51 | 52 | [rust.parameters] 53 | 54 | # File-specific settings contain the following settings 55 | # [[rust.files]] 56 | # path - (required) path for generated output file, relative to output_dir above 57 | # hbs - handlebars template name (without .hbs extension) 58 | # Only applicable if file is generated by a handlebars template 59 | # create_only - whether file should be generated only with --create (default false) 60 | # namespace - limit generated shapes to shapes in this namespace 61 | # * - any other fields are per-file parameters passed to codegen and renderer 62 | 63 | # Additional namespaces may be added to this library crate by adding 64 | # a rust source file below for each namespace, 65 | # and importing each of them into src/lib.rs 66 | 67 | [[rust.files]] 68 | path = "src/kubernetes_applier.rs" 69 | namespace = "com.cosmonic.kubernetesapplier" 70 | -------------------------------------------------------------------------------- /interface/html/org_wasmcloud_core.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | org.wasmcloud.core 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Namespace org.wasmcloud.core

16 |
17 |
18 | Services 19 |
20 |
21 | 23 |
24 |
25 |
26 | Operations 27 |
28 |
29 | 31 |
32 |
41 |
42 | Lists 43 |
44 |
45 | 47 |
48 |
49 |
50 | Maps 51 |
52 |
53 | 55 |
56 |
57 | 58 | 59 |

60 | Actor(service) 61 |

Actor service

62 |
63 |
64 |
65 | 66 | Operations 67 | 68 |
69 |
70 | 75 |
76 |
77 |
78 |
79 |

Traits 80 |

81 | 82 | 83 | 84 |
org.wasmcloud.model#wasmbus[object]
85 |

86 | 87 | 88 |

List of linked actors for a provider

91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
member:LinkDefinition 
108 |
109 | 110 | 111 | 112 |

113 | HealthCheckRequest(structure) 114 |

health check request parameter

115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
126 |
127 |
128 |

Traits 129 |

130 | 131 | 132 | 133 |
org.wasmcloud.model#wasmbusData[object]
134 |

135 | 136 | 137 |

138 | HealthCheckResponse(structure) 139 |

Return value from actors and providers for health check status

140 |
141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 |
healthy:BooleanA flag that indicates the the actor is healthy
message:StringA message containing additional information about the actors health
159 |
160 |
161 |

Traits 162 |

163 | 164 | 165 | 166 |
org.wasmcloud.model#wasmbusData[object]
167 |

168 | 169 | 170 |

171 | HealthRequest(operation) 172 |

Perform health check. Called at regular intervals by host

173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
input:HealthCheckRequest
output:HealthCheckResponse
192 |
193 | 194 | 195 | 196 |

197 | HostData(structure) 198 |

initialization data for a capability provider

199 |
200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 |
envValues:HostEnvValues
hostId:String
invocationSeed:String
latticeRpcPrefix:String
latticeRpcUrl:String
latticeRpcUserJwt:String
latticeRpcUserSeed:String
linkName:String
providerKey:String
246 |
247 |
248 |

Traits 249 |

250 | 251 | 252 | 253 |
org.wasmcloud.model#wasmbusData[object]
254 |

255 | 256 | 257 |

258 | HostEnvValues(map) 259 |

Environment settings for initializing a capability provider

260 |
261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 |
key:String
value:String
282 |
283 | 284 | 285 | 286 |

287 | Invocation(structure) 288 |

RPC message to capability provider

289 |
290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 |
encodedClaims:String
hostId:String
id:String
msg:Blob
operation:String
origin:WasmCloudEntity
target:WasmCloudEntity
328 |
329 |
330 |

Traits 331 |

332 | 333 | 334 | 335 |
org.wasmcloud.model#wasmbusData[object]
336 |

337 | 338 | 339 |

340 | InvocationResponse(structure) 341 |

Response to an invocation

342 |
343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
error:Stringoptional error message
invocationId:Stringid connecting this response to the invocation
msg:Blobserialize response message
365 |
366 |
367 |

Traits 368 |

369 | 370 | 371 | 372 |
org.wasmcloud.model#wasmbusData[object]
373 |

374 | 375 | 376 |

Link definition for binding actor to provider

379 |
380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 |
actorId:Stringactor public key
contractId:Stringcontract id
linkName:Stringlink name
providerId:Stringprovider public key
values:LinkSettings
410 |
411 |
412 |

Traits 413 |

414 | 415 | 416 | 417 |
org.wasmcloud.model#wasmbusData[object]
418 |

419 | 420 | 421 |

Settings associated with an actor-provider link

424 |
425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 |
key:String
value:String
446 |
447 | 448 | 449 | 450 |

451 | WasmCloudEntity(structure) 452 |

453 |
454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 |
contractId:org.wasmcloud.model#CapabilityContractId
linkName:String
publicKey:String
476 |
477 |
478 |

Traits 479 |

480 | 481 | 482 | 483 |
org.wasmcloud.model#wasmbusData[object]
484 |

485 |
486 |
487 | 488 | -------------------------------------------------------------------------------- /interface/html/org_wasmcloud_interface_factorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | org.wasmcloud.interface.factorial 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Namespace org.wasmcloud.interface.factorial

16 |
17 |
18 | Operations 19 |
20 |
21 | 23 |
24 |
25 | 26 | 27 |

28 | Factorial(operation) 29 |

Calculates n! (n factorial)

30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
input:org.wasmcloud.model#U32
output:org.wasmcloud.model#U32
49 |
50 | 51 |
52 |
53 | 54 | -------------------------------------------------------------------------------- /interface/html/org_wasmcloud_model.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | org.wasmcloud.model 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Namespace org.wasmcloud.model

16 |
25 |
26 | Simple 27 |
28 | 32 |
33 |
34 | Lists 35 |
36 |
37 | 39 |
40 |
41 | 42 | 43 |

44 | CapabilityContractId(string) 45 |

Capability contract id, e.g. 'wasmcloud:httpserver'

46 | 47 |
48 |

Traits 49 |

50 | 51 | 52 | 53 |
nonEmptyString[object]
54 |

55 | 56 | 57 |

58 | codegenRust(structure) 59 |

Rust codegen traits

60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 83 | 84 |
noDeriveDefault:Boolean 
noDeriveEq:Boolean 
85 |
86 | 87 | 88 | 89 |

90 | extends(structure) 91 |

indicates that a trait or class extends one or more bases

92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 108 | 109 |
base:IdentifierList 
110 |
111 | 112 | 113 | 114 |

115 | I8(byte) 116 |

signed byte

117 | 118 |
119 |

Traits 120 |

121 | 122 | 123 | 124 |
synonym[object]
125 |

126 | 127 | 128 |

129 | I16(short) 130 |

signed 16-bit int

131 | 132 |
133 |

Traits 134 |

135 | 136 | 137 | 138 |
synonym[object]
139 |

140 | 141 | 142 |

143 | I32(integer) 144 |

signed 32-bit int

145 | 146 |
147 |

Traits 148 |

149 | 150 | 151 | 152 |
synonym[object]
153 |

154 | 155 | 156 |

157 | I64(long) 158 |

signed 64-bit int

159 | 160 |
161 |

Traits 162 |

163 | 164 | 165 | 166 |
synonym[object]
167 |

168 | 169 | 170 |

171 | IdentifierList(list) 172 |

list of identifiers

173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 |
member:String 
190 |
191 | 192 | 193 | 194 |

195 | nonEmptyString(string) 196 |

A non-empty string (minimum length 1)

197 |
198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 |
208 |
209 | 210 |
211 |

Traits 212 |

213 | 214 | 215 | 216 |
length[object]
217 |

218 | 219 | 220 |

221 | serialization(structure) 222 |

Overrides for serializer & deserializer

223 |
224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 239 | 240 |
name:String 
241 |
242 | 243 | 244 | 245 |

246 | synonym(structure) 247 |

This trait doesn't have any functional impact on codegen. It is simply 248 | to document that the defined type is a synonym, and to silence 249 | the default validator that prints a notice for synonyms with no traits.

250 |
251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 |
261 |
262 | 263 | 264 | 265 |

266 | U8(byte) 267 |

unsigned byte

268 | 269 |
270 |

Traits 271 |

272 | 273 | 274 | 275 |
unsignedInt[object]
276 |

277 | 278 | 279 |

280 | U16(short) 281 |

unsigned 16-bit int

282 | 283 |
284 |

Traits 285 |

286 | 287 | 288 | 289 |
unsignedInt[object]
290 |

291 | 292 | 293 |

294 | U32(integer) 295 |

unsigned 32-bit int

296 | 297 |
298 |

Traits 299 |

300 | 301 | 302 | 303 |
unsignedInt[object]
304 |

305 | 306 | 307 |

308 | U64(long) 309 |

unsigned 64-bit int

310 | 311 |
312 |

Traits 313 |

314 | 315 | 316 | 317 |
unsignedInt[object]
318 |

319 | 320 | 321 |

322 | unsignedInt(structure) 323 |

The unsignedInt trait indicates that one of the number types is unsigned

324 |
325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 |
335 |
336 | 337 |
338 |

Traits 339 |

340 | 341 | 342 | 343 |
range[object]
344 |

345 | 346 | 347 |

348 | wasmbus(structure) 349 |

a protocol defines the semantics 350 | of how a client and server communicate.

351 |
352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 367 | 368 | 369 | 370 | 371 | 374 | 375 | 376 | 377 | 378 | 381 | 382 |
actorReceive:Boolean 
contractId:CapabilityContractId 
providerReceive:Boolean 
383 |
384 | 385 |
386 |

Traits 387 |

388 | 389 | 390 | 391 |
protocolDefinition[object]
392 |

393 | 394 | 395 |

396 | wasmbusData(structure) 397 |

data sent via wasmbus 398 | This trait is required for all messages sent via wasmbus

399 |
400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 |
410 |
411 | 412 |
413 |
414 | 415 | -------------------------------------------------------------------------------- /interface/interface.mk: -------------------------------------------------------------------------------- 1 | # interface.mak 2 | # 3 | # common rules for building smithy models 4 | # Some of these may depend on GNUMakefile >= 4.0 5 | # 6 | 7 | html_target ?= html 8 | project_dir ?= $(abspath $(shell pwd)) 9 | codegen_config ?= $(project_dir)/codegen.toml 10 | top_targets ?= all build clean lint validate test 11 | WASH ?= wash 12 | 13 | platform_id = $$( uname -s ) 14 | platform = $$( \ 15 | case $(platform_id) in \ 16 | ( Linux | Darwin | FreeBSD ) echo $(platform_id) ;; \ 17 | ( * ) echo Unrecognized Platform;; \ 18 | esac ) 19 | 20 | 21 | # traverse subdirs 22 | .ONESHELL: 23 | ifneq ($(subdirs),) 24 | $(top_targets):: 25 | for dir in $(subdirs); do \ 26 | $(MAKE) -C $$dir $@; \ 27 | done 28 | endif 29 | 30 | all:: 31 | 32 | 33 | clean:: 34 | rm -rf $(html_target)/*.html 35 | 36 | ifneq ($(wildcard $(codegen_config)),) 37 | # Run smithy model lint or validation checks 38 | lint validate:: 39 | $(WASH) $@ --config $(codegen_config) 40 | endif 41 | 42 | ifeq ($(wildcard rust),rust) 43 | # some rules for building rust subdirs 44 | all:: 45 | cd rust && cargo build 46 | test clean clippy:: 47 | cd rust && cargo $@ 48 | endif 49 | 50 | 51 | # for debugging - show variables make is using 52 | make-vars: 53 | @echo "WASH: : $(WASH)" 54 | @echo "codegen_config : $(codegen_config)" 55 | @echo "platform_id : $(platform_id)" 56 | @echo "platform : $(platform)" 57 | @echo "project_dir : $(project_dir)" 58 | @echo "subdirs : $(subdirs)" 59 | @echo "top_targets : $(top_targets)" 60 | 61 | 62 | .PHONY: all build release clean lint validate test 63 | -------------------------------------------------------------------------------- /interface/kubernetes_applier.smithy: -------------------------------------------------------------------------------- 1 | // kubernetes_applier.smithy 2 | // An interface that allows you to send Kubernetes manifests to a Kubernetes API. Basically the 3 | // equivalent of `kubectl apply -f` 4 | 5 | // Tell the code generator how to reference symbols defined in this namespace 6 | metadata package = [ { namespace: "com.cosmonic.kubernetesapplier", crate: "kubernetes_applier_interface" } ] 7 | 8 | namespace com.cosmonic.kubernetesapplier 9 | 10 | use org.wasmcloud.model#wasmbus 11 | use org.wasmcloud.model#U32 12 | use org.wasmcloud.model#U64 13 | 14 | /// The KubernetesApplier service has a two methods, one to apply an object (that can be a create or 15 | /// update) and to delete an object 16 | @wasmbus( 17 | contractId: "cosmonic:kubernetes_applier", 18 | providerReceive: true ) 19 | service KubernetesApplier { 20 | version: "0.1", 21 | operations: [ Apply, Delete ] 22 | } 23 | 24 | /// Attempts to create or update the arbitrary object it is given 25 | operation Apply { 26 | input: Blob, 27 | output: OperationResponse 28 | } 29 | 30 | /// Attempts to delete an object with the given GVK (group, version, kind), name, and namespace. 31 | /// This should be idempotent, meaning that it should return successful if the object doesn't exist 32 | operation Delete { 33 | input: DeleteRequest, 34 | output: OperationResponse 35 | } 36 | 37 | structure OperationResponse { 38 | /// Whether or not the operation succeeded 39 | @required 40 | succeeded: Boolean, 41 | /// An optional message describing the error if one occurred 42 | error: String, 43 | } 44 | 45 | structure DeleteRequest { 46 | /// The group of the object you are deleting (e.g. "networking.k8s.io"). This will be an empty 47 | /// string if part of `core` 48 | @required 49 | group: String, 50 | 51 | /// The API version of the object you are deleting (e.g. v1) 52 | @required 53 | version: String, 54 | 55 | /// The kind of the object you are deleting (e.g. Pod) 56 | @required 57 | kind: String, 58 | 59 | /// The name of the object you are deleting 60 | @required 61 | name: String, 62 | 63 | /// The namespace where the object you want to delete is located. If not specified, the default 64 | /// namespace for the context should be used 65 | namespace: String, 66 | } 67 | -------------------------------------------------------------------------------- /interface/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kubernetes-applier-interface" 3 | version = "0.3.0" 4 | description = "Interface library for the kubernetes-applier-interface kubernetes-applier capability, " 5 | authors = ["Cosmonic Inc"] 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | 9 | # when publishing to crates.io, freeze src by omitting build.rs 10 | exclude = [ "build.rs" ] 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | default = [] 17 | 18 | [dependencies] 19 | async-trait = "0.1" 20 | futures = "0.3" 21 | serde = { version = "1.0" , features = ["derive"] } 22 | serde_json = "1.0" 23 | serde_bytes = "0.11" 24 | wasmbus-rpc = "0.9" 25 | 26 | [dev-dependencies] 27 | base64 = "0.13" 28 | 29 | # build-dependencies needed for build.rs 30 | [build-dependencies] 31 | weld-codegen = "0.4" 32 | 33 | -------------------------------------------------------------------------------- /interface/rust/build.rs: -------------------------------------------------------------------------------- 1 | // build.rs - build smithy models into rust sources at compile tile 2 | 3 | // path to codegen.toml relative to location of Cargo.toml 4 | const CONFIG: &str = "../codegen.toml"; 5 | 6 | fn main() -> Result<(), Box> { 7 | weld_codegen::rust_build(CONFIG)?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /interface/rust/src/kubernetes_applier.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by wasmcloud/weld-codegen 0.4.6. 2 | // It is not intended for manual editing. 3 | // namespace: com.cosmonic.kubernetesapplier 4 | 5 | #[allow(unused_imports)] 6 | use async_trait::async_trait; 7 | #[allow(unused_imports)] 8 | use serde::{Deserialize, Serialize}; 9 | #[allow(unused_imports)] 10 | use std::{borrow::Borrow, borrow::Cow, io::Write, string::ToString}; 11 | #[allow(unused_imports)] 12 | use wasmbus_rpc::{ 13 | cbor::*, 14 | common::{ 15 | deserialize, message_format, serialize, Context, Message, MessageDispatch, MessageFormat, 16 | SendOpts, Transport, 17 | }, 18 | error::{RpcError, RpcResult}, 19 | Timestamp, 20 | }; 21 | 22 | #[allow(dead_code)] 23 | pub const SMITHY_VERSION: &str = "1.0"; 24 | 25 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 26 | pub struct DeleteRequest { 27 | /// The group of the object you are deleting (e.g. "networking.k8s.io"). This will be an empty 28 | /// string if part of `core` 29 | #[serde(default)] 30 | pub group: String, 31 | /// The kind of the object you are deleting (e.g. Pod) 32 | #[serde(default)] 33 | pub kind: String, 34 | /// The name of the object you are deleting 35 | #[serde(default)] 36 | pub name: String, 37 | /// The namespace where the object you want to delete is located. If not specified, the default 38 | /// namespace for the context should be used 39 | #[serde(default, skip_serializing_if = "Option::is_none")] 40 | pub namespace: Option, 41 | /// The API version of the object you are deleting (e.g. v1) 42 | #[serde(default)] 43 | pub version: String, 44 | } 45 | 46 | // Encode DeleteRequest as CBOR and append to output stream 47 | #[doc(hidden)] 48 | #[allow(unused_mut)] 49 | pub fn encode_delete_request( 50 | mut e: &mut wasmbus_rpc::cbor::Encoder, 51 | val: &DeleteRequest, 52 | ) -> RpcResult<()> 53 | where 54 | ::Error: std::fmt::Display, 55 | { 56 | e.map(5)?; 57 | e.str("group")?; 58 | e.str(&val.group)?; 59 | e.str("kind")?; 60 | e.str(&val.kind)?; 61 | e.str("name")?; 62 | e.str(&val.name)?; 63 | if let Some(val) = val.namespace.as_ref() { 64 | e.str("namespace")?; 65 | e.str(val)?; 66 | } else { 67 | e.null()?; 68 | } 69 | e.str("version")?; 70 | e.str(&val.version)?; 71 | Ok(()) 72 | } 73 | 74 | // Decode DeleteRequest from cbor input stream 75 | #[doc(hidden)] 76 | pub fn decode_delete_request( 77 | d: &mut wasmbus_rpc::cbor::Decoder<'_>, 78 | ) -> Result { 79 | let __result = { 80 | let mut group: Option = None; 81 | let mut kind: Option = None; 82 | let mut name: Option = None; 83 | let mut namespace: Option> = Some(None); 84 | let mut version: Option = None; 85 | 86 | let is_array = match d.datatype()? { 87 | wasmbus_rpc::cbor::Type::Array => true, 88 | wasmbus_rpc::cbor::Type::Map => false, 89 | _ => { 90 | return Err(RpcError::Deser( 91 | "decoding struct DeleteRequest, expected array or map".to_string(), 92 | )) 93 | } 94 | }; 95 | if is_array { 96 | let len = d.fixed_array()?; 97 | for __i in 0..(len as usize) { 98 | match __i { 99 | 0 => group = Some(d.str()?.to_string()), 100 | 1 => kind = Some(d.str()?.to_string()), 101 | 2 => name = Some(d.str()?.to_string()), 102 | 3 => { 103 | namespace = if wasmbus_rpc::cbor::Type::Null == d.datatype()? { 104 | d.skip()?; 105 | Some(None) 106 | } else { 107 | Some(Some(d.str()?.to_string())) 108 | } 109 | } 110 | 4 => version = Some(d.str()?.to_string()), 111 | _ => d.skip()?, 112 | } 113 | } 114 | } else { 115 | let len = d.fixed_map()?; 116 | for __i in 0..(len as usize) { 117 | match d.str()? { 118 | "group" => group = Some(d.str()?.to_string()), 119 | "kind" => kind = Some(d.str()?.to_string()), 120 | "name" => name = Some(d.str()?.to_string()), 121 | "namespace" => { 122 | namespace = if wasmbus_rpc::cbor::Type::Null == d.datatype()? { 123 | d.skip()?; 124 | Some(None) 125 | } else { 126 | Some(Some(d.str()?.to_string())) 127 | } 128 | } 129 | "version" => version = Some(d.str()?.to_string()), 130 | _ => d.skip()?, 131 | } 132 | } 133 | } 134 | DeleteRequest { 135 | group: if let Some(__x) = group { 136 | __x 137 | } else { 138 | return Err(RpcError::Deser( 139 | "missing field DeleteRequest.group (#0)".to_string(), 140 | )); 141 | }, 142 | 143 | kind: if let Some(__x) = kind { 144 | __x 145 | } else { 146 | return Err(RpcError::Deser( 147 | "missing field DeleteRequest.kind (#1)".to_string(), 148 | )); 149 | }, 150 | 151 | name: if let Some(__x) = name { 152 | __x 153 | } else { 154 | return Err(RpcError::Deser( 155 | "missing field DeleteRequest.name (#2)".to_string(), 156 | )); 157 | }, 158 | namespace: namespace.unwrap(), 159 | 160 | version: if let Some(__x) = version { 161 | __x 162 | } else { 163 | return Err(RpcError::Deser( 164 | "missing field DeleteRequest.version (#4)".to_string(), 165 | )); 166 | }, 167 | } 168 | }; 169 | Ok(__result) 170 | } 171 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 172 | pub struct OperationResponse { 173 | /// An optional message describing the error if one occurred 174 | #[serde(default, skip_serializing_if = "Option::is_none")] 175 | pub error: Option, 176 | /// Whether or not the operation succeeded 177 | #[serde(default)] 178 | pub succeeded: bool, 179 | } 180 | 181 | // Encode OperationResponse as CBOR and append to output stream 182 | #[doc(hidden)] 183 | #[allow(unused_mut)] 184 | pub fn encode_operation_response( 185 | mut e: &mut wasmbus_rpc::cbor::Encoder, 186 | val: &OperationResponse, 187 | ) -> RpcResult<()> 188 | where 189 | ::Error: std::fmt::Display, 190 | { 191 | e.map(2)?; 192 | if let Some(val) = val.error.as_ref() { 193 | e.str("error")?; 194 | e.str(val)?; 195 | } else { 196 | e.null()?; 197 | } 198 | e.str("succeeded")?; 199 | e.bool(val.succeeded)?; 200 | Ok(()) 201 | } 202 | 203 | // Decode OperationResponse from cbor input stream 204 | #[doc(hidden)] 205 | pub fn decode_operation_response( 206 | d: &mut wasmbus_rpc::cbor::Decoder<'_>, 207 | ) -> Result { 208 | let __result = { 209 | let mut error: Option> = Some(None); 210 | let mut succeeded: Option = None; 211 | 212 | let is_array = match d.datatype()? { 213 | wasmbus_rpc::cbor::Type::Array => true, 214 | wasmbus_rpc::cbor::Type::Map => false, 215 | _ => { 216 | return Err(RpcError::Deser( 217 | "decoding struct OperationResponse, expected array or map".to_string(), 218 | )) 219 | } 220 | }; 221 | if is_array { 222 | let len = d.fixed_array()?; 223 | for __i in 0..(len as usize) { 224 | match __i { 225 | 0 => { 226 | error = if wasmbus_rpc::cbor::Type::Null == d.datatype()? { 227 | d.skip()?; 228 | Some(None) 229 | } else { 230 | Some(Some(d.str()?.to_string())) 231 | } 232 | } 233 | 1 => succeeded = Some(d.bool()?), 234 | _ => d.skip()?, 235 | } 236 | } 237 | } else { 238 | let len = d.fixed_map()?; 239 | for __i in 0..(len as usize) { 240 | match d.str()? { 241 | "error" => { 242 | error = if wasmbus_rpc::cbor::Type::Null == d.datatype()? { 243 | d.skip()?; 244 | Some(None) 245 | } else { 246 | Some(Some(d.str()?.to_string())) 247 | } 248 | } 249 | "succeeded" => succeeded = Some(d.bool()?), 250 | _ => d.skip()?, 251 | } 252 | } 253 | } 254 | OperationResponse { 255 | error: error.unwrap(), 256 | 257 | succeeded: if let Some(__x) = succeeded { 258 | __x 259 | } else { 260 | return Err(RpcError::Deser( 261 | "missing field OperationResponse.succeeded (#1)".to_string(), 262 | )); 263 | }, 264 | } 265 | }; 266 | Ok(__result) 267 | } 268 | /// The KubernetesApplier service has a two methods, one to apply an object (that can be a create or 269 | /// update) and to delete an object 270 | /// wasmbus.contractId: cosmonic:kubernetes_applier 271 | /// wasmbus.providerReceive 272 | #[async_trait] 273 | pub trait KubernetesApplier { 274 | /// returns the capability contract id for this interface 275 | fn contract_id() -> &'static str { 276 | "cosmonic:kubernetes_applier" 277 | } 278 | /// Attempts to create or update the arbitrary object it is given 279 | async fn apply(&self, ctx: &Context, arg: &Vec) -> RpcResult; 280 | /// Attempts to delete an object with the given GVK (group, version, kind), name, and namespace. 281 | /// This should be idempotent, meaning that it should return successful if the object doesn't exist 282 | async fn delete(&self, ctx: &Context, arg: &DeleteRequest) -> RpcResult; 283 | } 284 | 285 | /// KubernetesApplierReceiver receives messages defined in the KubernetesApplier service trait 286 | /// The KubernetesApplier service has a two methods, one to apply an object (that can be a create or 287 | /// update) and to delete an object 288 | #[doc(hidden)] 289 | #[async_trait] 290 | pub trait KubernetesApplierReceiver: MessageDispatch + KubernetesApplier { 291 | async fn dispatch<'disp__, 'ctx__, 'msg__>( 292 | &'disp__ self, 293 | ctx: &'ctx__ Context, 294 | message: &Message<'msg__>, 295 | ) -> Result, RpcError> { 296 | match message.method { 297 | "Apply" => { 298 | let value: Vec = wasmbus_rpc::common::deserialize(&message.arg) 299 | .map_err(|e| RpcError::Deser(format!("'Blob': {}", e)))?; 300 | 301 | let resp = KubernetesApplier::apply(self, ctx, &value).await?; 302 | let buf = wasmbus_rpc::common::serialize(&resp)?; 303 | 304 | Ok(Message { 305 | method: "KubernetesApplier.Apply", 306 | arg: Cow::Owned(buf), 307 | }) 308 | } 309 | "Delete" => { 310 | let value: DeleteRequest = wasmbus_rpc::common::deserialize(&message.arg) 311 | .map_err(|e| RpcError::Deser(format!("'DeleteRequest': {}", e)))?; 312 | 313 | let resp = KubernetesApplier::delete(self, ctx, &value).await?; 314 | let buf = wasmbus_rpc::common::serialize(&resp)?; 315 | 316 | Ok(Message { 317 | method: "KubernetesApplier.Delete", 318 | arg: Cow::Owned(buf), 319 | }) 320 | } 321 | _ => Err(RpcError::MethodNotHandled(format!( 322 | "KubernetesApplier::{}", 323 | message.method 324 | ))), 325 | } 326 | } 327 | } 328 | 329 | /// KubernetesApplierSender sends messages to a KubernetesApplier service 330 | /// The KubernetesApplier service has a two methods, one to apply an object (that can be a create or 331 | /// update) and to delete an object 332 | /// client for sending KubernetesApplier messages 333 | #[derive(Debug)] 334 | pub struct KubernetesApplierSender { 335 | transport: T, 336 | } 337 | 338 | impl KubernetesApplierSender { 339 | /// Constructs a KubernetesApplierSender with the specified transport 340 | pub fn via(transport: T) -> Self { 341 | Self { transport } 342 | } 343 | 344 | pub fn set_timeout(&self, interval: std::time::Duration) { 345 | self.transport.set_timeout(interval); 346 | } 347 | } 348 | 349 | #[cfg(target_arch = "wasm32")] 350 | impl KubernetesApplierSender { 351 | /// Constructs a client for sending to a KubernetesApplier provider 352 | /// implementing the 'cosmonic:kubernetes_applier' capability contract, with the "default" link 353 | pub fn new() -> Self { 354 | let transport = wasmbus_rpc::actor::prelude::WasmHost::to_provider( 355 | "cosmonic:kubernetes_applier", 356 | "default", 357 | ) 358 | .unwrap(); 359 | Self { transport } 360 | } 361 | 362 | /// Constructs a client for sending to a KubernetesApplier provider 363 | /// implementing the 'cosmonic:kubernetes_applier' capability contract, with the specified link name 364 | pub fn new_with_link(link_name: &str) -> wasmbus_rpc::error::RpcResult { 365 | let transport = wasmbus_rpc::actor::prelude::WasmHost::to_provider( 366 | "cosmonic:kubernetes_applier", 367 | link_name, 368 | )?; 369 | Ok(Self { transport }) 370 | } 371 | } 372 | #[async_trait] 373 | impl KubernetesApplier 374 | for KubernetesApplierSender 375 | { 376 | #[allow(unused)] 377 | /// Attempts to create or update the arbitrary object it is given 378 | async fn apply(&self, ctx: &Context, arg: &Vec) -> RpcResult { 379 | let buf = wasmbus_rpc::common::serialize(arg)?; 380 | 381 | let resp = self 382 | .transport 383 | .send( 384 | ctx, 385 | Message { 386 | method: "KubernetesApplier.Apply", 387 | arg: Cow::Borrowed(&buf), 388 | }, 389 | None, 390 | ) 391 | .await?; 392 | 393 | let value: OperationResponse = wasmbus_rpc::common::deserialize(&resp) 394 | .map_err(|e| RpcError::Deser(format!("'{}': OperationResponse", e)))?; 395 | Ok(value) 396 | } 397 | #[allow(unused)] 398 | /// Attempts to delete an object with the given GVK (group, version, kind), name, and namespace. 399 | /// This should be idempotent, meaning that it should return successful if the object doesn't exist 400 | async fn delete(&self, ctx: &Context, arg: &DeleteRequest) -> RpcResult { 401 | let buf = wasmbus_rpc::common::serialize(arg)?; 402 | 403 | let resp = self 404 | .transport 405 | .send( 406 | ctx, 407 | Message { 408 | method: "KubernetesApplier.Delete", 409 | arg: Cow::Borrowed(&buf), 410 | }, 411 | None, 412 | ) 413 | .await?; 414 | 415 | let value: OperationResponse = wasmbus_rpc::common::deserialize(&resp) 416 | .map_err(|e| RpcError::Deser(format!("'{}': OperationResponse", e)))?; 417 | Ok(value) 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /interface/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Rust interface for interacting with a kubernetes applier 2 | 3 | #[allow(clippy::ptr_arg)] 4 | mod kubernetes_applier; 5 | pub use kubernetes_applier::*; 6 | -------------------------------------------------------------------------------- /service-applier/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /service-applier/.gitignore: -------------------------------------------------------------------------------- 1 | # This file lists build byproducts, 2 | # IDE-specific files (unless shared by your team) 3 | 4 | # 5 | 6 | ## Build 7 | /build 8 | /dist/ 9 | /target 10 | **target 11 | 12 | ## File system 13 | .DS_Store 14 | desktop.ini 15 | 16 | ## Editor 17 | *.swp 18 | *.swo 19 | Session.vim 20 | .cproject 21 | .idea 22 | *.iml 23 | .vscode 24 | .project 25 | .favorites.json 26 | .settings/ 27 | 28 | ## Temporary files 29 | *~ 30 | \#* 31 | \#*\# 32 | .#* 33 | 34 | ## Python 35 | __pycache__/ 36 | *.py[cod] 37 | *$py.class 38 | 39 | ## Node 40 | **node_modules 41 | **package-lock.json 42 | 43 | -------------------------------------------------------------------------------- /service-applier/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-applier" 3 | version = "0.3.0" 4 | authors = ["Cosmonic Inc"] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | name = "service_applier" 10 | 11 | [dependencies] 12 | wasmcloud-interface-messaging = "0.6" 13 | kubernetes-applier-interface = { version = "0.3", path = "../interface/rust" } 14 | wasmbus-rpc = "0.9" 15 | k8s-openapi = { version = "0.15", default-features = false, features = ["v1_22"] } 16 | serde_yaml = "0.8" 17 | wasmcloud-interface-logging = "0.6" 18 | serde_json = "1" 19 | futures = "0.3" 20 | serde = "1" 21 | 22 | [profile.release] 23 | # Optimize for small code size 24 | lto = true 25 | opt-level = "s" 26 | -------------------------------------------------------------------------------- /service-applier/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for service-applier 2 | 3 | PROJECT = service_applier 4 | VERSION = $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[] .version' | head -1) 5 | REVISION = 0 6 | # list of all contract claims for actor signing (space-separated) 7 | CLAIMS = wasmcloud:messaging cosmonic:kubernetes_applier wasmcloud:builtin:logging 8 | # registry url for our actor 9 | REG_URL = localhost:5000/v2/$(PROJECT):$(VERSION) 10 | # command to upload to registry (without last wasm parameter) 11 | PUSH_REG_CMD = wash reg push --insecure $(REG_URL) 12 | 13 | # friendly name for the actor 14 | ACTOR_NAME = "service-applier" 15 | # optional call alias for actor 16 | # ACTOR_ALIAS=nickname 17 | 18 | include ./actor.mk 19 | 20 | test:: 21 | cargo clippy --all-features --all-targets 22 | -------------------------------------------------------------------------------- /service-applier/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Service Applier Actor 2 | 3 | This actor is for use in Kubernetes <-> wasmCloud compatibility. It works by listening on the 4 | wasmCloud event messaging topic for new linkdefs between actors and `wasmcloud:httpserver` 5 | contracts. When one of these appear a service will be created to point at the proper port exposed on 6 | the httpserver. When the link is deleted, the service will be removed. 7 | 8 | This is also meant to be used as a template for an actor when using `wash new actor` for those who 9 | wish to customize what resources are created on link definitions. 10 | 11 | ## How to use 12 | 13 | In order to use this actor, you'll need the NATS `wasmcloud:messaging` provider and the [Kubernetes 14 | Applier Provider](../applier): 15 | 16 | ```console 17 | $ wash ctl start provider wasmcloud.azurecr.io/applier:0.3.0 18 | $ wash ctl start provider wasmcloud.azurecr.io/nats_messaging:0.14.2 19 | ``` 20 | 21 | Once you have started the providers, you can start the actor: 22 | 23 | ```console 24 | $ wash ctl start actor wasmcloud.azurecr.io/service_applier:0.3.0 25 | ``` 26 | 27 | Then you'll need to link the providers to the actor. For instructions on how you can configure the 28 | applier provider, see [its README](../applier/README.md). For the NATS provider, you will need to 29 | connect to the same NATS cluster that your wasmcloud hosts are connected to. This can be specified 30 | with the `URI` parameter (e.g. `URI=nats://localhost:4222`). You'll also need to set which 31 | subscription to listen to: `SUBSCRIPTION=wasmbus.evt.`, where lattice prefix is the 32 | same prefix you specified for your hosts, by default, this is `default` (so your configuration would 33 | be `SUBSCRIPTION=wasmbus.evt.default`); 34 | 35 | NOTE: All `Service`s will be created will be in the default namespace of the kubeconfig you use for 36 | the link definition between this actor and the applier provider. However, this is often desired 37 | behavior as you can run this actor on a host inside of Kubernetes, which means you can use service 38 | account credentials. By default, these credentials are scoped to the namespace where the hosts are 39 | running, which is where the `Service` should be at anyway 40 | 41 | ### Requirements for Hosts running in Kubernetes 42 | 43 | If you'd like your existing applications running in Kubernetes to be able to connect to applications 44 | running in wasmCloud, we recommend creating a "routing tier" of wasmCloud hosts. This means you will 45 | have one `Deployment` of pods running wasmCloud hosts that are just for running actors and other 46 | providers. You will then have a second `Deployment` of pods running wasmCloud hosts that all have 47 | the HTTP server provider running on them. Each of these pods should have the label and value 48 | `wasmcloud.dev/route-to=true` on them in order to have traffic routed to them. Essentially, the 49 | `Service`s created by this actor direct traffic to those HTTP servers, all of which will have the 50 | port you configured in your link definition available. Once the traffic has hit those HTTP servers, 51 | it will be transmitted to actors running in the lattice, whether those are running inside or outside 52 | of Kubernetes. A simple diagram is below: 53 | 54 | ``` 55 | ┌──────────────────────────────────┐ ┌─────────┐ 56 | │ Kubernetes │ │ │ Other 57 | │ │ │ │ ┌────┐ 58 | │ ┌────────┐ │ │ │ │ │ 59 | │ │Service │ │ │ ├─► │ 60 | │ │ │ │ │ │ └────┘ 61 | │ ┌──────┴┬───────┼───────┐ │ │ │ 62 | │ │ │ │ │ │ │ │ ┌────┐ 63 | │ ┌──▼─┐ ┌──▼─┐ ┌──▼─┐ ┌──▼─┐ │ │ │ │ │ 64 | │ │ │ │ │ │ │ │ │ │ │ ├─► │ 65 | │ │ │ │ │ │ │ │ │ │ │ │ └────┘ 66 | │ └─┬──┘ └────┘ └────┤ └──┬─┘ │ │ │ 67 | │ │ Router Hosts │ │ │ │ │ ┌────┐ 68 | │ │ │ └─────┴────┼─► Lattice │ │ │ 69 | │ │ │ │ │ ├─► │ 70 | │ └──────►└─────────────────────► │ │ └────┘ 71 | │ │ │ │ 72 | │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ ┌────┐ 73 | │ │ │ │ │ │ │ │ │ │ │ │ │ │ 74 | │ │ │ │ │ │ │ │ ◄──┼─┤ ├─► │ 75 | │ └────┘ └────┘ └────┘ └────┘ │ │ │ └────┘ 76 | │ Normal Hosts │ │ │ 77 | │ │ │ │ ┌────┐ 78 | │ │ │ │ │ │ 79 | │ │ │ ├─► │ 80 | │ │ │ │ └────┘ 81 | └──────────────────────────────────┘ └─────────┘ Hosts 82 | ``` 83 | 84 | ## See it in action 85 | 86 | The easiest way to see this in action is to start the httpserver provider 87 | (wasmcloud.azurecr.io/httpserver:0.14.6) and the echo actor (wasmcloud.azurecr.io/echo:0.3.2) and 88 | then link them (you can do this in washboard as well if you prefer a GUI): 89 | 90 | ```console 91 | $ wash ctl link put MBCFOPM6JW2APJLXJD3Z5O4CN7CPYJ2B4FTKLJUR5YR5MITIU7HD3WD5 VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M wasmcloud:httpserver 'address=0.0.0.0:8081' 92 | ⡃⠀ Defining link between MBCFOPM6JW2APJLXJD3Z5O4CN7CPYJ2B4FTKLJUR5YR5MITIU7HD3WD5 and VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M ... 93 | Published link (MBCFOPM6JW2APJLXJD3Z5O4CN7CPYJ2B4FTKLJUR5YR5MITIU7HD3WD5) <-> (VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M) successfully 94 | ``` 95 | 96 | Then you can see that the service was created by running: 97 | 98 | ```console 99 | $ kubectl get svc 100 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 101 | kubernetes ClusterIP 10.96.0.1 443/TCP 4d1h 102 | mbcfopm6jw2apjlxjd3z5o4cn7cpyj2b4ftkljur5yr5mitiu7hd3wd5 ClusterIP 10.96.170.75 8081/TCP 10s 103 | ``` 104 | 105 | The service name is the lowercased actor ID, so you can easily identify which actor it is pointing 106 | at. 107 | -------------------------------------------------------------------------------- /service-applier/actor.mk: -------------------------------------------------------------------------------- 1 | # common makefile rules for building actors 2 | # 3 | # Before including this, your project Makefile should define the following: 4 | # 5 | # Required 6 | # ----------- 7 | # PROJECT - Short name for the project, must be valid filename chars, no spaces 8 | # CLAIMS - Space-separtaed list of capability contracts to use for signing 9 | # These should match the capability providers the actor needs to use. 10 | # For example: 11 | # wasmcloud:httpserver wasmcloud:builtin:logging 12 | # VERSION - The actor version number, usually semver format, X.Y.Z 13 | # REVISION - A number that should be incremented with every build, 14 | # whether or not VERSION has changed 15 | # REG_URL - Registry url, e.g. 'localhost:5000' or 'wasmcloud.azurecr.io' 16 | # PUSH_REG_CMD - Command to push to registry, for example: 17 | # wash reg push --insecure $(REG_URL) 18 | # 19 | # 20 | # Optional 21 | # ----------- 22 | # KEYDIR - path to private key folder 23 | # CARGO - cargo binary (name or path), defaults to cargo 24 | # WASH - wash binary (name or path), defaults to wash 25 | # DIST_WASM - the final file after building and signing 26 | # TARGET_DIR - location of cargo build target folder if not in current dir 27 | # (if it's in a workspace, it may be elsewhere) 28 | # WASM_TARGET - type of wasm file, defaults to wasm32-unknown-unknown 29 | # 30 | 31 | KEYDIR ?= .keys 32 | CARGO ?= cargo 33 | WASH ?= wash 34 | # location of cargo output files 35 | TARGET_DIR ?= target 36 | # location of wasm file after build and signing 37 | DIST_WASM ?= build/$(PROJECT)_s.wasm 38 | WASM_TARGET ?= wasm32-unknown-unknown 39 | ACTOR_NAME ?= $(PROJECT) 40 | UNSIGNED_WASM = $(TARGET_DIR)/$(WASM_TARGET)/release/$(PROJECT).wasm 41 | 42 | # verify all required variables are set 43 | check-var-defined = $(if $(strip $($1)),,$(error Required variable "$1" is not defined)) 44 | 45 | $(call check-var-defined,PROJECT) 46 | $(call check-var-defined,CLAIMS) 47 | $(call check-var-defined,VERSION) 48 | $(call check-var-defined,REVISION) 49 | $(call check-var-defined,REG_URL) 50 | $(call check-var-defined,PUSH_REG_CMD) 51 | 52 | all:: $(DIST_WASM) 53 | 54 | # default target is signed wasm file 55 | # sign it 56 | $(DIST_WASM): $(UNSIGNED_WASM) Makefile 57 | @mkdir -p $(dir $@) 58 | $(WASH) claims sign $< \ 59 | $(foreach claim,$(CLAIMS), -c $(claim) ) \ 60 | --name $(ACTOR_NAME) --ver $(VERSION) --rev $(REVISION) \ 61 | $(if $(ACTOR_ALIAS),--call-alias $(ACTOR_ALIAS)) \ 62 | --destination $@ 63 | 64 | # rules to print file name and path of build target 65 | target-path: 66 | @echo $(DIST_WASM) 67 | target-path-abs: 68 | @echo $(abspath $(DIST_WASM)) 69 | target-file: 70 | @echo $(notdir $(DIST_WASM)) 71 | 72 | # the wasm should be rebuilt if any source files or dependencies change 73 | $(UNSIGNED_WASM): .FORCE 74 | $(CARGO) build --release 75 | 76 | # push signed wasm file to registry 77 | push: $(DIST_WASM) 78 | $(PUSH_REG_CMD) $(DIST_WASM) 79 | 80 | # tell host to start an instance of the actor 81 | start: 82 | $(WASH) ctl start actor $(REG_URL) --timeout-ms 3000 83 | 84 | # NOT WORKING - live actor updates not working yet 85 | # update it (should update revision before doing this) 86 | #update: 87 | # $(PUSH_REG_CMD) $(DIST_WASM) 88 | # $(WASH) ctl update actor \ 89 | # $(shell $(WASH) ctl get hosts -o json | jq -r ".hosts[0].id") \ 90 | # $(shell make --silent actor_id) \ 91 | # $(REG_URL) --timeout-ms 3000 92 | 93 | inventory: 94 | $(WASH) ctl get inventory $(shell $(WASH) ctl get hosts -o json | jq -r ".hosts[0].id") 95 | 96 | ifneq ($(wildcard test-options.json),) 97 | # if this is a test actor, run its start method 98 | # project makefile can set RPC_TEST_TIMEOUT_MS to override default 99 | RPC_TEST_TIMEOUT_MS ?= 2000 100 | test:: 101 | $(WASH) call --test --data test-options.json --rpc-timeout-ms $(RPC_TEST_TIMEOUT_MS) \ 102 | $(shell make --silent actor_id) \ 103 | Start 104 | endif 105 | 106 | # generate release build 107 | release:: 108 | cargo build --release 109 | 110 | # standard rust commands 111 | check clippy doc: 112 | $(CARGO) $@ 113 | 114 | # remove 115 | clean:: 116 | $(CARGO) clean 117 | rm -rf build 118 | 119 | inspect claims: $(DIST_WASM) 120 | $(WASH) claims inspect $(DIST_WASM) 121 | 122 | # need a signed wasm before we can print the id 123 | _actor_id: $(DIST_WASM) 124 | @$(WASH) claims inspect $(DIST_WASM) -o json | jq -r .module 125 | 126 | actor_id: 127 | @echo $(shell make --silent _actor_id 2>/dev/null | tail -1) 128 | 129 | ifeq ($(wildcard codegen.toml),codegen.toml) 130 | # if there are interfaces here, enable lint and validate rules 131 | lint validate:: 132 | $(WASH) $@ 133 | else 134 | lint validate:: 135 | 136 | endif 137 | 138 | .PHONY: actor_id check clean clippy doc release test update .FORCE 139 | -------------------------------------------------------------------------------- /service-applier/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | net::SocketAddr, 4 | }; 5 | 6 | use k8s_openapi::{ 7 | api::core::v1::{Service, ServicePort, ServiceSpec}, 8 | apimachinery::pkg::{apis::meta::v1::ObjectMeta, util::intstr::IntOrString}, 9 | Resource, 10 | }; 11 | use kubernetes_applier_interface::{DeleteRequest, KubernetesApplier, KubernetesApplierSender}; 12 | use wasmbus_rpc::{actor::prelude::*, core::LinkDefinition}; 13 | use wasmcloud_interface_logging::debug; 14 | use wasmcloud_interface_messaging::{MessageSubscriber, MessageSubscriberReceiver, SubMessage}; 15 | 16 | const LINKDEF_SET_EVENT_TYPE: &str = "com.wasmcloud.lattice.linkdef_set"; 17 | const LINKDEF_DELETED_EVENT_TYPE: &str = "com.wasmcloud.lattice.linkdef_deleted"; 18 | const EXPECTED_CONTRACT_ID: &str = "wasmcloud:httpserver"; 19 | 20 | const DATA_KEY: &str = "data"; 21 | const ADDRESS_KEY: &str = "address"; 22 | const PORT_KEY: &str = "port"; 23 | const LABEL_PREFIX: &str = "wasmcloud.dev"; 24 | 25 | #[derive(Debug, Default, Actor, HealthResponder)] 26 | #[services(Actor, MessageSubscriber)] 27 | struct ServiceApplierActor {} 28 | 29 | struct EventWrapper { 30 | raw: serde_json::Value, 31 | } 32 | 33 | // TODO: How do we handle configuring existing links/deleting links that no longer exist? A re-sync event? 34 | 35 | impl EventWrapper { 36 | fn ty(&self) -> RpcResult<&str> { 37 | unwrap_the_thingz(&self.raw, "type") 38 | } 39 | 40 | fn contract_id(&self) -> RpcResult<&str> { 41 | let data = self.raw.get(DATA_KEY).ok_or_else(|| { 42 | RpcError::InvalidParameter(format!("Event does not have key {}", DATA_KEY)) 43 | })?; 44 | unwrap_the_thingz(data, "contract_id") 45 | } 46 | 47 | /// Returns the linkdef values by deserializing them. This allows for lazy 48 | /// deserialization only when the type and contract ID match. This will normalize the value keys 49 | /// to lowercase 50 | fn linkdef(&self) -> RpcResult { 51 | let value = self.raw.get(DATA_KEY).ok_or_else(|| { 52 | RpcError::InvalidParameter(format!("Event does not have key {}", DATA_KEY)) 53 | })?; 54 | 55 | let mut ld: LinkDefinition = 56 | serde_json::from_value(value.to_owned()).map_err(|e| RpcError::Deser(e.to_string()))?; 57 | 58 | ld.values = ld 59 | .values 60 | .into_iter() 61 | .map(|(k, v)| (k.to_lowercase(), v)) 62 | .collect(); 63 | 64 | Ok(ld) 65 | } 66 | } 67 | 68 | fn unwrap_the_thingz<'a>(thing: &'a serde_json::Value, key: &str) -> RpcResult<&'a str> { 69 | thing 70 | .get(key) 71 | .ok_or_else(|| RpcError::InvalidParameter(format!("Event does not have key {}", key)))? 72 | .as_str() 73 | .ok_or_else(|| RpcError::InvalidParameter(format!("Event does not have key {}", key))) 74 | } 75 | 76 | #[async_trait] 77 | impl MessageSubscriber for ServiceApplierActor { 78 | async fn handle_message(&self, ctx: &Context, msg: &SubMessage) -> RpcResult<()> { 79 | let raw: serde_json::Value = serde_json::from_slice(&msg.body) 80 | .map_err(|e| RpcError::Deser(format!("Invalid JSON data in message: {}", e)))?; 81 | let evt = EventWrapper { raw }; 82 | 83 | let event_type = evt.ty()?; 84 | match event_type { 85 | LINKDEF_SET_EVENT_TYPE if evt.contract_id()? == EXPECTED_CONTRACT_ID => { 86 | debug!("Found new link definition for HTTP server"); 87 | handle_apply(ctx, evt.linkdef()?).await 88 | } 89 | LINKDEF_DELETED_EVENT_TYPE if evt.contract_id()? == EXPECTED_CONTRACT_ID => { 90 | debug!("Link definition for HTTP server deleted"); 91 | handle_delete(ctx, evt.linkdef()?).await 92 | } 93 | _ => { 94 | debug!("Skipping non-linkdef event {}", event_type); 95 | Ok(()) 96 | } 97 | } 98 | } 99 | } 100 | 101 | async fn handle_apply(ctx: &Context, ld: LinkDefinition) -> RpcResult<()> { 102 | let sender = KubernetesApplierSender::new(); 103 | let port = get_port(ld.values)?; 104 | let svc_name = ld.actor_id.to_lowercase(); 105 | 106 | let mut labels = BTreeMap::new(); 107 | labels.insert(format!("{}/{}", LABEL_PREFIX, "actor-id"), ld.actor_id); 108 | // We can't put in the full contract ID because it contains `:`, which isn't allowed in k8s 109 | labels.insert( 110 | format!("{}/{}", LABEL_PREFIX, "contract"), 111 | // SAFETY: We can unwrap because the contract ID is something we own and we know it has a `:` 112 | EXPECTED_CONTRACT_ID.rsplit_once(':').unwrap().1.to_owned(), 113 | ); 114 | labels.insert(format!("{}/{}", LABEL_PREFIX, "link-name"), ld.link_name); 115 | labels.insert( 116 | format!("{}/{}", LABEL_PREFIX, "provider-id"), 117 | ld.provider_id, 118 | ); 119 | 120 | let mut selector = BTreeMap::new(); 121 | // Select pods that have a label of `wasmcloud.dev/route-to=true` 122 | selector.insert( 123 | format!("{}/{}", LABEL_PREFIX, "route-to"), 124 | "true".to_string(), 125 | ); 126 | 127 | debug!( 128 | "Applying new kubernetes resource with name {}, listening on port {}, with labels {:?}, and selecting pods with labels matching {:?}", 129 | svc_name, 130 | port, 131 | labels, 132 | selector 133 | ); 134 | 135 | // NOTE: If you have more than one type of contract you are handling, you'll likely want to have 136 | // some sort of data store that maps a unique service name to the full link definition. For 137 | // here, you can only have one linkdef of this type for an actor, so we just use the lowercased 138 | // actor key 139 | let resp = sender 140 | .apply( 141 | ctx, 142 | &serde_yaml::to_vec(&Service { 143 | metadata: ObjectMeta { 144 | name: Some(svc_name), 145 | labels: Some(labels), 146 | ..Default::default() 147 | }, 148 | spec: Some(ServiceSpec { 149 | selector: Some(selector), 150 | ports: Some(vec![ServicePort { 151 | protocol: Some("TCP".to_string()), 152 | port, 153 | target_port: Some(IntOrString::Int(port)), 154 | ..Default::default() 155 | }]), 156 | ..Default::default() 157 | }), 158 | ..Default::default() 159 | }) 160 | .expect("Unable to serialize Service to yaml. This is programmer error"), 161 | ) 162 | .await?; 163 | 164 | if !resp.succeeded { 165 | return Err(RpcError::ActorHandler(format!( 166 | "Unable to apply kubernetes service: {}", 167 | resp.error.unwrap_or_default() 168 | ))); 169 | } 170 | 171 | Ok(()) 172 | } 173 | 174 | async fn handle_delete(ctx: &Context, ld: LinkDefinition) -> RpcResult<()> { 175 | let sender = KubernetesApplierSender::new(); 176 | let svc_name = ld.actor_id.to_lowercase(); 177 | 178 | debug!( 179 | "Deleting Kubernetes service with name {} from related linkdef {}-{}-{}", 180 | svc_name, ld.actor_id, ld.provider_id, ld.link_name 181 | ); 182 | 183 | let resp = sender 184 | .delete( 185 | ctx, 186 | &DeleteRequest { 187 | group: Service::GROUP.to_owned(), 188 | kind: Service::KIND.to_owned(), 189 | version: Service::VERSION.to_owned(), 190 | name: svc_name, 191 | namespace: None, 192 | }, 193 | ) 194 | .await?; 195 | 196 | if !resp.succeeded { 197 | return Err(RpcError::ActorHandler(format!( 198 | "Unable to delete kubernetes service: {}", 199 | resp.error.unwrap_or_default() 200 | ))); 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | fn get_port(values: HashMap) -> RpcResult { 207 | let port = if let Some(p) = values.get(PORT_KEY) { 208 | p.parse() 209 | .map_err(|_| RpcError::InvalidParameter("Port value is malformed".to_string()))? 210 | } else if let Some(p) = values.get(ADDRESS_KEY) { 211 | let addr: SocketAddr = p 212 | .parse() 213 | .map_err(|_| RpcError::InvalidParameter("Address value is malformed".to_string()))?; 214 | addr.port() as i32 215 | } else { 216 | // The default port from the HTTP server is 8080, so we are going to default it here as well 217 | 8080 218 | }; 219 | 220 | Ok(port) 221 | } 222 | --------------------------------------------------------------------------------