├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── env │ ├── .env │ ├── README.md │ └── functions.rs ├── event │ ├── README.md │ └── functions.rs ├── full │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── functions.rs │ │ └── lib.rs ├── json │ ├── README.md │ └── functions.rs └── surf │ ├── README.md │ └── functions.rs ├── src ├── main.rs └── template │ ├── Dev.Dockerfile │ ├── Libtest.Dockerfile │ ├── Release.Dockerfile │ └── function.rs └── tests ├── gen-default.rs ├── gen-params.rs ├── init-build.rs └── init-release.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | **/Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | 17 | /bak -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | services: 4 | - docker 5 | 6 | before_install: 7 | - rustup component add rustfmt 8 | script: 9 | - echo "$DOCKER_PASSWORD" | docker login quay.io -u "$DOCKER_USERNAME" --password-stdin && cargo fmt --all -- --check && cargo test 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # roche is an [OPEN Open Source Project](http://openopensource.org/) 2 | 3 | ----------------------------------------- 4 | 5 | ## What? 6 | 7 | Individuals making significant and valuable contributions are given 8 | commit-access to the project to contribute as they see fit. This project 9 | is more like an open wiki than a standard guarded open source project. 10 | 11 | ## Rules 12 | 13 | There are a few basic ground-rules for contributors: 14 | 15 | 1. **No `--force` pushes** or modifying the Git history in any way. 16 | 1. **Non-master branches** ought to be used for ongoing work. 17 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 18 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 19 | 1. Contributors should attempt to adhere to the prevailing code-style. 20 | 21 | ## Releases 22 | 23 | Declaring formal releases remains the prerogative of the project maintainer. 24 | 25 | ## Changes to this arrangement 26 | 27 | This is an experiment and feedback is welcome! This document may also be 28 | subject to pull-requests or changes by contributors where you believe 29 | you have something valuable to add or change. 30 | 31 | Get a copy of this manifesto as [markdown](https://raw.githubusercontent.com/openopensource/openopensource.github.io/master/Readme.md) and use it in your own projects. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roche" 3 | version = "0.3.3" 4 | authors = ["Anthony Whalley "] 5 | edition = "2018" 6 | description = "A cli to build serverless rust applications" 7 | keywords = ["cargo", "docker", "caching", "dependencies"] 8 | categories = ["command-line-utilities"] 9 | repository = "https://github.com/roche-rs/roche" 10 | documentation = "https://docs.rs/roche" 11 | license = "Apache-2.0 OR MIT" 12 | exclude = ["tests"] 13 | 14 | [dependencies] 15 | clap = "3.0.0-beta.2" 16 | cargo-generate = "0.6.1" 17 | anyhow = "1.0" 18 | dotenv = "0.15.0" 19 | 20 | [dev-dependencies] 21 | remove_dir_all = "0.7.0" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roche 2 | A cli for rapidly developing [tide](https://github.com/http-rs/tide) in containers. 3 | 4 |
5 |

6 | 7 | Install 8 | 9 | | 10 | 11 | Get Started 12 | 13 | | 14 | 15 | CLI Docs 16 | 17 | | 18 | 19 | Contributing 20 | 21 | | 22 | 23 | Chat 24 | 25 |

