├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── Torb.png
├── cli
├── .gitignore
├── Cargo.lock
├── Cargo.toml
└── src
│ ├── animation.rs
│ ├── artifacts.rs
│ ├── builder.rs
│ ├── cli.rs
│ ├── composer.rs
│ ├── config.rs
│ ├── deployer.rs
│ ├── initializer.rs
│ ├── main.rs
│ ├── resolver.rs
│ ├── resolver
│ └── inputs.rs
│ ├── utils.rs
│ ├── vcs.rs
│ └── watcher.rs
└── license_header.txt
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Torb CLI Release
2 | concurrency:
3 | group: tagged-release
4 | cancel-in-progress: true
5 | on:
6 | push:
7 | tags:
8 | - 'v[0-9]+.[0-9]+.[0-9]+-[0-9][0-9].[0-9][0-9]'
9 | jobs:
10 | build_nomac:
11 | strategy:
12 | matrix:
13 | include:
14 | - TARGET: aarch64-unknown-linux-gnu
15 | OS: ubuntu-latest
16 | - TARGET: x86_64-unknown-linux-gnu
17 | OS: ubuntu-latest
18 | name: Build Release Non Mac
19 | defaults:
20 | run:
21 | working-directory: ./cli
22 | runs-on: ${{matrix.OS}}
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v3
26 | - name: Build
27 | run: |
28 | cargo install -f cross
29 | cross build --release --target ${{matrix.TARGET}}
30 | - name: Zip Artifact
31 | run: |
32 | zip target/${{matrix.TARGET}}/release/torb_${{github.ref_name}}_${{matrix.TARGET}} target/${{matrix.TARGET}}/release/torb
33 | - name: Archive Release Artifact
34 | uses: actions/upload-artifact@v3
35 | with:
36 | name: torb_${{github.ref_name}}_${{matrix.TARGET}}
37 | path: cli/target/${{matrix.TARGET}}/release/torb_${{github.ref_name}}_${{matrix.TARGET}}
38 |
39 | build_mac:
40 | strategy:
41 | matrix:
42 | include:
43 | - TARGET: x86_64-apple-darwin
44 | OS: macos-12
45 | name: Build Release Mac
46 | defaults:
47 | run:
48 | working-directory: ./cli
49 | runs-on: ${{matrix.OS}}
50 | steps:
51 | - name: Checkout
52 | uses: actions/checkout@v3
53 | - name: Build
54 | run: |
55 | cargo build --release --target ${{matrix.TARGET}}
56 | - name: Zip Artifact
57 | run: |
58 | zip target/${{matrix.TARGET}}/release/torb_${{github.ref_name}}_${{matrix.TARGET}} target/${{matrix.TARGET}}/release/torb
59 | - name: Archive Release Artifact
60 | uses: actions/upload-artifact@v3
61 | with:
62 | name: torb_${{github.ref_name}}_${{matrix.TARGET}}
63 | path: cli/target/${{matrix.TARGET}}/release/torb_${{github.ref_name}}_${{matrix.TARGET}}
64 |
65 | publish:
66 | runs-on: ubuntu-latest
67 | name: Publish Release
68 | needs: [build_nomac, build_mac]
69 | steps:
70 | - name: Checkout
71 | uses: actions/checkout@v3
72 | - name: Download Build Artifacts
73 | uses: actions/download-artifact@v3
74 | with:
75 | path: build/releases
76 | - name: Publish release
77 | uses: eloquent/github-release-action@v3
78 | with:
79 | generateReleaseNotes: "true"
80 | reactions: +1,hooray,heart,rocket,eyes
81 | discussionCategory: Releases
82 | discussionReactions: +1,laugh,hooray,heart,rocket,eyes
83 | assets: |
84 | - path: build/releases/torb_${{github.ref_name}}_*
85 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Torb CLI Test Pipeline
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build:
14 | defaults:
15 | run:
16 | working-directory: ./cli
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 | - name: Build
23 | run: cargo build --verbose
24 | - name: Run tests
25 | run: cargo test --verbose
26 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/Lucas-C/pre-commit-hooks
3 | rev: v1.4.2
4 | hooks:
5 | - id: forbid-crlf
6 | - id: remove-crlf
7 | - id: forbid-tabs
8 | - id: remove-tabs
9 | args: [--whitespaces-count, '4']
10 | - id: insert-license
11 | files: \.rs$
12 | args:
13 | - --license-filepath
14 | - license_header.txt
15 | - --comment-style
16 | - //
17 | - --use-current-year
18 | - --detect-license-in-X-top-lines=10
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | TORB FOUNDRY BUSINESS SOURCE LICENSE AGREEMENT
2 |
3 | Business Source License 1.1
4 | Licensor: Torb Foundry
5 | Licensed Work: Torb v0.3.0-02.22
6 | The Licensed Work is © 2023-Present Torb Foundry
7 |
8 |
9 | Change License: GNU Affero General Public License Version 3
10 | Additional Use Grant: None
11 | Change Date: Feb 22, 2024
12 |
13 | License text copyright © 2023 MariaDB plc, All Rights Reserved.
14 | “Business Source License” is a trademark of MariaDB plc.
15 |
16 | Terms
17 |
18 | The Licensor hereby grants You the right to copy, modify, create derivative
19 | works, redistribute, and make non-production use of the Licensed Work. The
20 | Licensor may make an Additional Use Grant, above, permitting limited production
21 | use.
22 |
23 | Effective on the Change Date, or the fourth anniversary of the first publicly
24 | available distribution of a specific version of the Licensed Work under this
25 | License, whichever comes first, the Licensor hereby grants you rights under the
26 | terms of the Change License, and the rights granted in the paragraph above
27 | terminate.
28 |
29 | If your use of the Licensed Work does not comply with the requirements currently
30 | in effect as described in this License, you must purchase a commercial license
31 | from the Licensor, its affiliated entities, or authorized resellers, or you must
32 | refrain from using the Licensed Work.
33 |
34 | All copies of the original and modified Licensed Work, and derivative works of
35 | the Licensed Work, are subject to this License. This License applies separately
36 | for each version of the Licensed Work and the Change Date may vary for each
37 | version of the Licensed Work released by Licensor.
38 |
39 | You must conspicuously display this License on each original or modified copy of
40 | the Licensed Work. If you receive the Licensed Work in original or modified form
41 | from a third party, the terms and conditions set forth in this License apply to
42 | your use of that work.
43 |
44 | Any use of the Licensed Work in violation of this License will automatically
45 | terminate your rights under this License for the current and all other versions
46 | of the Licensed Work.
47 |
48 | This License does not grant you any right in any trademark or logo of Licensor
49 | or its affiliates (provided that you may use a trademark or logo of Licensor as
50 | expressly required by this License).
51 |
52 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
53 | "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS
54 | OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY,
55 | FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
56 |
57 | MariaDB hereby grants you permission to use this License's text to license your
58 | works, and to refer to it using the trademark "Business Source License", as long
59 | as you comply with the Covenants of Licensor below.
60 |
61 | Covenants of Licensor
62 |
63 | In consideration of the right to use this License's text and the "Business
64 | Source License" name and trademark, Licensor covenants to MariaDB, and to all
65 | other recipients of the licensed work to be provided by Licensor:
66 |
67 | 1. To specify as the Change License the GPL Version 2.0 or any later version, or
68 | a license that is compatible with GPL Version 2.0 or a later version, where
69 | "compatible" means that software provided under the Change License can be
70 | included in a program with software provided under GPL Version 2.0 or a later
71 | version. Licensor may specify additional Change Licenses without limitation.
72 |
73 | 2. To either: (a) specify an additional grant of rights to use that does not
74 | impose any additional restriction on the right granted in this License, as the
75 | Additional Use Grant; or (b) insert the text "None".
76 |
77 | 3. To specify a Change Date.
78 |
79 | 4. Not to modify this License in any other way.
80 |
81 | Notice
82 |
83 | The Business Source License (this document, or the "License") is not an Open
84 | Source license. However, the Licensed Work will eventually be made available
85 | under an Open Source License, as stated in this License.
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Shoutout to my friend @SystemOverlord \[REAL NAME REDACTED\] for the anvil and sparks image for the org. This shoutout and the beer I have yet to get him satisfy our arrangement.
4 |
5 | ## What
6 |
7 | Torb is a tool for quickly setting up best practice development infrastructure along with development stacks that have reasonably sane defaults. Instead of taking a couple hours to get a project started and then a week to get your infrastructure correct, do all of that in a couple minutes.
8 |
9 | ## Mission
10 |
11 | Make it simple and easy for software engineers to create and deploy infrastructure with best practices in place. The ideal would be 10 minutes or less to do so and the point where I'd consider Torb a failure is if it takes more than an hour to have dev, staging and prod with best practices in place.
12 |
13 | In addition to the above Torb needs to provide an easy way of adding additional infrastructure and requirements as a project scales. On day one you probably have logs and something like Sentry or Rollbar but you might not have great CI/CD or more complex distributed tracing or bill of materials or an artifact repository or whatever. It should be stupid simple to add these to an existing system. Infrastructure needs change as a project changes and we should be flexible enough to accomodate that.
14 |
15 | ## Getting Started
16 |
17 | First download the appropriate binary for your OS.
18 |
19 | After that's downloaded you can either run Torb from your preferred directory or copy it somewhere on your PATH.
20 |
21 | 1. Run `torb`. You'll see that the CLI is broken into nouns such as "repo" and "stack". Under each noun are the verbs that act upon it and let you do useful things.
22 | 2. Now run `torb init`. This will create a .torb folder located in your user's home directory. Inside of this we download a version of Terraform and pull our artifacts repo which contains community contributed Stacks, [Services](Torb#services) and [Projects](Torb#Projects). Finally this creates a `config.yaml` file which is where all of the CLI configuration is kept.
23 | 3. Now you're ready to begin setting up a project using Torb.
24 |
25 | ## Configuring Torb
26 |
27 | Earlier we mentioned a `config.yaml` file located in `~/.torb`, currently this file is pretty simple. It has two keys:
28 |
29 | - githubToken - a PAT with access to read, write and admin.
30 | - githubUser - The username of the user we are acting on behalf of.
31 |
32 | ## Repos
33 |
34 | ### Creating
35 |
36 | Torb can create both a local repo and a repo on a service such as GitHub automatically. This:
37 |
38 | - Creates the local folder
39 | - Initializes Git,
40 | - Creates a blank README
41 | - Creates the remote repository on GitHub.
42 | - Links local and remote
43 | - Pushes first commit
44 |
45 | Currently this doesn't happen under an organization, but that is on the list of things to tackle as config. It may be sufficient to provid and Organization token in the above config, but as of now it has not been tested.
46 |
47 | **Note: Providing the full path for the local repo instead of just a name is currently required.**
48 |
49 | torb repo create ~/example/path/to/new_repo
50 |
51 | This will create a local repo `new_repo` at the path provided and handle everything listed above.
52 |
53 | ## Stacks
54 |
55 | ### Checking-out and Initializing
56 |
57 | #### Checking-out
58 |
59 | First change directory into the repo where you'd like the stack to live.
60 |
61 | Next list the available stacks with:
62 |
63 | torb stack list
64 |
65 | This will output something like:
66 |
67 | ```
68 | Torb Stacks:
69 |
70 | - Flask App w/ React Frontend
71 | - Rook Ceph Cluster
72 | ```
73 |
74 | For this example we're going to choose `flask-react`
75 |
76 | Run:
77 |
78 | torb stack checkout flask-react
79 |
80 | **Note: Depending on your shell you may need different quotes for names with spaces.**
81 |
82 | This will produce a new file `stack.yaml` in your current directory, if you cat the file you can see the structure of a stack:
83 |
84 | ```
85 | ➜ test_repo git:(main) ✗ ls
86 | stack.yaml
87 |
88 | ➜ test_repo git:(main) ✗ cat stack.yaml
89 | version: v1.0.0
90 | kind: stack
91 | name: "Flask App w/ React Frontend"
92 | description: "Production ready flask web app."
93 | services:
94 | postgres_1:
95 | service: postgresql
96 | values: {}
97 | inputs:
98 | port: "5432"
99 | user: postgres
100 | password: postgres
101 | database: postgres
102 | num_replicas: "1"
103 | nginx_ingress_controller_1:
104 | service: nginx-ingress-controller
105 | values:
106 | controller:
107 | admissionWebhooks:
108 | enabled: false
109 | inputs: {}
110 | projects:
111 | flaskapp_1:
112 | project: flaskapp
113 | inputs:
114 | name: flaskapp
115 | db_host: self.service.postgres_1.output.host
116 | db_port: self.service.postgres_1.output.port
117 | db_user: self.service.postgres_1.output.user
118 | db_pass: self.service.postgres_1.output.password
119 | db_database: self.service.postgres_1.output.database
120 | values: {}
121 | build:
122 | tag: latest
123 | registry: ""
124 | createreactapp_1:
125 | project: createreactapp
126 | inputs:
127 | name: createreactapp
128 | ingress: "true"
129 | values:
130 | ingress:
131 | hosts:
132 | - name: localhost
133 | path: /
134 | servicePort: "8000"
135 | extraEnvVars:
136 | - name: API_HOST
137 | value: self.project.flaskapp_1.output.host
138 | - name: API_PORT
139 | value: "5000"
140 | build:
141 | tag: latest
142 | registry: ""
143 | deps:
144 | services:
145 | - nginx_ingress_controller_1
146 | ```
147 |
148 | A stack is comprised of `services` and `projects` as the basic units. We won't go into their structure deeply here, but approximately a service is something a user would just configure and deploy and a project is anything where a user is modifying source and building.
149 |
150 | Each stack is a DAG and dependencies can either be explicitly listed as they are above or Torb can figure them out implicitly based on references in the inputs sections and the values overrides in any of the units. Each unit in a stack is referenced internally by it's fully qualified name comprised of ..
151 |
152 | When a stack is initialized, built or deployed the dependency chain is walked to the end and executed, this is then unwound all the way to the initial starting unit(s).
153 |
154 | #### Initializing
155 |
156 | After you've checked out a stack you need to initialize it before you can proceed to build and deploy the stack. Each unit can in it's definition include an initialization step to help set it up in your project. Most of the time for `projects` this means creating the folder, running a generator of somekind to create default code and copying over any config or build files it will need. If you need to examine a particular unit to see what it does you can check it out in [Torb Artifacts](https://github.com/TorbFoundry/torb-artifacts)
157 |
158 | To initialize your stack run:
159 |
160 | torb stack init stack.yaml
161 |
162 | With the stack that we're using your repo will look something like this:
163 |
164 | ```
165 | ➜ test_repo git:(main) ✗ ls
166 | createreactapp flaskapp flaskapp_venv stack.yaml
167 | ```
168 |
169 | You'll need to install npm and node for this tutorial, if you don't already have recent versions you can follow their [guide](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
170 |
171 | Each folder is the name of the unit in [Torb Artifacts](https://github.com/TorbFoundry/torb-artifacts)
172 |
173 | Depending on the unit, you'll need to build the artifact like `npm build` before you are able to deploy. Go ahead and change directory into `createreactapp` and run `npm run build`. Torb will not install programming languages, libraries or anything else for working with projects so make sure you have these things installed.
174 |
175 |
176 | Next we'll look at building and deploying.
177 |
178 | ### Building and Deploying
179 |
180 | #### Building
181 |
182 | Currently Torb supports building an image with Docker, or a build script, but not both. The resulting artifact must be a docker image regardless. Using a build script will let you do additional steps such as have torb automatically run the `npm run build` step from above in addition to building the image.
183 |
184 | Building your stack will recurse through the graph and run whatever is configured in the `build` section for the individual unit defined in the `stack.yaml`
185 |
186 | ```
187 | flaskapp_1:
188 | project: flaskapp
189 | inputs:
190 | name: flaskapp
191 | db_host: self.service.postgres_1.output.host
192 | db_port: self.service.postgres_1.output.port
193 | db_user: self.service.postgres_1.output.user
194 | db_pass: self.service.postgres_1.output.password
195 | db_database: self.service.postgres_1.output.database
196 | values: {}
197 | build:
198 | tag: latest
199 | registry: ""
200 | deps:
201 | services:
202 | - postgres_1
203 | ```
204 |
205 | **Note: To use a script instead, set script_path instead of tag and registry.**
206 |
207 | You can see in the above unit that build is configured to tag the docker image with `latest` and since the registry is empty it will push the image to the default docker hub repository you are currently signed in to.
208 |
209 | If you just want to have the image locally and skip pushing to a registry you can change registry to `local`. This is useful is you're running a kubernetes cluster that can read your local docker images like the cluster that can be enabled with Docker Destkop on mac and wsl.
210 |
211 | If you're running a kubernetes cluster on a remote server you will need to make sure the appropriate registry is configured here and that you are authenticated to it as this will also be used to pull the image on your cluster later on.
212 |
213 | **Note: If you're using Minikube you will either need to use remote registry or load the local image with `minikube image load `**
214 |
215 | To build your stack run
216 |
217 | torb stack build stack.yaml
218 |
219 | **Note: If your image registry is a separate locally hosted service like the one found in our quickstart stack you will need to pass `--local-hosted-registry`
220 |
221 | Expect the first build to take some time as this will be building the docker images from scratch.
222 |
223 | If all goes well you should see output for the main IAC (Terraform) file torb generates for it's internal build state.
224 |
225 | **Note: All build state is kept in a hidden folder .torb_buildstate in your repo. Currently this isn't intended to be exposed to users, but that may change in the future. We want to add eject functionality if people choose to opt out of using Torb and at that time this will be more up front.***
226 |
227 |
228 | #### Deploying
229 |
230 | ##### Foreword
231 |
232 | Torb currently deploys only to Kubernetes, we use Terraform to handle the deploy and bundle a version of terraform in the .torb folder in your home directory. This is so we can control what version Torb is using and keep it separate from any other version of Terraform you might already be using on your system.
233 |
234 | Torb respects the `KUBECONFIG` env var and if that is not set we default to `~/.kube/config` whatever your active context is will be used so make sure you're set to the right cluster. This also makes us fairly cluster agnostic as we rely on you to have access and connectivity to the cluster you want to deploy to.
235 |
236 | Deploys respect the dependency ordering set in the `stack.yaml` we use the same method for detecing implicit and explicit depencies in Torb.
237 |
238 | There are some tricky aspects of the deploy, we rely on the `helm provider` in Terraform and Helm in general to deploy to Kube. Helm itself is a good tool and handles a lot of complexities of putting together a set of artifacts in a convenient bundle, but is fairly limited and opaque when it comes to handling errors, timeouts, dealing with data persistance etc during a deploy. In that case it really is only a vehicle for applying a chart. This means we are limited by Helm AND by the respective chart maintainers for our artifacts.
239 |
240 | As an example, if the chart being applied isn't useing StatefulSets and includes PersistentVolumeClaims your PVC will be deleted when the chart is cleaned up. In a lot of ways it may be better to create a separate PVC under a StatefulSet in addition to the existing Deployment based chart and see if the chart supports passing a reference to that claim, versus relying on them to do the correct thing for your usecase.
241 |
242 | Torb does not at this moment have a way to enforce these practices but as we grow can put requirements in place for our artifacts that will help here. Hopefully this isn't too often exposed to you as end users, but is a concern for anyone who is creating stacks and units under [Torb Artifacts](https://github.com/TorbFoundry/torb-artifacts)
243 |
244 | Longer term we may work on something to replace using Helm while trying to support the chart format itself but for now it's the best we have. As an example we've looked into Kustomize and handling releases ourselves but need to further evaluate how much we will lose out on from the Helm ecosystem.
245 |
246 | ##### Deploy
247 |
248 | To deploy with Torb run
249 |
250 | torb stack deploy stack.yaml
251 |
252 | You should see Terraform initialize a workspace and begin to apply a plan.
253 |
254 | At this point you can wait until things finish or use Kubectl to check the status of the deployment. The namespace being deployed to can be configured at the stack level and a per unit level in the `stack.yaml`.
255 |
256 | Currently we are using a local backend for Terraform but do plan to support popular cloud providers, and our own cloud solution.
257 |
258 | If all is good you will eventually see a success message from Terraform with a list of new infrastructure created, changed or removed.
259 |
260 | In the event of an issue the default timeout is 5 minutes and you can safely clean up releases in Helm without impacting Torb.
261 |
262 | #### Watcher
263 |
264 | Torb supports quick iteration with our filesystem watcher. Our watcher aggregates change events to files based on configured paths, and on a set interval, also configurable in your stack.yaml, will redeploy the services and projects if changes are found. Watcher configuration at the top level in the stack.yaml looks like:
265 |
266 | ```
267 | watcher:
268 | paths:
269 | - "./emojee"
270 | interval: 8000
271 | patch: true
272 | ```
273 |
274 | Paths are provided as a list and watched recursively, interval is in miliseconds and patch when true will change the imagePullPolicy to Always for all projects and services in your stack.yaml. All build files and general output like IaC files are kept separate from your main buildstate. However, Terraform's buildstate *is* copied between environments and the change to image pull policies is also reflected back in your main terraform buildstate. Doing all of this ensures when you go to build and deploy your stack normally any changes are properly reverted for the cluster you're using.
275 |
276 | You can run the watcher with
277 |
278 | torb stack watch stack.yaml
279 |
280 | **Note: If your image registry is a separate locally hosted service like the one found in our quickstart stack you will need to pass `--local-hosted-registry`**
281 |
282 | The watcher will initialize it's environment and redeploy any services with changes if patch is true. This may take a few moments as resource states are reconciled.
283 |
--------------------------------------------------------------------------------
/Torb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TorbFoundry/torb/6af1177a05a91d8675794b7b2269a3648c7a080f/Torb.png
--------------------------------------------------------------------------------
/cli/.gitignore:
--------------------------------------------------------------------------------
1 | target/*
2 |
--------------------------------------------------------------------------------
/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "torb"
3 | version = "0.1.0"
4 | edition = "2021"
5 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
6 |
7 | [dependencies]
8 | tempfile = "3.3.0"
9 | dirs = "1.0.4"
10 | clap = { version = "3.1.6", features = ["derive"] }
11 | serde = { version = "1.0", features = ["derive"] }
12 | serde_yaml = "0.8"
13 | thiserror = "1.0"
14 | sha2 = "0.10.2"
15 | base64ct = { version = "1.5.1", features = ["alloc"] }
16 | serde_json = "1.0.85"
17 | hcl-rs = "0.10.0"
18 | indexmap = "1.9.1"
19 | memorable-wordlist = "0.1.7"
20 | ureq = { version = "2.5.0", features = ["json"] }
21 | once_cell = "1.15.0"
22 | chrono = "0.4.22"
23 | data-encoding = { version = "2.3.2", features = ["alloc"] }
24 | rayon = "1.6.1"
25 | notify = "5.1.0"
26 | tokio = { version = "1.26.0", features = ["full"] }
27 | colored = "2.0.0"
28 | rust-embed = "6.6.0"
29 | gif = "0.12.0"
30 | drawille = "0.3.0"
31 | image = "0.24.5"
32 | crossterm = "0.26.1"
--------------------------------------------------------------------------------
/cli/src/animation.rs:
--------------------------------------------------------------------------------
1 | // Business Source License 1.1
2 | // Licensor: Torb Foundry
3 | // Licensed Work: Torb v0.3.7-03.23
4 | // The Licensed Work is © 2023-Present Torb Foundry
5 | //
6 | // Change License: GNU Affero General Public License Version 3
7 | // Additional Use Grant: None
8 | // Change Date: Feb 22, 2023
9 | //
10 | // See LICENSE file at https://github.com/TorbFoundry/torb/blob/main/LICENSE for details.
11 |
12 | use core::fmt::Display;
13 | use image::imageops::resize;
14 | use std::fmt::Debug;
15 |
16 | use crossterm::{cursor, terminal, ExecutableCommand, QueueableCommand};
17 | use drawille::{Canvas, PixelColor};
18 | use image::codecs::gif::GifDecoder;
19 | use image::{AnimationDecoder, ImageDecoder};
20 | use std::io::{stdout, Write};
21 | use std::sync::{
22 | atomic::{AtomicBool, Ordering},
23 | Arc,
24 | };
25 | use std::{thread, time};
26 |
27 | use crate::utils::{PrettyContext, PrettyExit};
28 |
29 | const FRAME_HEIGHT: u16 = 16;
30 |
31 | pub struct BuilderAnimation {}
32 |
33 | pub trait Animation {
34 | fn do_with_animation(&self, f: Box Result>) -> Result
35 | where
36 | E: Debug + Display;
37 |
38 | fn start_animation(
39 | &self,
40 | animation: std::fs::File,
41 | kill_flag: Arc,
42 | ) -> std::thread::JoinHandle<()>;
43 | }
44 |
45 | impl BuilderAnimation {
46 | pub fn new() -> Self {
47 | BuilderAnimation {}
48 | }
49 | }
50 | impl Animation for BuilderAnimation
51 | where
52 | E: Debug + Display,
53 | {
54 | fn start_animation(
55 | &self,
56 | animation: std::fs::File,
57 | kill_flag: Arc,
58 | ) -> std::thread::JoinHandle<()> {
59 | let decoder = GifDecoder::new(animation).unwrap();
60 |
61 | let (mut width, mut height) = decoder.dimensions();
62 |
63 | let scale = 0.3;
64 |
65 | width = f32::floor(width as f32 * scale) as u32;
66 | height = f32::floor(height as f32 * scale) as u32;
67 |
68 | let mut canvas = Canvas::new(width, height);
69 | let frames = decoder.into_frames();
70 | let frames_opt = frames.collect_frames().use_or_pretty_warn(
71 | PrettyContext::default()
72 | .warn("Warning! Unable to decode frames for animation GIF.")
73 | .pretty(),
74 | );
75 |
76 | let kill_flag_clone = kill_flag.clone();
77 |
78 | thread::spawn(move || {
79 | let mut thread_stdout = stdout();
80 |
81 | loop {
82 | let kill_flag = kill_flag_clone.clone();
83 |
84 | if kill_flag.load(Ordering::SeqCst) == true {
85 | thread_stdout.write("\r".as_bytes()).unwrap();
86 | thread_stdout.flush().unwrap();
87 | break;
88 | };
89 |
90 | let frames = frames_opt.clone().unwrap_or(vec![]);
91 |
92 | for frame in frames.iter().cloned() {
93 | let mut img = frame.into_buffer();
94 | img = resize(&img, width, height, image::imageops::FilterType::Gaussian);
95 | for x in 0..width {
96 | for y in 0..height {
97 | let pixel = img.get_pixel(x, y);
98 | let color = PixelColor::TrueColor {
99 | r: pixel[0],
100 | g: pixel[1],
101 | b: pixel[2],
102 | };
103 | canvas.set_colored(x, y, color);
104 | }
105 | }
106 |
107 | let frame = canvas.frame();
108 |
109 | thread_stdout.write_all(frame.as_bytes()).unwrap();
110 | thread_stdout.flush().unwrap();
111 | thread::sleep(time::Duration::from_millis(60));
112 | canvas.clear();
113 |
114 | // Move up the height of the frame in the terminal
115 | thread_stdout.queue(cursor::MoveUp(FRAME_HEIGHT)).unwrap();
116 | }
117 | }
118 | })
119 | }
120 |
121 | fn do_with_animation(&self, mut f: Box Result>) -> Result
122 | where
123 | E: Debug + Display,
124 | {
125 | let home_dir = dirs::home_dir().unwrap();
126 | let torb_path = home_dir.join(".torb");
127 | let repository_path = torb_path.join("repositories");
128 | let repo = "torb-artifacts";
129 | let gif_path = "torb_dwarf_animation.gif";
130 |
131 | let artifacts_path = repository_path.join(repo);
132 | let animation_path = artifacts_path.join(gif_path);
133 |
134 | let animation_opt = std::fs::File::open(animation_path).use_or_pretty_warn(
135 | PrettyContext::default()
136 | .warn("Warning! Unable to open animation GIF.")
137 | .pretty(),
138 | );
139 |
140 | let mut new_stdout = stdout();
141 | new_stdout.execute(cursor::Hide).use_or_pretty_warn_send(
142 | PrettyContext::default()
143 | .warn("Warning! Unable to hide cursor for animation.")
144 | .pretty(),
145 | );
146 |
147 | let kill_flag = Arc::new(AtomicBool::new(false));
148 | let animation_thread_handle_opt = if animation_opt.is_some() {
149 | let animation = animation_opt.unwrap();
150 |
151 | Some(>::start_animation(
152 | self,
153 | animation,
154 | kill_flag.clone(),
155 | ))
156 | } else {
157 | None
158 | };
159 |
160 | let res = f();
161 | kill_flag.store(true, Ordering::SeqCst);
162 |
163 | if animation_thread_handle_opt.is_some() {
164 | let handle = animation_thread_handle_opt.unwrap();
165 | handle.join().use_or_pretty_warn_send(
166 | PrettyContext::default()
167 | .warn("Warning! Animation thread in an errored state when joining.")
168 | .pretty(),
169 | );
170 | };
171 |
172 | new_stdout.execute(cursor::Show).unwrap();
173 | new_stdout
174 | .execute(terminal::Clear(terminal::ClearType::FromCursorDown))
175 | .unwrap();
176 | res
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/cli/src/artifacts.rs:
--------------------------------------------------------------------------------
1 | // Business Source License 1.1
2 | // Licensor: Torb Foundry
3 | // Licensed Work: Torb v0.3.7-03.23
4 | // The Licensed Work is © 2023-Present Torb Foundry
5 | //
6 | // Change License: GNU Affero General Public License Version 3
7 | // Additional Use Grant: None
8 | // Change Date: Feb 22, 2023
9 | //
10 | // See LICENSE file at https://github.com/TorbFoundry/torb/blob/main/LICENSE for details.
11 |
12 | use crate::composer::InputAddress;
13 | use crate::resolver::inputs::{InputResolver, NO_INITS_FN};
14 | use crate::resolver::{resolve_stack, NodeDependencies, StackGraph};
15 | use crate::utils::{buildstate_path_or_create, checksum, kebab_to_snake_case, snake_case_to_kebab};
16 | use crate::watcher::{WatcherConfig};
17 |
18 | use data_encoding::BASE32;
19 | use indexmap::{IndexMap, IndexSet};
20 | use memorable_wordlist;
21 | use once_cell::sync::Lazy;
22 | use serde::ser::SerializeSeq;
23 | use serde::{de, de::SeqAccess, de::Visitor, Deserialize, Deserializer, Serialize};
24 | use serde_yaml::{self};
25 | use sha2::{Digest, Sha256};
26 | use std::fs;
27 | use std::io::Write;
28 | use thiserror::Error;
29 |
30 | #[derive(Error, Debug)]
31 | pub enum TorbArtifactErrors {
32 | #[error("Hash of loaded build file does not match hash of file on disk.")]
33 | LoadChecksumFailed,
34 | }
35 |
36 | #[derive(Serialize, Deserialize, Debug, Clone)]
37 | pub struct InitStep {
38 | pub steps: Vec,
39 | }
40 |
41 | #[derive(Serialize, Deserialize, Clone, Debug, Default)]
42 | pub struct BuildStep {
43 | #[serde(default = "String::new")]
44 | pub script_path: String,
45 | #[serde(default = "String::new")]
46 | pub dockerfile: String,
47 | #[serde(default = "String::new")]
48 | pub tag: String,
49 | #[serde(default = "String::new")]
50 | pub registry: String,
51 | }
52 |
53 | fn get_types() -> IndexSet<&'static str> {
54 | IndexSet::from(["bool", "array", "string", "numeric"])
55 | }
56 |
57 | pub static TYPES: Lazy> = Lazy::new(get_types);
58 |
59 | #[derive(Serialize, Deserialize, Debug, Clone)]
60 | pub enum TorbNumeric {
61 | Int(u64),
62 | NegInt(i64),
63 | Float(f64),
64 | }
65 |
66 | #[derive(Debug, Clone)]
67 | pub enum TorbInput {
68 | Bool(bool),
69 | Array(Vec),
70 | String(String),
71 | Numeric(TorbNumeric),
72 | }
73 |
74 | impl From for TorbInput {
75 | fn from(value: bool) -> Self {
76 | TorbInput::Bool(value)
77 | }
78 | }
79 |
80 | impl From for TorbInput {
81 | fn from(value: u64) -> Self {
82 | let wrapper = TorbNumeric::Int(value);
83 |
84 | TorbInput::Numeric(wrapper)
85 | }
86 | }
87 |
88 | impl From for TorbInput {
89 | fn from(value: i64) -> Self {
90 | let wrapper = TorbNumeric::NegInt(value);
91 |
92 | TorbInput::Numeric(wrapper)
93 | }
94 | }
95 |
96 | impl From for TorbInput {
97 | fn from(value: f64) -> Self {
98 | let wrapper = TorbNumeric::Float(value);
99 |
100 | TorbInput::Numeric(wrapper)
101 | }
102 | }
103 |
104 | impl From for TorbInput {
105 | fn from(value: String) -> Self {
106 | TorbInput::String(value)
107 | }
108 | }
109 |
110 | impl From<&str> for TorbInput {
111 | fn from(value: &str) -> Self {
112 | TorbInput::String(value.to_string())
113 | }
114 | }
115 |
116 |
117 | impl From> for TorbInput
118 | where
119 | TorbInput: From,
120 | T: Clone,
121 | {
122 | fn from(value: Vec) -> Self {
123 | let mut new_vec = Vec::::new();
124 |
125 | for item in value.iter().cloned() {
126 | new_vec.push(Into::::into(item));
127 | }
128 |
129 | TorbInput::Array(new_vec)
130 | }
131 | }
132 |
133 | impl TorbInput {
134 | pub fn serialize_for_init(&self) -> String {
135 |
136 | let serde_val = serde_json::to_string(self).unwrap();
137 |
138 | serde_json::to_string(&serde_val).expect("Unable to serialize TorbInput to JSON, this is a bug and should be reported to the project maintainer(s).")
139 | }
140 |
141 | }
142 |
143 | #[derive(Debug, Clone)]
144 | pub struct TorbInputSpec {
145 | typing: String,
146 | default: TorbInput,
147 | mapping: String,
148 | }
149 |
150 | #[derive(Serialize, Deserialize, Debug, Clone)]
151 | pub struct ArtifactNodeRepr {
152 | #[serde(default = "String::new")]
153 | pub fqn: String,
154 | pub name: String,
155 | pub version: String,
156 | pub kind: String,
157 | pub lang: Option,
158 | #[serde(alias = "init")]
159 | pub init_step: Option>,
160 | #[serde(alias = "build")]
161 | pub build_step: Option,
162 | #[serde(alias = "deploy")]
163 | pub deploy_steps: IndexMap>>,
164 | #[serde(default = "IndexMap::new")]
165 | pub mapped_inputs: IndexMap,
166 | #[serde(alias = "inputs", default = "IndexMap::new")]
167 | pub input_spec: IndexMap,
168 | #[serde(default = "Vec::new")]
169 | pub outputs: Vec,
170 | #[serde(default = "Vec::new")]
171 | pub dependencies: Vec,
172 | #[serde(default = "IndexSet::new")]
173 | pub implicit_dependency_fqns: IndexSet,
174 | #[serde(skip)]
175 | pub dependency_names: NodeDependencies,
176 | #[serde(default = "String::new")]
177 | pub file_path: String,
178 | #[serde(skip)]
179 | pub stack_graph: Option,
180 | pub files: Option>,
181 | #[serde(default = "String::new")]
182 | pub values: String,
183 | pub namespace: Option,
184 | pub source: Option,
185 | #[serde(default="bool::default")]
186 | pub expedient: bool
187 | }
188 |
189 | struct TorbInputDeserializer;
190 | impl<'de> Visitor<'de> for TorbInputDeserializer {
191 | type Value = TorbInput;
192 |
193 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
194 | formatter.write_str("a numeric value.")
195 | }
196 |
197 | fn visit_seq(self, mut seq: A) -> Result
198 | where
199 | A: SeqAccess<'de>, {
200 | let mut container = Vec::::new();
201 |
202 | loop {
203 | let val_opt: Option = seq.next_element()?;
204 |
205 | if val_opt.is_some() {
206 | let value = val_opt.unwrap();
207 |
208 | let input = match value {
209 | serde_yaml::Value::String(val) => {
210 | TorbInput::String(val)
211 | }
212 | serde_yaml::Value::Bool(val) => {
213 | TorbInput::Bool(val)
214 | },
215 | serde_yaml::Value::Number(val) => {
216 | if val.is_f64() {
217 | TorbInput::Numeric(TorbNumeric::Float(val.as_f64().unwrap()))
218 | } else if val.is_u64() {
219 | TorbInput::Numeric(TorbNumeric::Int(val.as_u64().unwrap()))
220 | } else {
221 | TorbInput::Numeric(TorbNumeric::NegInt(val.as_i64().unwrap()))
222 | }
223 | },
224 | serde_yaml::Value::Null => {
225 | panic!("Null values not acceptable as element in type Array.")
226 | },
227 | serde_yaml::Value::Sequence(_) => {
228 | panic!("Nested Array types are not currently supported.")
229 | }
230 | serde_yaml::Value::Mapping(_val) => {
231 | panic!("Map types are not currently supported as array elements. (Or at all.)")
232 | }
233 | };
234 |
235 | container.push(input);
236 | } else {
237 | break;
238 | }
239 | }
240 |
241 | let input = TorbInput::Array(container);
242 |
243 | Ok(input)
244 | }
245 |
246 | fn visit_f32(self, v: f32) -> Result
247 | where
248 | E: de::Error,
249 | {
250 | Ok(TorbInput::Numeric(TorbNumeric::Float(v.into())))
251 | }
252 |
253 | fn visit_str(self, v: &str) -> Result
254 | where
255 | E: de::Error,
256 | {
257 | Ok(TorbInput::String(v.to_string()))
258 | }
259 |
260 | fn visit_string(self, v: String) -> Result
261 | where
262 | E: de::Error,
263 | {
264 | Ok(TorbInput::String(v))
265 | }
266 |
267 | fn visit_bool(self, v: bool) -> Result
268 | where
269 | E: de::Error,
270 | {
271 | Ok(TorbInput::Bool(v))
272 | }
273 |
274 | fn visit_f64(self, v: f64) -> Result
275 | where
276 | E: de::Error,
277 | {
278 | Ok(TorbInput::Numeric(TorbNumeric::Float(v.into())))
279 | }
280 |
281 | fn visit_u64(self, v: u64) -> Result
282 | where
283 | E: de::Error,
284 | {
285 | Ok(TorbInput::Numeric(TorbNumeric::Int(v.into())))
286 | }
287 | fn visit_u32(self, v: u32) -> Result
288 | where
289 | E: de::Error,
290 | {
291 | Ok(TorbInput::Numeric(TorbNumeric::Int(v.into())))
292 | }
293 | fn visit_u16(self, v: u16) -> Result
294 | where
295 | E: de::Error,
296 | {
297 | Ok(TorbInput::Numeric(TorbNumeric::Int(v.into())))
298 | }
299 |
300 | fn visit_u8(self, v: u8) -> Result
301 | where
302 | E: de::Error,
303 | {
304 | Ok(TorbInput::Numeric(TorbNumeric::Int(v.into())))
305 | }
306 |
307 | fn visit_i8(self, v: i8) -> Result
308 | where
309 | E: de::Error, {
310 | if v > 0 {
311 | panic!("Only for negatives.")
312 | }
313 | Ok(TorbInput::Numeric(TorbNumeric::NegInt(v.into())))
314 | }
315 | fn visit_i16(self, v: i16) -> Result
316 | where
317 | E: de::Error, {
318 | if v > 0 {
319 | panic!("Only for negatives.")
320 | }
321 | Ok(TorbInput::Numeric(TorbNumeric::NegInt(v.into())))
322 | }
323 | fn visit_i32(self, v: i32) -> Result
324 | where
325 | E: de::Error, {
326 | if v > 0 {
327 | panic!("Only for negatives.")
328 | }
329 | Ok(TorbInput::Numeric(TorbNumeric::NegInt(v.into())))
330 | }
331 | fn visit_i64(self, v: i64) -> Result
332 | where
333 | E: de::Error, {
334 | if v > 0 {
335 | panic!("Only for negatives.")
336 | }
337 | Ok(TorbInput::Numeric(TorbNumeric::NegInt(v.into())))
338 | }
339 | }
340 |
341 | impl<'de> Deserialize<'de> for TorbInput {
342 | fn deserialize(deserializer: D) -> Result
343 | where
344 | D: Deserializer<'de>,
345 | {
346 | deserializer.deserialize_any(TorbInputDeserializer)
347 | }
348 | }
349 |
350 | struct TorbInputSpecDeserializer;
351 | impl<'de> Visitor<'de> for TorbInputSpecDeserializer {
352 | type Value = TorbInputSpec;
353 |
354 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
355 | formatter.write_str("a list.")
356 | }
357 |
358 | fn visit_str(self, v: &str) -> Result
359 | where
360 | E: de::Error,
361 | {
362 | let default = TorbInput::String(String::new());
363 | let mapping = v.to_string();
364 | let typing = "string".to_string();
365 |
366 | Ok(TorbInputSpec {
367 | typing,
368 | default,
369 | mapping,
370 | })
371 | }
372 |
373 | fn visit_seq(self, mut seq: A) -> Result
374 | where
375 | A: SeqAccess<'de>,
376 | {
377 | let mut count = 0;
378 | let mut typing = String::new();
379 | let mut mapping = String::new();
380 | let mut default = TorbInput::String(String::new());
381 |
382 | if seq.size_hint().is_some() && seq.size_hint() != Some(3) {
383 | return Err(de::Error::custom(format!(
384 | "Didn't find the right sequence of values to create a TorbInputSpec."
385 | )));
386 | }
387 |
388 | while count < 3 {
389 | match count {
390 | 0 => {
391 | let value_opt = seq.next_element::()?;
392 |
393 | let value = if !value_opt.is_some() {
394 | return Err(de::Error::custom(format!(
395 | "Didn't find the right sequence of values to create a TorbInputSpec."
396 | )));
397 | } else {
398 | value_opt.unwrap()
399 | };
400 |
401 | if !TYPES.contains(value.as_str()) {
402 | return Err(de::Error::custom(format!(
403 | "Please set a valid type for your input spec. Valid types are {:#?}. \n If you see this as a regular user, a unit author has included a broken spec.",
404 | TYPES
405 | )));
406 | }
407 |
408 | typing = value;
409 | count += 1;
410 | }
411 | 1 => {
412 | match typing.as_str() {
413 | "bool" => {
414 | let value_opt = seq.next_element::()?;
415 |
416 | let value = if !value_opt.is_some() {
417 | return Err(de::Error::custom(format!(
418 | "Didn't find the right sequence of values to create a TorbInputSpec."
419 | )));
420 | } else {
421 | value_opt.unwrap()
422 | };
423 |
424 | default = TorbInput::Bool(value);
425 | }
426 | "string" => {
427 | let value_opt = seq.next_element::()?;
428 |
429 | let value = if !value_opt.is_some() {
430 | return Err(de::Error::custom(format!(
431 | "Didn't find the right sequence of values to create a TorbInputSpec."
432 | )));
433 | } else {
434 | value_opt.unwrap()
435 | };
436 |
437 | default = TorbInput::String(value);
438 | }
439 | "array" => {
440 | let value = seq.next_element::()?.unwrap();
441 |
442 | let mut new_vec = Vec::::new();
443 |
444 | for ele in value.iter() {
445 | match ele {
446 | serde_yaml::Value::Bool(val) => {
447 | new_vec.push(TorbInput::Bool(val.clone()))
448 | }
449 | serde_yaml::Value::Number(val) => {
450 | let numeric = if val.is_f64() {
451 | TorbNumeric::Float(val.as_f64().unwrap())
452 | } else if val.is_u64() {
453 | TorbNumeric::Int(val.as_u64().unwrap())
454 | } else {
455 | TorbNumeric::NegInt(val.as_i64().unwrap())
456 | };
457 |
458 | new_vec.push(TorbInput::Numeric(numeric))
459 | }
460 | serde_yaml::Value::String(val) => {
461 | new_vec.push(TorbInput::String(val.clone()))
462 | }
463 | _ => panic!("Typing was array, array elements are not a supported type. Supported array types are bool, numeric and string. Nesting is not supported.")
464 | }
465 | }
466 |
467 | default = TorbInput::Array(new_vec);
468 | }
469 | "numeric" => {
470 | let value = seq.next_element::()?.unwrap();
471 | if let serde_yaml::Value::Number(val) = value {
472 | let numeric = if val.is_f64() {
473 | TorbNumeric::Float(val.as_f64().unwrap())
474 | } else if val.is_u64() {
475 | TorbNumeric::Int(val.as_u64().unwrap())
476 | } else {
477 | TorbNumeric::NegInt(val.as_i64().unwrap())
478 | };
479 | default = TorbInput::Numeric(numeric);
480 | } else {
481 | panic!("Typing was numeric, default value was not numeric.")
482 | }
483 |
484 | }
485 | _ => {
486 | panic!("Type not supported by Torb! Supported types are String, Numeric, Array, Bool.")
487 | }
488 | }
489 | count += 1;
490 | }
491 | 2 => {
492 | let value_opt = seq.next_element::()?;
493 |
494 | let value = if !value_opt.is_some() {
495 | return Err(de::Error::custom(format!(
496 | "Didn't find the right sequence of values to create a TorbInputSpec."
497 | )));
498 | } else {
499 | value_opt.unwrap()
500 | };
501 |
502 | mapping = value;
503 | count += 1;
504 | }
505 | _ => {
506 | return Err(de::Error::custom(format!(
507 | "Didn't find the right sequence of values to create a TorbInputSpec."
508 | )));
509 | }
510 | }
511 | }
512 |
513 | let new_obj = TorbInputSpec {
514 | typing,
515 | mapping,
516 | default,
517 | };
518 |
519 | Ok(new_obj)
520 | }
521 | }
522 |
523 | impl<'de> Deserialize<'de> for TorbInputSpec {
524 | fn deserialize(deserializer: D) -> Result
525 | where
526 | D: Deserializer<'de>,
527 | {
528 | deserializer.deserialize_any(TorbInputSpecDeserializer)
529 | }
530 | }
531 |
532 | impl Serialize for TorbInput {
533 | fn serialize(&self, serializer: S) -> Result
534 | where
535 | S: serde::Serializer {
536 |
537 | match self {
538 | TorbInput::Numeric(val) => {
539 | match val {
540 | TorbNumeric::Float(val) => {
541 | serializer.serialize_f64(val.clone())
542 | },
543 | TorbNumeric::Int(val) => {
544 | serializer.serialize_u64(val.clone())
545 | },
546 | TorbNumeric::NegInt(val) => {
547 | serializer.serialize_i64(val.clone())
548 | }
549 | }
550 | },
551 | TorbInput::Array(val) => {
552 | let len = val.len();
553 | let mut seq = serializer.serialize_seq(Some(len))?;
554 |
555 | for input in val.iter().cloned() {
556 | let expr = match input {
557 | TorbInput::String(val) => serde_yaml::Value::String(val),
558 | TorbInput::Bool(val) => serde_yaml::Value::Bool(val),
559 | TorbInput::Numeric(val) => {
560 | match val {
561 | TorbNumeric::Float(val) => serde_yaml::Value::Number(serde_yaml::Number::from(val)),
562 | TorbNumeric::Int(val) => serde_yaml::Value::Number(serde_yaml::Number::from(val)),
563 | TorbNumeric::NegInt(val) => serde_yaml::Value::Number(serde_yaml::Number::from(val))
564 | }
565 | }
566 | TorbInput::Array(_val) => {
567 | panic!("Nested array types are not supported.")
568 | }
569 | };
570 |
571 | seq.serialize_element(&expr)?;
572 | }
573 | seq.end()
574 | },
575 | TorbInput::String(val) => {
576 | serializer.serialize_str(val)
577 | },
578 | TorbInput::Bool(val) => {
579 | serializer.serialize_bool(val.clone())
580 | }
581 | }
582 |
583 | }
584 | }
585 |
586 | impl Serialize for TorbInputSpec {
587 | fn serialize(&self, serializer: S) -> Result
588 | where
589 | S: serde::Serializer {
590 | let mut seq = serializer.serialize_seq(Some(3))?;
591 |
592 | let typing = self.typing.clone();
593 | let default = self.default.clone();
594 | let mapping = self.mapping.clone();
595 |
596 | seq.serialize_element(&typing)?;
597 | seq.serialize_element(&default)?;
598 | seq.serialize_element(&mapping)?;
599 | seq.end()
600 |
601 | }
602 | }
603 |
604 | impl ArtifactNodeRepr {
605 | pub fn display_name(&self, kebab: bool) -> String {
606 | let name = self.mapped_inputs.get("name").map(|(_, input)| {
607 | if let crate::artifacts::TorbInput::String(val) = input.clone() {
608 | val
609 | }
610 | else {
611 | self.name.clone()
612 | }
613 | }).or(Some(self.name.clone())).unwrap();
614 |
615 | if kebab {
616 | snake_case_to_kebab(&name)
617 | } else {
618 | kebab_to_snake_case(&name)
619 | }
620 | }
621 |
622 | #[allow(dead_code)]
623 | pub fn new(
624 | fqn: String,
625 | name: String,
626 | version: String,
627 | kind: String,
628 | lang: Option,
629 | init_step: Option>,
630 | build_step: Option,
631 | deploy_steps: IndexMap>>,
632 | inputs: IndexMap,
633 | input_spec: IndexMap,
634 | outputs: Vec,
635 | file_path: String,
636 | stack_graph: Option,
637 | files: Option>,
638 | values: String,
639 | namespace: Option,
640 | source: Option,
641 | expedient: bool
642 | ) -> ArtifactNodeRepr {
643 | ArtifactNodeRepr {
644 | fqn: fqn,
645 | name: name,
646 | version: version,
647 | kind: kind,
648 | lang: lang,
649 | init_step: init_step,
650 | build_step: build_step,
651 | deploy_steps: deploy_steps,
652 | mapped_inputs: inputs,
653 | input_spec: input_spec,
654 | outputs: outputs,
655 | implicit_dependency_fqns: IndexSet::new(),
656 | dependencies: Vec::new(),
657 | dependency_names: NodeDependencies {
658 | services: None,
659 | projects: None,
660 | stacks: None,
661 | },
662 | file_path,
663 | stack_graph,
664 | files,
665 | values,
666 | namespace,
667 | source,
668 | expedient
669 | }
670 | }
671 |
672 | fn address_to_fqn(
673 | graph_name: &String,
674 | addr_result: Result,
675 | ) -> Option {
676 | match addr_result {
677 | Ok(addr) => {
678 | let fqn = format!(
679 | "{}.{}.{}",
680 | graph_name,
681 | addr.node_type.clone(),
682 | addr.node_name.clone()
683 | );
684 |
685 | Some(fqn)
686 | }
687 | Err(_s) => None,
688 | }
689 | }
690 |
691 | pub fn discover_and_set_implicit_dependencies(
692 | &mut self,
693 | graph_name: &String,
694 | ) -> Result<(), Box> {
695 | let mut implicit_deps_inputs = IndexSet::new();
696 |
697 | let inputs_fn = |_spec: &String, val: Result| -> String {
698 | let fqn_option = ArtifactNodeRepr::address_to_fqn(graph_name, val);
699 |
700 | if fqn_option.is_some() {
701 | let fqn = fqn_option.unwrap();
702 |
703 | if fqn != self.fqn {
704 | implicit_deps_inputs.insert(fqn);
705 | }
706 | };
707 |
708 | "".to_string()
709 | };
710 |
711 | let mut implicit_deps_values = IndexSet::new();
712 |
713 | let values_fn = |addr: Result| -> String {
714 | let fqn_option = ArtifactNodeRepr::address_to_fqn(graph_name, addr);
715 |
716 | if fqn_option.is_some() {
717 | let fqn = fqn_option.unwrap();
718 | if fqn != self.fqn {
719 | implicit_deps_values.insert(fqn);
720 | }
721 | };
722 |
723 | "".to_string()
724 | };
725 |
726 | let (_, _, _) =
727 | InputResolver::resolve(&self, Some(values_fn), Some(inputs_fn), NO_INITS_FN)?;
728 |
729 | let unioned_deps = implicit_deps_inputs.union(&mut implicit_deps_values);
730 |
731 | self.implicit_dependency_fqns = unioned_deps.cloned().collect();
732 |
733 | Ok(())
734 | }
735 |
736 | pub fn validate_map_and_set_inputs(&mut self, inputs: IndexMap) {
737 | if !self.input_spec.is_empty() {
738 | let input_spec = &self.input_spec.clone();
739 |
740 | match ArtifactNodeRepr::validate_inputs(&inputs, &input_spec) {
741 | Ok(_) => {
742 | self.mapped_inputs = ArtifactNodeRepr::map_inputs(&inputs, &input_spec);
743 | }
744 | Err(e) => panic!(
745 | "Input validation failed: {} is not a valid key. Valid Keys: {}",
746 | e,
747 | input_spec
748 | .keys()
749 | .into_iter()
750 | .map(AsRef::as_ref)
751 | .collect::>()
752 | .join(", ")
753 | ),
754 | }
755 | } else {
756 | if !inputs.is_empty() {
757 | println!(
758 | "Warning: {} has inputs but no input spec, passing empty values.",
759 | &self.fqn
760 | );
761 | }
762 |
763 | self.mapped_inputs = IndexMap::::new();
764 | }
765 | }
766 |
767 | fn validate_inputs(
768 | inputs: &IndexMap,
769 | spec: &IndexMap,
770 | ) -> Result<(), String> {
771 | for (key, val) in inputs.iter() {
772 | if !spec.contains_key(key) {
773 | return Err(key.clone());
774 | }
775 |
776 | let input_spec = spec.get(key).unwrap();
777 |
778 | let val_type = match val {
779 | TorbInput::String(val) => match InputAddress::try_from(val.as_str()) {
780 | Ok(_) => "input_address",
781 | _ => "string",
782 | },
783 | TorbInput::Bool(_val) => "bool",
784 | TorbInput::Numeric(_val) => "numeric",
785 | TorbInput::Array(_val) => "array",
786 | };
787 |
788 | if val_type != "input_address" && input_spec.typing != val_type {
789 | return Err(format!(
790 | "{key} is type {val_type} but is supposed to be {}",
791 | input_spec.typing
792 | ));
793 | }
794 | }
795 |
796 | Ok(())
797 | }
798 |
799 | fn map_inputs(
800 | inputs: &IndexMap,
801 | spec: &IndexMap,
802 | ) -> IndexMap {
803 | let mut mapped_inputs = IndexMap::::new();
804 |
805 | for (key, value) in spec.iter() {
806 | let input = inputs.get(key).unwrap_or(&value.default);
807 | mapped_inputs.insert(key.to_string(), (value.mapping.clone(), input.clone()));
808 | }
809 |
810 | mapped_inputs
811 | }
812 | }
813 |
814 | #[derive(Serialize, Deserialize, Clone)]
815 | pub struct ArtifactRepr {
816 | pub torb_version: String,
817 | pub helm_version: String,
818 | pub terraform_version: String,
819 | pub commits: IndexMap,
820 | pub stack_name: String,
821 | pub meta: Box