26 |
27 | 28 |
29 | 30 | 31 | Crates.io version 33 | 34 | 35 | 36 | Download 38 | 39 | 40 | 41 | docs.rs docs 43 | 44 | 45 | 46 | Build Status 48 | 49 |
50 | 51 | ## introduction 52 | 53 | Services built with Rust have some fantastic runtime qualities for serverless applications: 54 | 55 | * low resource footprint 56 | 57 | * quick startup time 58 | 59 | * zero garbage collection 60 | 61 | However these things come with a trade off as build times are not ideal for rapid application development. 62 | 63 | roche addresses this short coming by providing a function as a service pattern for [tide](https://github.com/http-rs/tide) that reduces build times to seconds and enables developers to focus on business logic allowing for the speedy delivery of blazing fast and energy efficient software. 64 | 65 | It leverages the [nesting feature of tide](https://github.com/http-rs/tide/blob/main/examples/nested.rs) so all that is required to be developed is a handler while the application infrastructure is provided by prebuilt docker containers. 66 | 67 | Once the base images are downloaded build times are around 5s for debug and 30s for Release. 68 | 69 | Roche is intended to target [knative environments](https://knative.dev/docs/knative-offerings/) but it can also be used to build standard docker containers. 70 | 71 | See the [Architecture](https://github.com/roche-rs/roche/wiki/Architecture) page for details. 72 | 73 | 74 | 75 | ## pre-reqs 76 | 77 | 1. A [docker](https://docs.docker.com/get-docker/) or [podman](https://podman.io/getting-started/installation) environment on your local machine. 78 | 79 | 1. A local [rust](https://rustup.rs/) installation. 80 | 81 | ## install 82 | 83 | ``` 84 | $ cargo install roche 85 | ``` 86 | 87 | ## usage 88 | 89 | 1. make an empty folder and generate a function template 90 | ``` 91 | $ mkdir tide-faas 92 | $ cd tide-faas 93 | $ roche init 94 | ``` 95 | This creates a single function file that you can add functionality into. 96 | 97 | ```rust 98 | pub fn handler() -> tide::Server<()> { 99 | let mut api = tide::new(); 100 | api.at("/").get(|_| async { Ok("Hello, World!") }); 101 | api 102 | } 103 | ``` 104 | That's all you need! 105 | Support for external libs will be added in the future probably through custome base images. 106 | 107 | 108 | 2. Build the function image. 109 | ``` 110 | $ docker login 111 | $ roche build 112 | # optionally you can provide an image name 113 | $ roche build -t registry/namespace/devimagename:version 114 | ``` 115 | 116 | 3. If you would like to run the image use the standard docker commands 117 | ``` 118 | docker run -p 8080:8080 registry/namespace/devimagename:version 119 | ``` 120 | 121 | 4. For a release build run the following - These take slightly longer as they are compiled with the --release flag 122 | ``` 123 | $ roche release registry/namespace/imagename:version 124 | ``` 125 | 126 | 5. Deploy to your favourite container based FaaS platform. 127 | ``` 128 | $ docker push registry/namespace/imagename:version 129 | # knative 130 | $ kn service create roche-function --image registry/namespace/imagename:version 131 | # ibmcloud 132 | $ ibmcloud ce app create -n roche-function --image registry/namespace/imagename:version 133 | ``` 134 | 135 | ## notes 136 | 137 | If you would like to run the build process as part of a CI/CD chain then the following command will generate a `Dockerfile` to ship in the same folder as function.rs. 138 | ``` 139 | $ roche gen 140 | ``` 141 | 142 | ## contribution 143 | 144 | roche is an **OPEN Open Source Project**. This means that: 145 | 146 | > Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 147 | 148 | See the [Contribution Guide](CONTRIBUTING.md) for more details. 149 | 150 | ## attributions 151 | 152 |
Icons made by Freepik from www.flaticon.com
153 | 154 | Project generation features rely on the excellent [cargo generator project](https://crates.io/crates/cargo-generate) 155 | 156 | Insperation from [Appsody](https://github.com/appsody) (end of life) -------------------------------------------------------------------------------- /examples/env/.env: -------------------------------------------------------------------------------- 1 | MY_VAR=hello -------------------------------------------------------------------------------- /examples/env/README.md: -------------------------------------------------------------------------------- 1 | # env Example 2 | 3 | This demonstrates using an .env file for local dev builds. 4 | N.B. .env files are NOT supported in the release build as enviroment variables should be configured as part of your deployment. 5 | 6 | ``` 7 | $ roche build 8 | 9 | $ docker run -p 8080:8080 YOUR_USER/dev-env 10 | tide::log Logger started 11 | level Info 12 | Running server on: http://localhost:8080/ 13 | tide::server Server listening on http://0.0.0.0:8080 14 | 15 | $ curl -s http://localhost:8080/ 16 | hello 17 | ``` -------------------------------------------------------------------------------- /examples/env/functions.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | pub fn handler() -> tide::Server<()> { 3 | let mut api = tide::new(); 4 | api.at("/").get(|_| async { 5 | let key = "MY_VAR"; 6 | let envvar = match env::var(key) { 7 | Ok(val) => val, 8 | Err(_e) => "".to_string(), 9 | }; 10 | Ok(format!("{}", envvar)) 11 | }); 12 | api 13 | } 14 | -------------------------------------------------------------------------------- /examples/event/README.md: -------------------------------------------------------------------------------- 1 | # knative event 2 | 3 | [knative events](https://knative.dev/docs/eventing/) provide composable primitives to enable late-binding event sources and event consumers. 4 | 5 | This example is based on the python [helloworld](https://knative.dev/docs/eventing/samples/helloworld/helloworld-python/). 6 | 7 | A more typed approach would be to extend https://github.com/cloudevents/sdk-rust which is work under progress. 8 | 9 | In this directory run the following command: 10 | 11 | ``` 12 | $ roche build 13 | $ docker run -p 8080:8080 YOUR_REGISTRY_ID/dev-event 14 | $ curl -s -d {} http://localhost:8080/ 15 | {"msg":"Hi from rust app!"} 16 | ``` 17 | 18 | To see the response headers use the -v flag. 19 | ``` 20 | $ curl -s -v -d {} http://localhost:8080/ 21 | 22 | * Trying ::1... 23 | * TCP_NODELAY set 24 | * Connected to localhost (::1) port 8080 (#0) 25 | > POST / HTTP/1.1 26 | > Host: localhost:8080 27 | > User-Agent: curl/7.61.1 28 | > Accept: */* 29 | > Content-Length: 2 30 | > Content-Type: application/x-www-form-urlencoded 31 | > 32 | * upload completely sent off: 2 out of 2 bytes 33 | < HTTP/1.1 203 Non Authoritative Information 34 | < ce-id: e670db49-cba1-486f-ad85-50b4a27eb838 35 | < ce-source: knative/eventing/samples/hello-world 36 | < ce-specversion: 0.3 37 | < ce-type: dev.knative.samples.hifromknative 38 | < content-length: 27 39 | < content-type: application/json 40 | < date: Thu, 10 Dec 2020 00:31:36 GMT 41 | < 42 | * Connection #0 to host localhost left intact 43 | ``` 44 | 45 | 46 | No image tag provided. 47 | Going to use: docker.io/number9/dev-full:latest 48 | Sending build context to Docker daemon 3.631kB 49 | Step 1/12 : FROM number9/roche-baseimage-debug as builder 50 | ---> 603e43542443 51 | Step 2/12 : COPY functions.rs /app-build/src/app/ 52 | ---> Using cache 53 | ---> 00d2e882c933 54 | Step 3/12 : RUN cargo build 55 | ---> Using cache 56 | ---> 7770ec40b6f9 57 | Step 4/12 : FROM alpine 58 | ---> d6e46aa2470d 59 | Step 5/12 : RUN apk add --no-cache libgcc 60 | ---> Using cache 61 | ---> 13dd42fef2e5 62 | Step 6/12 : RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser 63 | ---> Using cache 64 | ---> 8e52e3654f2d 65 | Step 7/12 : WORKDIR "/app" 66 | ---> Using cache 67 | ---> c5290577d0f0 68 | Step 8/12 : COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/debug/roche-service ./ 69 | ---> Using cache 70 | ---> a5f9490070c4 71 | Step 9/12 : USER rocheuser 72 | ---> Using cache 73 | ---> f8065a53411e 74 | Step 10/12 : ENV PORT 8080 75 | ---> Using cache 76 | ---> b208b17127a6 77 | Step 11/12 : EXPOSE 8080 78 | ---> Using cache 79 | ---> ea77183716e1 80 | Step 12/12 : CMD ["./run.sh"] 81 | ---> Using cache 82 | ---> 3c16f946f1e4 83 | Successfully built 3c16f946f1e4 84 | Successfully tagged number9/dev-full:latest 85 | Finished test [unoptimized + debuginfo] target(s) in 0.23s 86 | Running target/debug/deps/full-6db207771b810bbf 87 | 88 | running 1 test 89 | tide::log Logger started 90 | level Info 91 | Running server on: http://localhost:8080/ 92 | tide::server Server listening on http://0.0.0.0:8080 93 | tide::log::middleware <-- Request received 94 | method GET 95 | path / 96 | surf::middleware::logger::native sending request 97 | req.id 0 98 | req.method GET 99 | req.uri https://httpbin.org/get 100 | surf::middleware::logger::native request completed 101 | req.id 0 102 | req.status 200 103 | elapsed 410.920775ms 104 | tide::log::middleware --> Response sent 105 | method GET 106 | path / 107 | status 200 - OK 108 | duration 412.615899ms 109 | test tests::it_works ... ok 110 | 111 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 112 | 113 | Doc-tests full 114 | 115 | running 0 tests 116 | 117 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 118 | 119 | a717dfb97c16 120 | ``` -------------------------------------------------------------------------------- /examples/event/functions.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use tide::http::mime; 3 | use tide::prelude::*; // Pulls in the json! macro. 4 | use tide::{Body, Response}; 5 | use uuid::Uuid; 6 | 7 | #[derive(Deserialize, Serialize)] 8 | struct Cat { 9 | name: String, 10 | } 11 | 12 | pub fn handler() -> tide::Server<()> { 13 | let mut api = tide::new(); 14 | api.at("/").post(|_| async { 15 | let msguuid = Uuid::new_v4(); 16 | let json = json!({ 17 | "msg": "Hi from rust app!" 18 | }); 19 | let body = Body::from_json(&json)?; 20 | 21 | let response = Response::builder(200) 22 | .body(body) 23 | .header("Ce-Id", msguuid.to_string()) 24 | .header("Ce-specversion", "0.3") 25 | .header("Ce-Source", "knative/eventing/samples/hello-world") 26 | .header("Ce-Type", "dev.knative.samples.hifromknative") 27 | .content_type(mime::JSON) 28 | .build(); 29 | Ok(response) 30 | }); 31 | api 32 | } 33 | -------------------------------------------------------------------------------- /examples/full/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "full" 3 | version = "0.1.0" 4 | authors = ["Anton Whalley "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | async-std = { version = "1.6.5", features = ["attributes"] } 11 | surf = "2.1.0" 12 | -------------------------------------------------------------------------------- /examples/full/README.md: -------------------------------------------------------------------------------- 1 | # Service with Test Example 2 | 3 | In this directory run the following command: 4 | 5 | ``` 6 | $ roche build-test 7 | 8 | No image tag provided. 9 | Going to use: docker.io/number9/dev-full:latest 10 | Sending build context to Docker daemon 3.631kB 11 | Step 1/12 : FROM number9/roche-baseimage-debug as builder 12 | ---> 603e43542443 13 | Step 2/12 : COPY functions.rs /app-build/src/app/ 14 | ---> Using cache 15 | ---> 00d2e882c933 16 | Step 3/12 : RUN cargo build 17 | ---> Using cache 18 | ---> 7770ec40b6f9 19 | Step 4/12 : FROM alpine 20 | ---> d6e46aa2470d 21 | Step 5/12 : RUN apk add --no-cache libgcc 22 | ---> Using cache 23 | ---> 13dd42fef2e5 24 | Step 6/12 : RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser 25 | ---> Using cache 26 | ---> 8e52e3654f2d 27 | Step 7/12 : WORKDIR "/app" 28 | ---> Using cache 29 | ---> c5290577d0f0 30 | Step 8/12 : COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/debug/roche-service ./ 31 | ---> Using cache 32 | ---> a5f9490070c4 33 | Step 9/12 : USER rocheuser 34 | ---> Using cache 35 | ---> f8065a53411e 36 | Step 10/12 : ENV PORT 8080 37 | ---> Using cache 38 | ---> b208b17127a6 39 | Step 11/12 : EXPOSE 8080 40 | ---> Using cache 41 | ---> ea77183716e1 42 | Step 12/12 : CMD ["./run.sh"] 43 | ---> Using cache 44 | ---> 3c16f946f1e4 45 | Successfully built 3c16f946f1e4 46 | Successfully tagged number9/dev-full:latest 47 | Finished test [unoptimized + debuginfo] target(s) in 0.23s 48 | Running target/debug/deps/full-6db207771b810bbf 49 | 50 | running 1 test 51 | tide::log Logger started 52 | level Info 53 | Running server on: http://localhost:8080/ 54 | tide::server Server listening on http://0.0.0.0:8080 55 | tide::log::middleware <-- Request received 56 | method GET 57 | path / 58 | surf::middleware::logger::native sending request 59 | req.id 0 60 | req.method GET 61 | req.uri https://httpbin.org/get 62 | surf::middleware::logger::native request completed 63 | req.id 0 64 | req.status 200 65 | elapsed 410.920775ms 66 | tide::log::middleware --> Response sent 67 | method GET 68 | path / 69 | status 200 - OK 70 | duration 412.615899ms 71 | test tests::it_works ... ok 72 | 73 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 74 | 75 | Doc-tests full 76 | 77 | running 0 tests 78 | 79 | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 80 | 81 | a717dfb97c16 82 | ``` -------------------------------------------------------------------------------- /examples/full/src/functions.rs: -------------------------------------------------------------------------------- 1 | pub fn handler() -> tide::Server<()> { 2 | let mut api = tide::new(); 3 | api.at("/").get(|_| async { 4 | let uri = "https://httpbin.org/get"; 5 | let string: String = surf::get(uri).recv_string().await?; 6 | Ok(string) 7 | }); 8 | api 9 | } 10 | -------------------------------------------------------------------------------- /examples/full/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[async_std::test] 4 | async fn it_works() { 5 | let uri = "http://localhost:8080/"; 6 | let string: String = match surf::get(uri).recv_string().await { 7 | Ok(result) => result, 8 | Err(_) => "".to_string() 9 | }; 10 | 11 | assert!(string.contains("httpbin.org")); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/json/README.md: -------------------------------------------------------------------------------- 1 | # JSON Example 2 | 3 | ``` 4 | $ roche build 5 | 6 | $ docker run -p 8080:8080 YOUR_USER/dev-json 7 | tide::log Logger started 8 | level Info 9 | Running server on: http://localhost:8080/ 10 | tide::server Server listening on http://0.0.0.0:8080 11 | 12 | $ curl -s http://localhost:8080/animals 13 | {"animals":[{"name":"chashu","type":"cat"},{"name":"nori","type":"cat"}],"meta":{"count":2}} 14 | ``` -------------------------------------------------------------------------------- /examples/json/functions.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use tide::prelude::*; // Pulls in the json! macro. 3 | 4 | 5 | #[derive(Deserialize, Serialize)] 6 | struct Cat { 7 | name: String, 8 | } 9 | 10 | pub fn handler() -> tide::Server<()> { 11 | let mut api = tide::new(); 12 | api.at("/animals").get(|_| async { 13 | Ok(json!({ 14 | "meta": { "count": 2 }, 15 | "animals": [ 16 | { "type": "cat", "name": "chashu" }, 17 | { "type": "cat", "name": "nori" } 18 | ] 19 | })) 20 | }); 21 | api 22 | } -------------------------------------------------------------------------------- /examples/surf/README.md: -------------------------------------------------------------------------------- 1 | # Surf Example 2 | 3 | In this directory run the following commands: 4 | 5 | ``` 6 | $ roche build 7 | 8 | $ docker run -p 8080:8080 YOUR_USER/dev-surf 9 | tide::log Logger started 10 | level Info 11 | Running server on: http://localhost:8080/ 12 | tide::server Server listening on http://0.0.0.0:8080 13 | 14 | $ curl -s http://localhost:8080/ 15 | { 16 | "args": {}, 17 | "headers": { 18 | "Accept": "*/*", 19 | "Accept-Encoding": "deflate, gzip", 20 | "Content-Length": "0", 21 | "Host": "httpbin.org", 22 | "User-Agent": "curl/7.73.0-DEV isahc/0.9.13", 23 | "X-Amzn-Trace-Id": "Root=1-5fcebd0b-6f864ba16f1d102e5798dcc9" 24 | }, 25 | "origin": "78.16.190.221", 26 | "url": "https://httpbin.org/get" 27 | } 28 | ``` -------------------------------------------------------------------------------- /examples/surf/functions.rs: -------------------------------------------------------------------------------- 1 | pub fn handler() -> tide::Server<()> { 2 | let mut api = tide::new(); 3 | api.at("/").get(|_| async { 4 | let uri = "https://httpbin.org/get"; 5 | let string: String = surf::get(uri).recv_string().await?; 6 | Ok(string) 7 | }); 8 | api 9 | } 10 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cargo_generate::{generate, Args}; 3 | use clap::{App, Arg}; 4 | use dotenv; 5 | use std::env; 6 | use std::fs::File; 7 | use std::io::prelude::*; 8 | use std::path::Path; 9 | use std::process; 10 | use std::process::{Command, Stdio}; 11 | 12 | // Public Args look out for this PR landing to fix this 13 | // https://github.com/ashleygwilliams/cargo-generate/pull/264 14 | 15 | #[derive(Debug)] 16 | pub struct PublicArgs { 17 | /// Git repository to clone template from. Can be a URL (like 18 | /// `https://github.com/rust-cli/cli-template`), a path (relative or absolute), or an 19 | /// `owner/repo` abbreviated GitHub URL (like `rust-cli/cli-template`). 20 | /// Note that cargo generate will first attempt to interpret the `owner/repo` form as a 21 | /// relative path and only try a GitHub URL if the local path doesn't exist. 22 | pub git: String, 23 | /// Branch to use when installing from git 24 | pub branch: Option, 25 | /// Directory to create / project name; if the name isn't in kebab-case, it will be converted 26 | /// to kebab-case unless `--force` is given. 27 | pub name: Option, 28 | /// Don't convert the project name to kebab-case before creating the directory. 29 | /// Note that cargo generate won't overwrite an existing directory, even if `--force` is given. 30 | pub force: bool, 31 | /// Enables more verbose output. 32 | pub verbose: bool, 33 | } 34 | 35 | impl Into for PublicArgs { 36 | fn into(self) -> Args { 37 | Args { 38 | git: Some(self.git), 39 | branch: self.branch, 40 | name: self.name, 41 | force: self.force, 42 | verbose: self.verbose, 43 | vcs: cargo_generate::Vcs::Git, 44 | 45 | // cargo_generate::Args doesn't implement Default 46 | list_favorites: false, 47 | favorite: None, 48 | template_values_file: None, 49 | silent: false, 50 | config: None, 51 | } 52 | } 53 | } 54 | 55 | pub fn generateimagetag(buildtype: String) -> Option { 56 | let fullpath = match env::current_dir() { 57 | Err(why) => panic!("Couldn't get current dir {}", why), 58 | Ok(s) => s, 59 | }; 60 | let pieces: Vec<&str> = fullpath 61 | .to_str() 62 | .unwrap() 63 | .split(std::path::MAIN_SEPARATOR) 64 | .collect(); 65 | let mut dir = pieces[pieces.len() - 1]; 66 | if dir == "src" { 67 | dir = pieces[pieces.len() - 2]; 68 | } 69 | 70 | match getlogin() { 71 | Some(l) => { 72 | let img = format!("{}/{}{}", l, buildtype, dir); 73 | Some(img) 74 | } 75 | None => { 76 | let img = format!("{}{}", buildtype, dir); 77 | Some(img) 78 | } 79 | } 80 | } 81 | 82 | pub fn getdockerlogin() -> Option { 83 | match env::var("DOCKER_USERNAME") { 84 | Ok(val) => return Some(val), 85 | Err(_e) => { 86 | println!("DOCKER_USERNAME environment variable not found trying to docker cli") 87 | } 88 | }; 89 | 90 | let process = match Command::new("docker") 91 | .stdin(Stdio::piped()) 92 | .stdout(Stdio::piped()) 93 | .arg("info") 94 | .spawn() 95 | { 96 | Err(why) => { 97 | println!("couldn't spawn docker: {}", why); 98 | process::exit(1); 99 | } 100 | Ok(process) => process, 101 | }; 102 | let mut s = String::new(); 103 | let mut username: String = String::new(); 104 | 105 | match process.stdout.unwrap().read_to_string(&mut s) { 106 | Err(why) => panic!("couldn't read docker stdout: {}", why), 107 | Ok(_) => { 108 | for line in s.lines() { 109 | if line.contains("Username") { 110 | let vusername = line.split_whitespace(); 111 | username = vusername.last().unwrap_or_default().to_string(); 112 | } 113 | } 114 | } 115 | }; 116 | if username.is_empty() { 117 | None 118 | } else { 119 | Some(username) 120 | } 121 | } 122 | 123 | pub fn getlogin() -> Option { 124 | match getdockerlogin() { 125 | Some(s) => Some(s), 126 | None => getpodmanlogin(), 127 | } 128 | } 129 | 130 | pub fn getpodmanlogin() -> Option { 131 | let podman = match Command::new("podman") 132 | .stdin(Stdio::piped()) 133 | .stdout(Stdio::piped()) 134 | .arg("login") 135 | .arg("--get-login") 136 | .spawn() 137 | { 138 | Err(why) => { 139 | println!("No Username found with docker or podman: {}", why); 140 | process::exit(1); 141 | } 142 | Ok(process) => process, 143 | }; 144 | let mut podmanoutput = String::new(); 145 | let username; 146 | match podman.stdout.unwrap().read_to_string(&mut podmanoutput) { 147 | Err(why) => panic!("couldn't read podman stdout: {}", why), 148 | Ok(_) => { 149 | username = podmanoutput.lines().next().unwrap_or_default().to_string(); 150 | } 151 | } 152 | if username.is_empty() { 153 | None 154 | } else { 155 | Some(username) 156 | } 157 | } 158 | fn main() -> Result<()> { 159 | const FUNCTION: &str = include_str!("template/function.rs"); 160 | const RELEASE_BUILD: &str = include_str!("template/Release.Dockerfile"); 161 | const LOCAL_BUILD: &str = include_str!("template/Dev.Dockerfile"); 162 | const TEST_BUILD: &str = include_str!("template/Libtest.Dockerfile"); 163 | 164 | let dirname = env::current_dir().unwrap(); 165 | let env_src_location = format!("{}/src/.rocherc", dirname.display()); 166 | if Path::new(&env_src_location).exists() { 167 | dotenv::from_path(env_src_location).unwrap(); 168 | } 169 | 170 | let env_top_location = format!("{}/.rocherc", dirname.display()); 171 | if Path::new(&env_top_location).exists() { 172 | dotenv::from_path(env_top_location).unwrap(); 173 | } 174 | 175 | let dev_build_image = 176 | env::var("dev_build_image").unwrap_or("quay.io/roche/dev-default:1.4.0".to_string()); 177 | let test_build_image = 178 | env::var("test_build_image").unwrap_or("quay.io/roche/dev-default:1.4.0".to_string()); 179 | let release_build_image = 180 | env::var("release_build_image").unwrap_or("quay.io/roche/default:1.4.0".to_string()); 181 | let runtime_image = 182 | env::var("runtime_image").unwrap_or("quay.io/roche/alpine-libgcc:3.12".to_string()); 183 | let default_project = "https://github.com/roche-rs/default"; 184 | let mongodb_project = "https://github.com/roche-rs/mongodb"; 185 | 186 | let matches = App::new("roche") 187 | .version("0.3.1") 188 | .author("Anton Whalley. ") 189 | .about("A tool for building rust http and event services using containers") 190 | .subcommand( 191 | App::new("init").about("Generates a project").arg( 192 | Arg::new("template") 193 | .about("template name 'default', 'mongodb' or git location. If not supplied a functions.rs file is generated.") 194 | .index(1) 195 | .required(false) 196 | .multiple_values(true) 197 | ) 198 | .arg( 199 | Arg::new("name") 200 | .about("Name of the project") 201 | .required(false) 202 | .takes_value(true) 203 | .short('n') 204 | .long("name"), 205 | ) 206 | .arg( 207 | Arg::new("branch") 208 | .about("Branch to use when installing from git. Defaults to main.") 209 | .required(false) 210 | .takes_value(true) 211 | .short('b') 212 | .long("branch"), 213 | ) 214 | .arg( 215 | Arg::new("force") 216 | .about("Don't convert the project name to kebab-case before creating the directory.") 217 | .required(false) 218 | .takes_value(false) 219 | .short('f') 220 | .long("force"), 221 | ) 222 | .arg( 223 | Arg::new("verbose") 224 | .about("Enables more verbose output.") 225 | .required(false) 226 | .takes_value(true) 227 | .short('v') 228 | .long("verbose"), 229 | ) 230 | , 231 | ).subcommand( 232 | App::new("build").about("Builds a development image").arg( 233 | Arg::new("buildimage") 234 | .about("buildimage to use. If not provided defaults to quay.io/roche/default:1.1.0") 235 | .takes_value(true) 236 | .short('b') 237 | .long("buildimage") 238 | .required(false) 239 | ) 240 | .arg( 241 | Arg::new("runtimeimage") 242 | .about("baseimage to use. If not provided defaults to quay.io/roche/alpine-libgcc:3.12") 243 | .takes_value(true) 244 | .short('r') 245 | .long("runtime") 246 | .required(false) 247 | ) 248 | .arg( 249 | Arg::new("tag") 250 | .about("tag for the build.") 251 | .takes_value(true) 252 | .short('t') 253 | .long("tag") 254 | .required(false) 255 | ) 256 | ) 257 | .subcommand( 258 | App::new("test").about("Runs the lib tests in an image").arg( 259 | Arg::new("libtestimage") 260 | .about("Lib test image to use. If not provided defaults to quay.io/roche/default:1.2.0") 261 | .takes_value(true) 262 | .short('l') 263 | .long("libtestimage") 264 | .required(false) 265 | ) 266 | .arg( 267 | Arg::new("tag") 268 | .about("tag for the test run.") 269 | .takes_value(true) 270 | .short('t') 271 | .long("tag") 272 | .required(false) 273 | ) 274 | ) 275 | .subcommand( 276 | App::new("release").about("Builds a release image").arg( 277 | Arg::new("buildimage") 278 | .about("buildimage to use. If not provided defaults to quay.io/roche/default:1.1.0") 279 | .takes_value(true) 280 | .short('b') 281 | .long("buildimage") 282 | .required(false) 283 | ) 284 | .arg( 285 | Arg::new("runtimeimage") 286 | .about("baseimage to use. If not provided defaults to quay.io/roche/alpine-libgcc:3.12") 287 | .takes_value(true) 288 | .short('r') 289 | .long("runtime") 290 | .required(false) 291 | ) 292 | .arg( 293 | Arg::new("tag") 294 | .about("tag for the build.") 295 | .takes_value(true) 296 | .short('t') 297 | .long("tag") 298 | .required(false) 299 | ) 300 | ).subcommand( 301 | App::new("gen").about("Generates a release Dockerfile") 302 | .arg( 303 | Arg::new("buildimage") 304 | .about("buildimage to use. If not provided defaults to quay.io/roche/default:1.1.0") 305 | .takes_value(true) 306 | .short('b') 307 | .long("buildimage") 308 | .required(false) 309 | ) 310 | .arg( 311 | Arg::new("runtimeimage") 312 | .about("baseimage to use. If not provided defaults to quay.io/roche/alpine-libgcc:3.12") 313 | .takes_value(true) 314 | .short('r') 315 | .long("runtime") 316 | .required(false) 317 | ) 318 | ) 319 | .get_matches(); 320 | 321 | match matches.subcommand_name() { 322 | None => println!("No subcommand was used - try 'roche help'"), 323 | _ => {} 324 | }; 325 | 326 | if matches.is_present("build") { 327 | // Check we have a functions.rs to build. 328 | let dirname = env::current_dir()?; 329 | 330 | let functionlocation = format!("{}/functions.rs", dirname.display()); 331 | if !Path::new(&functionlocation).exists() { 332 | let srcfunction = format!("{}/src/functions.rs", dirname.display()); 333 | if !Path::new(&srcfunction).exists() { 334 | println!( 335 | "Cannot find functions.rs in the current folder or in src subfolder. Exiting" 336 | ); 337 | process::exit(1); 338 | } else { 339 | let srcfolder = format!("{}/src", dirname.display()); 340 | let srcpath = Path::new(&srcfolder); 341 | assert!(env::set_current_dir(&srcpath).is_ok()); 342 | } 343 | } 344 | 345 | if let Some(build_matches) = matches.subcommand_matches("build") { 346 | let mut tag = format!("-t{}", build_matches.value_of("tag").unwrap_or("")); 347 | 348 | //let mut tag = build_matches.value_of("tag").unwrap_or("").to_string(); 349 | 350 | if tag == "-t" { 351 | tag = match generateimagetag("dev-".to_string()) { 352 | Some(s) => { 353 | println!("No tag provided using {}", s); 354 | format!("-t{}", s) 355 | } 356 | None => { 357 | panic!("No tag provided and couldn't generate a tag. Please check you have logged into docker or podman") 358 | } 359 | } 360 | } 361 | 362 | let buildimage = build_matches 363 | .value_of("buildimage") 364 | .unwrap_or(dev_build_image.as_str()); 365 | let runtimeimage = build_matches 366 | .value_of("runtimeimage") 367 | .unwrap_or(runtime_image.as_str()); 368 | let mut tmp_docker_file = str::replace(LOCAL_BUILD, "DEV_BASE_IMAGE", buildimage); 369 | tmp_docker_file = str::replace(tmp_docker_file.as_str(), "RUNTIME_IMAGE", runtimeimage); 370 | if Path::new(".env").exists() { 371 | tmp_docker_file = str::replace( 372 | tmp_docker_file.as_str(), 373 | "INCLUDE_ENV", 374 | "app-build/src/.env*", 375 | ); 376 | } else { 377 | tmp_docker_file = str::replace(tmp_docker_file.as_str(), "INCLUDE_ENV ", ""); 378 | } 379 | let process = match Command::new("docker") 380 | .stdin(Stdio::piped()) 381 | .stdout(Stdio::piped()) 382 | .arg("build") 383 | .arg(&tag) 384 | .arg("-f-") 385 | .arg(".") 386 | .spawn() 387 | { 388 | Err(why) => { 389 | println!("couldn't spawn docker: {}", why); 390 | process::exit(1); 391 | } 392 | Ok(process) => process, 393 | }; 394 | 395 | match process.stdin.unwrap().write_all(tmp_docker_file.as_bytes()) { 396 | Err(why) => panic!("couldn't write to docker stdin: {}", why), 397 | Ok(_) => println!("Roche: Sent file to builder for {}", &tag), 398 | } 399 | let mut s = String::new(); 400 | match process.stdout.unwrap().read_to_string(&mut s) { 401 | Err(why) => panic!("couldn't read docker stdout: {}", why), 402 | Ok(_) => print!("Roche: Build complete for {}\n{}", &tag, s), 403 | } 404 | } 405 | 406 | // 407 | } 408 | if matches.is_present("test") { 409 | // Check we have a functions.rs to test. 410 | let dirname = env::current_dir()?; 411 | 412 | let functionlocation = format!("{}/functions.rs", dirname.display()); 413 | if !Path::new(&functionlocation).exists() { 414 | let srcfunction = format!("{}/src/functions.rs", dirname.display()); 415 | if !Path::new(&srcfunction).exists() { 416 | println!( 417 | "Cannot find functions.rs in the current folder or in src subfolder. Exiting" 418 | ); 419 | process::exit(1); 420 | } else { 421 | let srcfolder = format!("{}/src", dirname.display()); 422 | let srcpath = Path::new(&srcfolder); 423 | assert!(env::set_current_dir(&srcpath).is_ok()); 424 | } 425 | } 426 | let dirname = env::current_dir()?; 427 | let testlocation = format!("{}/lib.rs", dirname.display()); 428 | if !Path::new(&testlocation).exists() { 429 | println!("Cannot find lib.rs in the src folder. Exiting"); 430 | process::exit(1); 431 | } 432 | 433 | if let Some(build_matches) = matches.subcommand_matches("test") { 434 | let mut tag = format!("-t{}", build_matches.value_of("tag").unwrap_or("")); 435 | 436 | //let mut tag = build_matches.value_of("tag").unwrap_or("").to_string(); 437 | 438 | if tag == "-t" { 439 | tag = match generateimagetag("test-".to_string()) { 440 | Some(s) => { 441 | println!("No tag provided using {}", s); 442 | format!("-t{}", s) 443 | } 444 | None => { 445 | panic!("No tag provided and couldn't generate a tag. Please check you have logged into docker or podman") 446 | } 447 | } 448 | } 449 | 450 | let testimage = build_matches 451 | .value_of("libtestimage") 452 | .unwrap_or(test_build_image.as_str()); 453 | let mut tmp_docker_file = str::replace(TEST_BUILD, "TEST_BASE_IMAGE", testimage); 454 | if Path::new(".env").exists() { 455 | tmp_docker_file = str::replace( 456 | tmp_docker_file.as_str(), 457 | "INCLUDE_ENV", 458 | "app-build/src/.env*", 459 | ); 460 | } else { 461 | tmp_docker_file = str::replace(tmp_docker_file.as_str(), "INCLUDE_ENV ", ""); 462 | } 463 | let process = match Command::new("docker") 464 | .stdin(Stdio::piped()) 465 | .stdout(Stdio::piped()) 466 | .arg("build") 467 | .arg(&tag) 468 | .arg("-f-") 469 | .arg(".") 470 | .spawn() 471 | { 472 | Err(why) => { 473 | println!("couldn't spawn docker: {}", why); 474 | process::exit(1); 475 | } 476 | Ok(process) => process, 477 | }; 478 | 479 | match process.stdin.unwrap().write_all(tmp_docker_file.as_bytes()) { 480 | Err(why) => panic!("couldn't write to docker stdin: {}", why), 481 | Ok(_) => println!("Roche: Sent file to builder for {}", &tag), 482 | } 483 | let mut s = String::new(); 484 | match process.stdout.unwrap().read_to_string(&mut s) { 485 | Err(why) => panic!("couldn't read docker stdout: {}", why), 486 | Ok(_) => print!("Roche: Build complete for {}\n{}", &tag, s), 487 | } 488 | } 489 | } 490 | 491 | if matches.is_present("release") { 492 | // Check we have a functions.rs to build. 493 | println!("in release"); 494 | let dirname = env::current_dir()?; 495 | 496 | let functionlocation = format!("{}/functions.rs", dirname.display()); 497 | if !Path::new(&functionlocation).exists() { 498 | let srcfunction = format!("{}/src/functions.rs", dirname.display()); 499 | if !Path::new(&srcfunction).exists() { 500 | println!( 501 | "Cannot find functions.rs in the current folder or in src subfolder. Exiting" 502 | ); 503 | process::exit(1); 504 | } else { 505 | let srcfolder = format!("{}/src", dirname.display()); 506 | let srcpath = Path::new(&srcfolder); 507 | assert!(env::set_current_dir(&srcpath).is_ok()); 508 | } 509 | } 510 | 511 | if let Some(build_matches) = matches.subcommand_matches("release") { 512 | let mut tag = format!("-t{}", build_matches.value_of("tag").unwrap_or("")); 513 | 514 | //let mut tag = build_matches.value_of("tag").unwrap_or("").to_string(); 515 | 516 | if tag == "-t" { 517 | tag = match generateimagetag("".to_string()) { 518 | Some(s) => { 519 | println!("No tag provided using {}", s); 520 | format!("-t{}", s) 521 | } 522 | None => { 523 | panic!("No tag provided and couldn't generate a tag. Please check you have logged into docker or podman") 524 | } 525 | } 526 | } 527 | 528 | let buildimage = build_matches 529 | .value_of("buildimage") 530 | .unwrap_or(release_build_image.as_str()); 531 | let runtimeimage = build_matches 532 | .value_of("runtimeimage") 533 | .unwrap_or(runtime_image.as_str()); 534 | 535 | let mut tmp_docker_file = str::replace(RELEASE_BUILD, "BASE_IMAGE", buildimage); 536 | 537 | if Path::new("lib.rs").exists() { 538 | tmp_docker_file = str::replace( 539 | tmp_docker_file.as_str(), 540 | "#LIB_RS", 541 | "COPY lib.rs /app-build/src", 542 | ); 543 | tmp_docker_file = str::replace( 544 | tmp_docker_file.as_str(), 545 | "#TEST", 546 | "RUN cargo test --lib --release", 547 | ); 548 | } 549 | if Path::new(".env").exists() { 550 | tmp_docker_file = 551 | str::replace(tmp_docker_file.as_str(), "#ENV", "COPY .env /app-build/src"); 552 | } 553 | 554 | tmp_docker_file = str::replace(tmp_docker_file.as_str(), "RUNTIME_IMAGE", runtimeimage); 555 | 556 | let process = match Command::new("docker") 557 | .stdin(Stdio::piped()) 558 | .stdout(Stdio::piped()) 559 | .arg("build") 560 | .arg(&tag) 561 | .arg("-f-") 562 | .arg(".") 563 | .spawn() 564 | { 565 | Err(why) => { 566 | println!("couldn't spawn docker: {}", why); 567 | process::exit(1); 568 | } 569 | Ok(process) => process, 570 | }; 571 | 572 | match process.stdin.unwrap().write_all(tmp_docker_file.as_bytes()) { 573 | Err(why) => panic!("couldn't write to docker stdin: {}", why), 574 | Ok(_) => println!("Roche: Sent file to builder for {}", &tag), 575 | } 576 | let mut s = String::new(); 577 | match process.stdout.unwrap().read_to_string(&mut s) { 578 | Err(why) => panic!("couldn't read docker stdout: {}", why), 579 | Ok(_) => print!("Roche: Build complete for {}\n{}", &tag, s), 580 | } 581 | } 582 | } 583 | if matches.is_present("init") { 584 | if let Some(init_matches) = matches.subcommand_matches("init") { 585 | match init_matches.value_of("template").unwrap_or_default() { 586 | "default" => { 587 | let name = init_matches.value_of("name").map(ToOwned::to_owned); 588 | let branch = match init_matches.value_of("branch").map(ToOwned::to_owned) { 589 | Some(b) => b, 590 | None => String::from("main"), 591 | }; 592 | 593 | let force: bool = init_matches.value_of_t("force").unwrap_or(false); 594 | let verbose: bool = init_matches.value_of_t("verbose").unwrap_or(false); 595 | 596 | let args_exposed: PublicArgs = PublicArgs { 597 | git: default_project.to_string(), 598 | branch: Some(branch), 599 | name, 600 | force, 601 | verbose, 602 | }; 603 | 604 | let args: Args = args_exposed.into(); 605 | 606 | generate(args)? 607 | } 608 | "mongodb" => { 609 | let name = init_matches.value_of("name").map(ToOwned::to_owned); 610 | let branch = match init_matches.value_of("branch").map(ToOwned::to_owned) { 611 | Some(b) => b, 612 | None => String::from("main"), 613 | }; 614 | 615 | let force: bool = init_matches.value_of_t("force").unwrap_or(false); 616 | let verbose: bool = init_matches.value_of_t("verbose").unwrap_or(false); 617 | 618 | let args_exposed: PublicArgs = PublicArgs { 619 | git: mongodb_project.to_string(), 620 | branch: Some(branch), 621 | name, 622 | force, 623 | verbose, 624 | }; 625 | 626 | let args: Args = args_exposed.into(); 627 | 628 | generate(args)? 629 | } 630 | &_ => { 631 | if init_matches 632 | .value_of("template") 633 | .unwrap_or_default() 634 | .contains("https://") 635 | { 636 | let name = init_matches.value_of("name").map(ToOwned::to_owned); 637 | let branch = match init_matches.value_of("branch").map(ToOwned::to_owned) { 638 | Some(b) => b, 639 | None => String::from("main"), 640 | }; 641 | 642 | let force: bool = init_matches.value_of_t("force").unwrap_or(false); 643 | let verbose: bool = init_matches.value_of_t("verbose").unwrap_or(false); 644 | 645 | let args_exposed: PublicArgs = PublicArgs { 646 | git: init_matches 647 | .value_of("template") 648 | .unwrap_or_default() 649 | .to_string(), 650 | branch: Some(branch), 651 | name, 652 | force, 653 | verbose, 654 | }; 655 | let args: Args = args_exposed.into(); 656 | 657 | generate(args)? 658 | } else { 659 | // init called but with no options so just generating a function. 660 | let mut file = File::create("functions.rs")?; 661 | file.write_all(FUNCTION.as_bytes())? 662 | } 663 | } 664 | } 665 | } 666 | } 667 | if matches.is_present("gen") { 668 | if let Some(build_matches) = matches.subcommand_matches("gen") { 669 | let buildimage = build_matches 670 | .value_of("buildimage") 671 | .unwrap_or(release_build_image.as_str()); 672 | let runtimeimage = build_matches 673 | .value_of("runtimeimage") 674 | .unwrap_or(runtime_image.as_str()); 675 | let mut tmp_docker_file = str::replace(RELEASE_BUILD, "BASE_IMAGE", buildimage); 676 | tmp_docker_file = str::replace(tmp_docker_file.as_str(), "RUNTIME_IMAGE", runtimeimage); 677 | if Path::new("lib.rs").exists() { 678 | tmp_docker_file = str::replace( 679 | tmp_docker_file.as_str(), 680 | "#LIB_RS", 681 | "COPY lib.rs /app-build/src", 682 | ); 683 | tmp_docker_file = str::replace( 684 | tmp_docker_file.as_str(), 685 | "#TEST", 686 | "RUN cargo test --lib --release", 687 | ); 688 | } 689 | if Path::new(".env").exists() { 690 | tmp_docker_file = 691 | str::replace(tmp_docker_file.as_str(), "#ENV", "COPY .env /app-build/src"); 692 | } 693 | if !Path::new("Dockerfile").exists() { 694 | let mut file = File::create("Dockerfile")?; 695 | file.write_all(tmp_docker_file.as_bytes())?; 696 | } else { 697 | println!("Dockerfile already exists refusing to overwrite it. Please delete it and try again."); 698 | } 699 | } 700 | } 701 | Ok(()) 702 | } 703 | -------------------------------------------------------------------------------- /src/template/Dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM DEV_BASE_IMAGE as builder 2 | COPY . /app-build/src/ 3 | RUN cargo build 4 | FROM RUNTIME_IMAGE 5 | RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser 6 | WORKDIR "/app" 7 | COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/debug/roche-service INCLUDE_ENV ./ 8 | USER rocheuser 9 | ENV PORT 8080 10 | EXPOSE 8080 11 | CMD ["./run.sh"] 12 | -------------------------------------------------------------------------------- /src/template/Libtest.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM TEST_BASE_IMAGE 2 | COPY . /app-build/src/ 3 | RUN cargo test --lib 4 | 5 | -------------------------------------------------------------------------------- /src/template/Release.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM BASE_IMAGE as builder 2 | COPY functions.rs /app-build/src 3 | #LIB_RS 4 | #ENV 5 | RUN cargo build --release 6 | #TEST 7 | FROM RUNTIME_IMAGE 8 | RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser 9 | WORKDIR "/app" 10 | COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/release/roche-service ./ 11 | USER rocheuser 12 | ENV PORT 8080 13 | EXPOSE 8080 14 | CMD ["./run.sh"] 15 | -------------------------------------------------------------------------------- /src/template/function.rs: -------------------------------------------------------------------------------- 1 | pub fn handler() -> tide::Server<()> { 2 | let mut api = tide::new(); 3 | api.at("/").get(|_| async { Ok("Hello, world!") }); 4 | api 5 | } -------------------------------------------------------------------------------- /tests/gen-default.rs: -------------------------------------------------------------------------------- 1 | use remove_dir_all::*; 2 | use std::path::{Path, PathBuf}; 3 | use std::process::Command; 4 | use std::sync::atomic::*; 5 | use std::{env, fs}; 6 | 7 | #[cfg(test)] 8 | 9 | static CNT: AtomicUsize = AtomicUsize::new(0); 10 | thread_local!(static IDX: usize = CNT.fetch_add(1, Ordering::SeqCst)); 11 | 12 | #[test] 13 | fn generate_docker_file_default() { 14 | let path = root("generate_docker_file_default"); 15 | if Path::new(&path).exists() { 16 | remove_dir_all(&path).unwrap(); 17 | } 18 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 19 | assert!(env::set_current_dir(&path).is_ok()); 20 | 21 | let mut me = env::current_exe().expect("couldn't find current exe"); 22 | me.pop(); // chop off exe name 23 | me.pop(); // chop off `deps` 24 | me.push("roche"); 25 | 26 | let mut init_cmd = Command::new(&me) 27 | .arg("gen") 28 | .current_dir(&path) 29 | .spawn() 30 | .unwrap(); 31 | assert!(init_cmd.wait().is_ok()); 32 | 33 | let df = match fs::read_to_string("Dockerfile") { 34 | Ok(s) => s, 35 | Err(e) => panic!("Read Dockerfile failed: {}", e), 36 | }; 37 | assert!(df.contains("quay.io/roche/default:1.4.0")); 38 | assert!(df.contains("quay.io/roche/alpine-libgcc:3.12")); 39 | 40 | remove_dir_all(path).unwrap(); 41 | } 42 | 43 | fn root(name: &str) -> PathBuf { 44 | let idx = IDX.with(|x| *x); 45 | 46 | let mut me = env::current_exe().expect("couldn't find current exe"); 47 | me.pop(); // chop off exe name 48 | me.pop(); // chop off `deps` 49 | me.pop(); // chop off `debug` / `release` 50 | me.push("generated-tests"); 51 | me.push(&format!("test-{}-{}", idx, name)); 52 | return me; 53 | } 54 | -------------------------------------------------------------------------------- /tests/gen-params.rs: -------------------------------------------------------------------------------- 1 | use remove_dir_all::*; 2 | use std::path::{Path, PathBuf}; 3 | use std::process::Command; 4 | use std::sync::atomic::*; 5 | use std::{env, fs}; 6 | 7 | #[cfg(test)] 8 | 9 | static CNT: AtomicUsize = AtomicUsize::new(0); 10 | thread_local!(static IDX: usize = CNT.fetch_add(1, Ordering::SeqCst)); 11 | 12 | #[test] 13 | fn generate_docker_file_with_params() { 14 | let path = root("generate_docker_file_with_params"); 15 | if Path::new(&path).exists() { 16 | remove_dir_all(&path).unwrap(); 17 | } 18 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 19 | assert!(env::set_current_dir(&path).is_ok()); 20 | 21 | let mut me = env::current_exe().expect("couldn't find current exe"); 22 | me.pop(); // chop off exe name 23 | me.pop(); // chop off `deps` 24 | me.push("roche"); 25 | 26 | let mut init_cmd = Command::new(&me) 27 | .arg("gen") 28 | .arg("-r") 29 | .arg("runtime/testcontainer") 30 | .arg("-b") 31 | .arg("build/testimage") 32 | .current_dir(&path) 33 | .spawn() 34 | .unwrap(); 35 | assert!(init_cmd.wait().is_ok()); 36 | 37 | let df = match fs::read_to_string("Dockerfile") { 38 | Ok(s) => s, 39 | Err(e) => panic!("Read Dockerfile failed: {}", e), 40 | }; 41 | assert!(df.contains("build/testimage")); 42 | assert!(df.contains("runtime/testcontainer")); 43 | 44 | remove_dir_all(path).unwrap(); 45 | } 46 | 47 | fn root(name: &str) -> PathBuf { 48 | let idx = IDX.with(|x| *x); 49 | 50 | let mut me = env::current_exe().expect("couldn't find current exe"); 51 | me.pop(); // chop off exe name 52 | me.pop(); // chop off `deps` 53 | me.pop(); // chop off `debug` / `release` 54 | me.push("generated-tests"); 55 | me.push(&format!("test-{}-{}", idx, name)); 56 | return me; 57 | } 58 | -------------------------------------------------------------------------------- /tests/init-build.rs: -------------------------------------------------------------------------------- 1 | use remove_dir_all::*; 2 | use std::env; 3 | use std::fs; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | use std::sync::atomic::*; 7 | 8 | #[cfg(test)] 9 | 10 | static CNT: AtomicUsize = AtomicUsize::new(0); 11 | thread_local!(static IDX: usize = CNT.fetch_add(1, Ordering::SeqCst)); 12 | 13 | #[test] 14 | fn generate_build_function_file_only() { 15 | let path = root("generate_build_function_file_only"); 16 | if Path::new(&path).exists() { 17 | remove_dir_all(&path).unwrap(); 18 | } 19 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 20 | assert!(env::set_current_dir(&path).is_ok()); 21 | 22 | let mut me = env::current_exe().expect("couldn't find current exe"); 23 | me.pop(); // chop off exe name 24 | me.pop(); // chop off `deps` 25 | me.push("roche"); 26 | 27 | let mut init_cmd = Command::new(&me) 28 | .arg("init") 29 | .current_dir(&path) 30 | .spawn() 31 | .unwrap(); 32 | assert!(init_cmd.wait().is_ok()); 33 | 34 | let function_file = format!("{}/{}", path.display(), "functions.rs"); 35 | println!("{}", function_file); 36 | assert!(Path::new(&function_file).exists()); 37 | let mut build_cmd = Command::new(&me) 38 | .arg("build") 39 | .arg("-t") 40 | .arg("generates_function_file_only") 41 | .current_dir(&path) 42 | .spawn() 43 | .unwrap(); 44 | 45 | assert!(build_cmd.wait().is_ok()); 46 | 47 | remove_dir_all(path).unwrap(); 48 | } 49 | 50 | #[test] 51 | fn generate_build_project() { 52 | let path = root("generate_build_project"); 53 | if Path::new(&path).exists() { 54 | remove_dir_all(&path).unwrap(); 55 | } 56 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 57 | assert!(env::set_current_dir(&path).is_ok()); 58 | 59 | let mut me = env::current_exe().expect("couldn't find current exe"); 60 | me.pop(); 61 | me.pop(); 62 | me.push("roche"); 63 | 64 | let mut init_cmd = Command::new(&me) 65 | .arg("init") 66 | .arg("default") 67 | .arg("-n") 68 | .arg("generate_build_project") 69 | .current_dir(&path) 70 | .spawn() 71 | .unwrap(); 72 | assert!(init_cmd.wait().is_ok()); 73 | 74 | let function_folder = format!("{}/{}", path.display(), "generate-build-project"); 75 | println!("{}", function_folder); 76 | assert!(env::set_current_dir(&function_folder).is_ok()); 77 | 78 | //assert!(Path::new(&function_file).exists()); 79 | 80 | let mut build_cmd = Command::new(&me) 81 | .arg("build") 82 | .arg("-t") 83 | .arg("generate_build_project") 84 | .current_dir(&function_folder) 85 | .spawn() 86 | .unwrap(); 87 | 88 | assert!(build_cmd.wait().is_ok()); 89 | 90 | remove_dir_all(path).unwrap(); 91 | } 92 | 93 | #[test] 94 | fn generate_build_project_no_tag() { 95 | let path = root("generate_build_project_no_tag"); 96 | if Path::new(&path).exists() { 97 | remove_dir_all(&path).unwrap(); 98 | } 99 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 100 | assert!(env::set_current_dir(&path).is_ok()); 101 | 102 | let mut me = env::current_exe().expect("couldn't find current exe"); 103 | me.pop(); 104 | me.pop(); 105 | me.push("roche"); 106 | 107 | let mut init_cmd = Command::new(&me) 108 | .arg("init") 109 | .arg("default") 110 | .arg("-n") 111 | .arg("generate_build_project_no_tag") 112 | .current_dir(&path) 113 | .spawn() 114 | .unwrap(); 115 | assert!(init_cmd.wait().is_ok()); 116 | 117 | let function_folder = format!("{}/{}", path.display(), "generate-build-project-no-tag"); 118 | println!("{}", function_folder); 119 | assert!(env::set_current_dir(&function_folder).is_ok()); 120 | 121 | //assert!(Path::new(&function_file).exists()); 122 | 123 | let mut build_cmd = Command::new(&me) 124 | .arg("build") 125 | .current_dir(&function_folder) 126 | .spawn() 127 | .unwrap(); 128 | 129 | assert!(build_cmd.wait().is_ok()); 130 | 131 | remove_dir_all(path).unwrap(); 132 | } 133 | 134 | fn root(name: &str) -> PathBuf { 135 | let idx = IDX.with(|x| *x); 136 | 137 | let mut me = env::current_exe().expect("couldn't find current exe"); 138 | me.pop(); // chop off exe name 139 | me.pop(); // chop off `deps` 140 | me.pop(); // chop off `debug` / `release` 141 | me.push("generated-tests"); 142 | me.push(&format!("test-{}-{}", idx, name)); 143 | return me; 144 | } 145 | -------------------------------------------------------------------------------- /tests/init-release.rs: -------------------------------------------------------------------------------- 1 | use remove_dir_all::*; 2 | use std::env; 3 | use std::fs; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | use std::sync::atomic::*; 7 | 8 | #[cfg(test)] 9 | 10 | static CNT: AtomicUsize = AtomicUsize::new(0); 11 | thread_local!(static IDX: usize = CNT.fetch_add(1, Ordering::SeqCst)); 12 | 13 | #[test] 14 | fn generate_release_function_file_only() { 15 | let path = root("generate_release_function_file_only"); 16 | if Path::new(&path).exists() { 17 | remove_dir_all(&path).unwrap(); 18 | } 19 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 20 | assert!(env::set_current_dir(&path).is_ok()); 21 | 22 | let mut me = env::current_exe().expect("couldn't find current exe"); 23 | me.pop(); // chop off exe name 24 | me.pop(); // chop off `deps` 25 | me.push("roche"); 26 | 27 | let mut init_cmd = Command::new(&me) 28 | .arg("init") 29 | .current_dir(&path) 30 | .spawn() 31 | .unwrap(); 32 | assert!(init_cmd.wait().is_ok()); 33 | 34 | let function_file = format!("{}/{}", path.display(), "functions.rs"); 35 | println!("{}", function_file); 36 | assert!(Path::new(&function_file).exists()); 37 | let mut build_cmd = Command::new(&me) 38 | .arg("release") 39 | .arg("-t") 40 | .arg("generates_release_function_file_only") 41 | .current_dir(&path) 42 | .spawn() 43 | .unwrap(); 44 | 45 | assert!(build_cmd.wait().is_ok()); 46 | 47 | remove_dir_all(path).unwrap(); 48 | } 49 | 50 | #[test] 51 | fn generate_release_project() { 52 | let path = root("generate_release_project"); 53 | if Path::new(&path).exists() { 54 | remove_dir_all(&path).unwrap(); 55 | } 56 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 57 | assert!(env::set_current_dir(&path).is_ok()); 58 | 59 | let mut me = env::current_exe().expect("couldn't find current exe"); 60 | me.pop(); 61 | me.pop(); 62 | me.push("roche"); 63 | 64 | let mut init_cmd = Command::new(&me) 65 | .arg("init") 66 | .arg("default") 67 | .arg("-n") 68 | .arg("generate_release_project") 69 | .current_dir(&path) 70 | .spawn() 71 | .unwrap(); 72 | assert!(init_cmd.wait().is_ok()); 73 | 74 | let function_folder = format!("{}/{}", path.display(), "generate-release-project"); 75 | println!("{}", function_folder); 76 | assert!(env::set_current_dir(&function_folder).is_ok()); 77 | 78 | //assert!(Path::new(&function_file).exists()); 79 | 80 | let mut build_cmd = Command::new(&me) 81 | .arg("release") 82 | .arg("-t") 83 | .arg("generate_release_project") 84 | .current_dir(&function_folder) 85 | .spawn() 86 | .unwrap(); 87 | 88 | assert!(build_cmd.wait().is_ok()); 89 | 90 | remove_dir_all(path).unwrap(); 91 | } 92 | 93 | #[test] 94 | fn generate_release_project_no_tag() { 95 | let path = root("generate_release_project_no_tag"); 96 | if Path::new(&path).exists() { 97 | remove_dir_all(&path).unwrap(); 98 | } 99 | fs::create_dir_all(&path).expect(&format!("couldn't create {:?} directory", path.display())); 100 | assert!(env::set_current_dir(&path).is_ok()); 101 | 102 | let mut me = env::current_exe().expect("couldn't find current exe"); 103 | me.pop(); 104 | me.pop(); 105 | me.push("roche"); 106 | 107 | let mut init_cmd = Command::new(&me) 108 | .arg("init") 109 | .arg("default") 110 | .arg("-n") 111 | .arg("generate_release_project_no_tag") 112 | .current_dir(&path) 113 | .spawn() 114 | .unwrap(); 115 | assert!(init_cmd.wait().is_ok()); 116 | 117 | let function_folder = format!("{}/{}", path.display(), "generate-release-project-no-tag"); 118 | println!("{}", function_folder); 119 | assert!(env::set_current_dir(&function_folder).is_ok()); 120 | 121 | //assert!(Path::new(&function_file).exists()); 122 | 123 | let mut build_cmd = Command::new(&me) 124 | .arg("release") 125 | .current_dir(&function_folder) 126 | .spawn() 127 | .unwrap(); 128 | 129 | assert!(build_cmd.wait().is_ok()); 130 | 131 | remove_dir_all(path).unwrap(); 132 | } 133 | 134 | fn root(name: &str) -> PathBuf { 135 | let idx = IDX.with(|x| *x); 136 | 137 | let mut me = env::current_exe().expect("couldn't find current exe"); 138 | me.pop(); // chop off exe name 139 | me.pop(); // chop off `deps` 140 | me.pop(); // chop off `debug` / `release` 141 | me.push("generated-tests"); 142 | me.push(&format!("test-{}-{}", idx, name)); 143 | return me; 144 | } 145 | --------------------------------------------------------------------------------