├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── conditions ├── k8s.rs └── mod.rs ├── install ├── config.rs ├── container │ ├── args.rs │ ├── env.rs │ ├── mod.rs │ ├── port.rs │ └── volumes.rs ├── delete.rs ├── meta.rs ├── mod.rs ├── resources.rs └── value.rs ├── lib.rs ├── process.rs ├── selectors.rs ├── tracker.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | 3 | /target 4 | Cargo.lock 5 | 6 | # Eclipse 7 | 8 | .project 9 | .settings/ 10 | 11 | # IntelliJ 12 | 13 | .idea/ 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "operator-framework" 3 | version = "0.7.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | keywords = ["kubernetes", "operator"] 7 | license = "EPL-2.0" 8 | description = "Tools and helpers used to create Kubernetes operators" 9 | repository = "https://github.com/ctron/operator-framework" 10 | 11 | [dependencies] 12 | 13 | anyhow = "1.0" 14 | async-trait = "0.1" 15 | chrono = "0.4" 16 | either = "1.6" 17 | futures = "0.3" 18 | k8s-openapi = { version = "0.16" } 19 | kube = { version = "0.75", features = ["derive"] } 20 | log = "0.4" 21 | schemars = { version = "0.8", optional = true } 22 | serde = "1.0" 23 | serde_derive = "1.0" 24 | serde_json = "1.0" 25 | sha1 = "0.10" 26 | 27 | [dev-dependencies] 28 | 29 | k8s-openapi = { version = "0.16", features = ["v1_21"] } 30 | 31 | [features] 32 | 33 | default = [] 34 | 35 | # choose k8s API version, align with k8s-openapi features 36 | v1_18 = ["k8s-openapi/v1_18"] 37 | v1_19 = ["k8s-openapi/v1_19"] 38 | v1_20 = ["k8s-openapi/v1_20"] 39 | v1_21 = ["k8s-openapi/v1_21"] 40 | v1_22 = ["k8s-openapi/v1_22"] 41 | v1_23 = ["k8s-openapi/v1_23"] 42 | 43 | schemas = ["schemars", "k8s-openapi/schemars"] 44 | 45 | [patch.crates-io] 46 | #kube = { path = "../kube-rs/kube" } 47 | #kube = { git = "https://github.com/ctron/kube-rs", rev = "59f175adc61575b83c01fc8809ea70cb7c172ebb" } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Operator Framework for Rust 2 | 3 | This is a set of tools and helpers to allow creating Kubernetes 4 | Operators in Rust. 5 | 6 | The goal of this project is provide some syntactic sugar, making it easier to work with certain types. 7 | 8 | Contributions are welcome! -------------------------------------------------------------------------------- /src/conditions/k8s.rs: -------------------------------------------------------------------------------- 1 | k8s_openapi::k8s_if_ge_1_20! { 2 | crate::condition!(k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition[core]); 3 | } 4 | 5 | crate::condition!(k8s_openapi::api::batch::v1::JobCondition[probe]); 6 | crate::condition!(k8s_openapi::api::apps::v1::StatefulSetCondition); 7 | -------------------------------------------------------------------------------- /src/conditions/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | mod k8s; 15 | 16 | pub use k8s::*; 17 | 18 | use crate::utils::UseOrCreate; 19 | use chrono::{DateTime, Utc}; 20 | use std::fmt::{Display, Formatter}; 21 | 22 | /// The state of the condition. 23 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 24 | pub enum State { 25 | True, 26 | False, 27 | Unknown, 28 | } 29 | 30 | impl Display for State { 31 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 32 | match self { 33 | Self::True => write!(f, "True"), 34 | Self::False => write!(f, "False"), 35 | Self::Unknown => write!(f, "Unknown"), 36 | } 37 | } 38 | } 39 | 40 | impl From for StateDetails { 41 | fn from(state: State) -> Self { 42 | Self { 43 | state, 44 | reason: None, 45 | message: None, 46 | observed_generation: None, 47 | } 48 | } 49 | } 50 | 51 | /// Help building a condition state. 52 | pub trait StateBuilder: Sized { 53 | fn with_reason(self, reason: S) -> StateDetails 54 | where 55 | S: Into, 56 | { 57 | self.with_reason_opt(Some(reason.into())) 58 | } 59 | 60 | fn with_message(self, message: S) -> StateDetails 61 | where 62 | S: Into, 63 | { 64 | self.with_message_opt(Some(message.into())) 65 | } 66 | 67 | fn with_reason_opt(self, reason: S) -> StateDetails 68 | where 69 | S: Into>; 70 | 71 | fn with_message_opt(self, message: S) -> StateDetails 72 | where 73 | S: Into>; 74 | 75 | fn with_observed(self, observed: G) -> StateDetails 76 | where 77 | G: Into>; 78 | } 79 | 80 | /// Details of the condition state. 81 | #[derive(Clone, Debug, Eq, PartialEq)] 82 | pub struct StateDetails { 83 | pub state: State, 84 | pub reason: Option, 85 | pub message: Option, 86 | pub observed_generation: Option, 87 | } 88 | 89 | impl StateBuilder for State { 90 | fn with_reason_opt(self, reason: S) -> StateDetails 91 | where 92 | S: Into>, 93 | { 94 | StateDetails::from(self).with_reason_opt(reason) 95 | } 96 | 97 | fn with_message_opt(self, message: S) -> StateDetails 98 | where 99 | S: Into>, 100 | { 101 | StateDetails::from(self).with_message_opt(message) 102 | } 103 | 104 | fn with_observed(self, observed: G) -> StateDetails 105 | where 106 | G: Into>, 107 | { 108 | StateDetails::from(self).with_observed(observed) 109 | } 110 | } 111 | 112 | impl StateBuilder for StateDetails { 113 | fn with_reason_opt(mut self, reason: S) -> StateDetails 114 | where 115 | S: Into>, 116 | { 117 | self.reason = reason.into(); 118 | self 119 | } 120 | 121 | fn with_message_opt(mut self, message: S) -> StateDetails 122 | where 123 | S: Into>, 124 | { 125 | self.message = message.into(); 126 | self 127 | } 128 | 129 | fn with_observed(mut self, observed: G) -> StateDetails 130 | where 131 | G: Into>, 132 | { 133 | self.observed_generation = observed.into(); 134 | self 135 | } 136 | } 137 | 138 | /// Trait to allow universal access to the conditions 139 | pub trait Condition { 140 | fn state(&self) -> State; 141 | fn set_state(&mut self, state: State); 142 | 143 | fn r#type(&self) -> &str; 144 | 145 | fn message(&self) -> Option<&str>; 146 | fn set_message(&mut self, message: S) 147 | where 148 | S: Into>; 149 | 150 | fn reason(&self) -> Option<&str>; 151 | fn set_reason(&mut self, reason: S) 152 | where 153 | S: Into>; 154 | 155 | fn last_probe_time(&self) -> Option>; 156 | fn set_last_probe_time(&mut self, time: T) 157 | where 158 | T: Into>>; 159 | 160 | fn last_transition_time(&self) -> Option>; 161 | fn set_last_transition_time(&mut self, time: T) 162 | where 163 | T: Into>>; 164 | 165 | fn observed_generation(&self) -> Option; 166 | fn set_observed_generation(&mut self, observed_generation: S) 167 | where 168 | S: Into>; 169 | 170 | fn from( 171 | r#type: String, 172 | state: State, 173 | reason: Option, 174 | message: Option, 175 | observed_generation: Option, 176 | now: DateTime, 177 | ) -> Self; 178 | } 179 | 180 | #[macro_export] 181 | macro_rules! condition { 182 | ($n:ty) => { 183 | impl $crate::conditions::Condition for $n { 184 | fn from( 185 | r#type: String, 186 | state: $crate::conditions::State, 187 | reason: Option, 188 | message: Option, 189 | _observed_generation: Option, 190 | now: chrono::DateTime, 191 | ) -> Self { 192 | let now = k8s_openapi::apimachinery::pkg::apis::meta::v1::Time(now); 193 | Self { 194 | last_transition_time: Some(now), 195 | reason, 196 | message, 197 | status: state.to_string(), 198 | type_: r#type, 199 | } 200 | } 201 | 202 | fn last_probe_time(&self) -> Option> { 203 | None 204 | } 205 | 206 | fn set_last_probe_time(&mut self, _: T) 207 | where 208 | T: Into>>, 209 | { 210 | } 211 | 212 | $crate::condition!($n[common]); 213 | $crate::condition!($n[opt]); 214 | } 215 | }; 216 | ($n:ty[core]) => { 217 | impl $crate::conditions::Condition for $n { 218 | fn from( 219 | r#type: String, 220 | state: $crate::conditions::State, 221 | reason: Option, 222 | message: Option, 223 | observed_generation: Option, 224 | now: chrono::DateTime, 225 | ) -> Self { 226 | let now = k8s_openapi::apimachinery::pkg::apis::meta::v1::Time(now); 227 | Self { 228 | last_transition_time: now, 229 | reason: reason.unwrap_or_default(), 230 | message: message.unwrap_or_default(), 231 | observed_generation, 232 | status: state.to_string(), 233 | type_: r#type, 234 | } 235 | } 236 | 237 | fn last_probe_time(&self) -> Option> { 238 | None 239 | } 240 | 241 | fn set_last_probe_time(&mut self, _: T) 242 | where 243 | T: Into>>, 244 | { 245 | } 246 | 247 | $crate::condition!($n[common]); 248 | $crate::condition!($n[non_opt]); 249 | } 250 | }; 251 | ($n:ty [probe]) => { 252 | impl $crate::conditions::Condition for $n { 253 | fn from( 254 | r#type: String, 255 | state: $crate::conditions::State, 256 | reason: Option, 257 | message: Option, 258 | _observed_generation: Option, 259 | now: chrono::DateTime, 260 | ) -> Self { 261 | let now = k8s_openapi::apimachinery::pkg::apis::meta::v1::Time(now); 262 | Self { 263 | last_probe_time: Some(now.clone()), 264 | last_transition_time: Some(now), 265 | reason, 266 | message, 267 | status: state.to_string(), 268 | type_: r#type, 269 | } 270 | } 271 | 272 | fn last_probe_time(&self) -> Option> { 273 | self.last_probe_time.as_ref().map(|t| t.0) 274 | } 275 | 276 | fn set_last_probe_time(&mut self, time: T) 277 | where 278 | T: Into>>, 279 | { 280 | self.last_probe_time = time 281 | .into() 282 | .map(k8s_openapi::apimachinery::pkg::apis::meta::v1::Time); 283 | } 284 | 285 | $crate::condition!($n[common]); 286 | $crate::condition!($n[opt]); 287 | } 288 | }; 289 | 290 | ($n:ty [common]) => { 291 | fn state(&self) -> $crate::conditions::State { 292 | use std::ops::Deref; 293 | match self.status.deref() { 294 | "True" => $crate::conditions::State::True, 295 | "False" => $crate::conditions::State::False, 296 | _ => $crate::conditions::State::Unknown, 297 | } 298 | } 299 | 300 | fn set_state(&mut self, state: $crate::conditions::State) { 301 | self.status = state.to_string(); 302 | } 303 | 304 | fn r#type(&self) -> &str { 305 | &self.type_ 306 | } 307 | }; 308 | ($n:ty [non_opt]) => { 309 | fn message(&self) -> Option<&str> { 310 | Some(&self.message) 311 | } 312 | 313 | fn set_message(&mut self, message: S) 314 | where 315 | S: Into>, 316 | { 317 | self.message = message.into().unwrap_or_default(); 318 | } 319 | 320 | fn reason(&self) -> Option<&str> { 321 | Some(&self.reason) 322 | } 323 | 324 | fn set_reason(&mut self, reason: S) 325 | where 326 | S: Into>, 327 | { 328 | self.reason = reason.into().unwrap_or_default(); 329 | } 330 | 331 | fn last_transition_time(&self) -> Option> { 332 | Some(self.last_transition_time.0) 333 | } 334 | 335 | fn set_last_transition_time(&mut self, time: T) 336 | where 337 | T: Into>>, 338 | { 339 | self.last_transition_time = k8s_openapi::apimachinery::pkg::apis::meta::v1::Time( 340 | time.into().unwrap_or_else(|| chrono::Utc::now()), 341 | ); 342 | } 343 | 344 | fn observed_generation(&self) -> Option { 345 | self.observed_generation 346 | } 347 | fn set_observed_generation(&mut self, observed_generation: S) 348 | where 349 | S: Into>, 350 | { 351 | self.observed_generation = observed_generation.into(); 352 | } 353 | }; 354 | ($n:ty [opt]) => { 355 | fn message(&self) -> Option<&str> { 356 | self.message.as_deref() 357 | } 358 | 359 | fn set_message(&mut self, message: S) 360 | where 361 | S: Into>, 362 | { 363 | self.message = message.into(); 364 | } 365 | 366 | fn reason(&self) -> Option<&str> { 367 | self.reason.as_deref() 368 | } 369 | 370 | fn set_reason(&mut self, reason: S) 371 | where 372 | S: Into>, 373 | { 374 | self.reason = reason.into(); 375 | } 376 | fn last_transition_time(&self) -> Option> { 377 | self.last_transition_time.as_ref().map(|t| t.0) 378 | } 379 | 380 | fn set_last_transition_time(&mut self, time: T) 381 | where 382 | T: Into>>, 383 | { 384 | self.last_transition_time = time 385 | .into() 386 | .map(k8s_openapi::apimachinery::pkg::apis::meta::v1::Time); 387 | } 388 | 389 | fn observed_generation(&self) -> Option { 390 | None 391 | } 392 | 393 | fn set_observed_generation(&mut self, _: S) 394 | where 395 | S: Into>, 396 | { 397 | } 398 | }; 399 | } 400 | 401 | pub trait Conditions { 402 | fn update_condition(&mut self, r#type: S, state: D) 403 | where 404 | S: AsRef, 405 | D: Into, 406 | { 407 | self.update_condition_on(r#type, state, Utc::now()) 408 | } 409 | 410 | fn update_condition_on(&mut self, r#type: S, state: D, now: DT) 411 | where 412 | S: AsRef, 413 | D: Into, 414 | DT: Into>; 415 | } 416 | 417 | impl Conditions for Option> 418 | where 419 | C: Condition, 420 | { 421 | fn update_condition_on(&mut self, r#type: S, state: D, now: DT) 422 | where 423 | S: AsRef, 424 | D: Into, 425 | DT: Into>, 426 | { 427 | self.use_or_create(|conditions| conditions.update_condition_on(r#type, state, now)); 428 | } 429 | } 430 | 431 | impl Conditions for Vec 432 | where 433 | C: Condition, 434 | { 435 | fn update_condition_on(&mut self, r#type: S, state: D, now: DT) 436 | where 437 | S: AsRef, 438 | D: Into, 439 | DT: Into>, 440 | { 441 | let info = state.into(); 442 | let now = now.into(); 443 | 444 | for condition in self.into_iter() { 445 | if condition.r#type() == r#type.as_ref() { 446 | if condition.state() != info.state { 447 | condition.set_last_transition_time(now); 448 | condition.set_state(info.state); 449 | } 450 | condition.set_last_probe_time(now); 451 | condition.set_reason(info.reason); 452 | condition.set_message(info.message); 453 | condition.set_observed_generation(info.observed_generation); 454 | 455 | return; 456 | } 457 | } 458 | 459 | // did not find entry so far 460 | 461 | self.push(C::from( 462 | r#type.as_ref().to_string(), 463 | info.state, 464 | info.reason, 465 | info.message, 466 | info.observed_generation, 467 | now, 468 | )); 469 | } 470 | } 471 | 472 | #[cfg(test)] 473 | mod test { 474 | 475 | use super::*; 476 | use crate::utils::UseOrCreate; 477 | use k8s_openapi::api::batch::v1::*; 478 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::Time; 479 | 480 | #[test] 481 | fn test_basic() { 482 | let mut job = Job { 483 | ..Default::default() 484 | }; 485 | 486 | job.status.use_or_create(|status| { 487 | assert_eq!(status.conditions, None); 488 | 489 | // initial update 490 | 491 | let now = Utc::now(); 492 | 493 | status 494 | .conditions 495 | .update_condition_on("Ready", State::True, now); 496 | 497 | assert_eq!( 498 | status.conditions, 499 | Some(vec![JobCondition { 500 | type_: "Ready".into(), 501 | status: "True".into(), 502 | last_probe_time: Some(Time(now)), 503 | last_transition_time: Some(Time(now)), 504 | ..Default::default() 505 | }]) 506 | ); 507 | 508 | // second update, no change 509 | 510 | let now_2 = Utc::now(); 511 | 512 | status 513 | .conditions 514 | .update_condition_on("Ready", State::True, now_2); 515 | 516 | assert_eq!( 517 | status.conditions, 518 | Some(vec![JobCondition { 519 | type_: "Ready".into(), 520 | status: "True".into(), 521 | last_probe_time: Some(Time(now_2)), 522 | last_transition_time: Some(Time(now)), 523 | ..Default::default() 524 | }]) 525 | ); 526 | 527 | // third update, change 528 | 529 | let now = Utc::now(); 530 | 531 | status.conditions.update_condition_on( 532 | "Ready", 533 | State::False 534 | .with_reason("NotReady") 535 | .with_message("Something failed"), 536 | now, 537 | ); 538 | 539 | assert_eq!( 540 | status.conditions, 541 | Some(vec![JobCondition { 542 | type_: "Ready".into(), 543 | status: "False".into(), 544 | reason: Some("NotReady".into()), 545 | message: Some("Something failed".into()), 546 | last_probe_time: Some(Time(now)), 547 | last_transition_time: Some(Time(now)), 548 | ..Default::default() 549 | }]) 550 | ); 551 | 552 | // fourth update, back to ok 553 | 554 | let now = Utc::now(); 555 | 556 | status 557 | .conditions 558 | .update_condition_on("Ready", State::True, now); 559 | 560 | assert_eq!( 561 | status.conditions, 562 | Some(vec![JobCondition { 563 | type_: "Ready".into(), 564 | status: "True".into(), 565 | last_probe_time: Some(Time(now)), 566 | last_transition_time: Some(Time(now)), 567 | ..Default::default() 568 | }]) 569 | ); 570 | }); 571 | } 572 | #[test] 573 | fn test_multi() { 574 | let mut job = Job { 575 | ..Default::default() 576 | }; 577 | 578 | job.status.use_or_create(|status| { 579 | assert_eq!(status.conditions, None); 580 | 581 | // initial update 582 | 583 | let now = Utc::now(); 584 | 585 | status 586 | .conditions 587 | .update_condition_on("Ready", State::True, now); 588 | status 589 | .conditions 590 | .update_condition_on("Foo", State::True, now); 591 | status 592 | .conditions 593 | .update_condition_on("Bar", State::True, now); 594 | 595 | assert_eq!( 596 | status.conditions, 597 | Some(vec![ 598 | JobCondition { 599 | type_: "Ready".into(), 600 | status: "True".into(), 601 | last_probe_time: Some(Time(now)), 602 | last_transition_time: Some(Time(now)), 603 | ..Default::default() 604 | }, 605 | JobCondition { 606 | type_: "Foo".into(), 607 | status: "True".into(), 608 | last_probe_time: Some(Time(now)), 609 | last_transition_time: Some(Time(now)), 610 | ..Default::default() 611 | }, 612 | JobCondition { 613 | type_: "Bar".into(), 614 | status: "True".into(), 615 | last_probe_time: Some(Time(now)), 616 | last_transition_time: Some(Time(now)), 617 | ..Default::default() 618 | } 619 | ]) 620 | ); 621 | 622 | let now_2 = Utc::now(); 623 | 624 | status 625 | .conditions 626 | .update_condition_on("Foo", State::False, now_2); 627 | 628 | assert_eq!( 629 | status.conditions, 630 | Some(vec![ 631 | JobCondition { 632 | type_: "Ready".into(), 633 | status: "True".into(), 634 | last_probe_time: Some(Time(now)), 635 | last_transition_time: Some(Time(now)), 636 | ..Default::default() 637 | }, 638 | JobCondition { 639 | type_: "Foo".into(), 640 | status: "False".into(), 641 | last_probe_time: Some(Time(now_2)), 642 | last_transition_time: Some(Time(now_2)), 643 | ..Default::default() 644 | }, 645 | JobCondition { 646 | type_: "Bar".into(), 647 | status: "True".into(), 648 | last_probe_time: Some(Time(now)), 649 | last_transition_time: Some(Time(now)), 650 | ..Default::default() 651 | } 652 | ]) 653 | ); 654 | }); 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /src/install/config.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use crate::utils::UseOrCreate; 14 | use k8s_openapi::{ 15 | api::core::v1::{ConfigMap, Secret}, 16 | ByteString, 17 | }; 18 | 19 | pub trait AppendString { 20 | fn insert_string(&mut self, key: S, keep_existing: bool, provider: P) 21 | where 22 | S: ToString, 23 | P: FnOnce() -> T; 24 | 25 | fn append_string(&mut self, key: S, value: T) 26 | where 27 | S: ToString, 28 | { 29 | self.insert_string(key, false, || value); 30 | } 31 | 32 | fn init_string_from(&mut self, key: S, provider: P) 33 | where 34 | S: ToString, 35 | P: FnOnce() -> T, 36 | { 37 | self.insert_string(key, true, provider); 38 | } 39 | 40 | fn init_string(&mut self, key: S, value: T) 41 | where 42 | S: ToString, 43 | { 44 | self.insert_string(key, true, || value); 45 | } 46 | } 47 | 48 | impl> AppendString for Secret { 49 | fn insert_string(&mut self, key: S, keep_existing: bool, provider: P) 50 | where 51 | S: ToString, 52 | P: FnOnce() -> T, 53 | { 54 | self.data.use_or_create(|data| { 55 | if keep_existing { 56 | let entry = data.entry(key.to_string()); 57 | entry.or_insert(ByteString(provider().into().into_bytes())); 58 | } else { 59 | data.insert(key.to_string(), ByteString(provider().into().into_bytes())); 60 | } 61 | }) 62 | } 63 | } 64 | 65 | impl> AppendString for ConfigMap { 66 | fn insert_string(&mut self, key: S, keep_existing: bool, provider: P) 67 | where 68 | S: ToString, 69 | P: FnOnce() -> T, 70 | { 71 | self.data.use_or_create(|data| { 72 | if keep_existing { 73 | let entry = data.entry(key.to_string()); 74 | entry.or_insert(provider().into()); 75 | } else { 76 | data.insert(key.to_string(), provider().into()); 77 | } 78 | }); 79 | } 80 | } 81 | 82 | pub trait AppendBinary { 83 | fn insert_binary(&mut self, key: S, keep_existing: bool, provider: P) 84 | where 85 | S: ToString, 86 | P: FnOnce() -> T; 87 | 88 | fn append_binary(&mut self, key: S, value: T) 89 | where 90 | S: ToString, 91 | { 92 | self.insert_binary(key, false, || value); 93 | } 94 | 95 | fn init_binary_from(&mut self, key: S, provider: P) 96 | where 97 | S: ToString, 98 | P: FnOnce() -> T, 99 | { 100 | self.insert_binary(key, true, provider); 101 | } 102 | 103 | fn init_binary(&mut self, key: S, value: T) 104 | where 105 | S: ToString, 106 | { 107 | self.insert_binary(key, true, || value); 108 | } 109 | } 110 | 111 | impl>> AppendBinary for Secret { 112 | fn insert_binary(&mut self, key: S, keep_existing: bool, provider: P) 113 | where 114 | S: ToString, 115 | P: FnOnce() -> T, 116 | { 117 | self.data.use_or_create(|data| { 118 | if keep_existing { 119 | let entry = data.entry(key.to_string()); 120 | entry.or_insert(ByteString(provider().into())); 121 | } else { 122 | data.insert(key.to_string(), ByteString(provider().into())); 123 | } 124 | }) 125 | } 126 | } 127 | 128 | impl>> AppendBinary for ConfigMap { 129 | fn insert_binary(&mut self, key: S, keep_existing: bool, provider: P) 130 | where 131 | S: ToString, 132 | P: FnOnce() -> T, 133 | { 134 | self.binary_data.use_or_create(|binary_data| { 135 | if keep_existing { 136 | let entry = binary_data.entry(key.to_string()); 137 | entry.or_insert(ByteString(provider().into())); 138 | } else { 139 | binary_data.insert(key.to_string(), ByteString(provider().into())); 140 | } 141 | }); 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | 148 | use super::*; 149 | use std::collections::BTreeMap; 150 | 151 | #[test] 152 | fn test_cm_string_append() { 153 | let mut cm: ConfigMap = Default::default(); 154 | cm.append_string("foo", "bar"); 155 | 156 | let mut expected = BTreeMap::new(); 157 | expected.insert("foo".into(), ByteString("bar".into())); 158 | assert_eq!(cm.binary_data.clone().unwrap_or_default(), expected); 159 | 160 | cm.append_string("foo", "bar2"); 161 | let mut expected = BTreeMap::new(); 162 | expected.insert("foo".into(), ByteString("bar2".into())); 163 | assert_eq!(cm.binary_data.clone().unwrap_or_default(), expected); 164 | } 165 | 166 | #[test] 167 | fn test_cm_string_init() { 168 | let mut cm: ConfigMap = Default::default(); 169 | cm.append_string("foo", "bar"); 170 | 171 | let mut expected = BTreeMap::new(); 172 | expected.insert("foo".into(), ByteString("bar".into())); 173 | assert_eq!(cm.binary_data.clone().unwrap_or_default(), expected.clone()); 174 | 175 | cm.init_string("foo", "bar2"); 176 | assert_eq!(cm.binary_data.clone().unwrap_or_default(), expected); 177 | } 178 | 179 | #[test] 180 | fn test_secret_string() { 181 | let mut secret: Secret = Default::default(); 182 | secret.append_string("foo", "bar"); 183 | } 184 | 185 | #[test] 186 | fn test_cm_binary() { 187 | let mut cm: ConfigMap = Default::default(); 188 | let data = [1u8, 2u8, 3u8]; 189 | cm.append_binary("foo", &data[..]); 190 | } 191 | 192 | #[test] 193 | fn test_cm_bigger_binary() { 194 | let mut cm: ConfigMap = Default::default(); 195 | let data = [0u8; 100]; 196 | cm.append_binary("foo", &data[..]); 197 | } 198 | 199 | #[test] 200 | fn test_secret_binary() { 201 | let mut secret: Secret = Default::default(); 202 | let data = [1u8, 2u8, 3u8]; 203 | secret.append_binary("foo", data.clone()); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/install/container/args.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | use k8s_openapi::api::core::v1::Container; 15 | 16 | pub trait SetArgs 17 | where 18 | T: Into, 19 | I: IntoIterator, 20 | { 21 | fn args(&mut self, args: I); 22 | } 23 | 24 | impl SetArgs for Container 25 | where 26 | T: Into, 27 | I: IntoIterator, 28 | { 29 | fn args(&mut self, args: I) { 30 | self.args = Some(args.into_iter().map(|s| s.into()).collect()); 31 | } 32 | } 33 | 34 | pub trait SetCommand 35 | where 36 | T: Into, 37 | I: IntoIterator, 38 | { 39 | fn command(&mut self, args: I); 40 | } 41 | 42 | impl SetCommand for Container 43 | where 44 | T: Into, 45 | I: IntoIterator, 46 | { 47 | fn command(&mut self, command: I) { 48 | self.command = Some(command.into_iter().map(|s| s.into()).collect()); 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod test { 54 | 55 | use super::*; 56 | 57 | #[test] 58 | fn test() { 59 | let mut container = Container { 60 | ..Default::default() 61 | }; 62 | container.command(vec!["foo", "bar"]); 63 | container.args(vec!["foo", "bar"]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/install/container/env.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | use crate::utils::UseOrCreate; 15 | use anyhow::Result; 16 | use k8s_openapi::api::core::v1::{ 17 | ConfigMapKeySelector, Container, EnvVar, EnvVarSource, ObjectFieldSelector, 18 | ResourceFieldSelector, SecretKeySelector, 19 | }; 20 | 21 | pub trait ApplyEnvironmentVariable { 22 | /// Apply the mutator function to the environment variable with the provided name. 23 | /// 24 | /// If there currently exists no environment variable with this name, a new entry is created. 25 | /// 26 | /// The function may only throw an error if the mutator threw an error. 27 | fn apply_env(&mut self, name: S, mutator: F) -> Result<()> 28 | where 29 | F: FnOnce(&mut EnvVar) -> Result<()>, 30 | S: AsRef; 31 | 32 | /// Drop an environment variable with the provided name. 33 | /// 34 | /// If no entry with that name exists, this is a no-op. 35 | fn drop_env(&mut self, name: S) 36 | where 37 | S: AsRef; 38 | 39 | fn add_env(&mut self, name: S1, value: S2) -> Result<()> 40 | where 41 | S1: AsRef, 42 | S2: Into, 43 | { 44 | self.apply_env(name, |env| { 45 | env.value = Some(value.into()); 46 | env.value_from = None; 47 | Ok(()) 48 | }) 49 | } 50 | 51 | fn set_env(&mut self, name: S1, value: Option) -> Result<()> 52 | where 53 | S1: AsRef, 54 | S2: Into, 55 | { 56 | match value { 57 | Some(value) => self.add_env(name, value), 58 | None => { 59 | self.drop_env(name); 60 | Ok(()) 61 | } 62 | } 63 | } 64 | 65 | fn add_env_from(&mut self, name: S, from: EnvVarSource) -> Result<()> 66 | where 67 | S: AsRef, 68 | { 69 | self.apply_env(name, |env| { 70 | env.value = None; 71 | env.value_from = Some(from); 72 | Ok(()) 73 | }) 74 | } 75 | 76 | fn set_env_from(&mut self, name: S, from: Option) -> Result<()> 77 | where 78 | S: AsRef, 79 | { 80 | match from { 81 | Some(from) => self.add_env_from(name, from), 82 | None => { 83 | self.drop_env(name); 84 | Ok(()) 85 | } 86 | } 87 | } 88 | 89 | fn add_env_from_field(&mut self, name: S, selector: ObjectFieldSelector) -> Result<()> 90 | where 91 | S: AsRef, 92 | { 93 | self.add_env_from( 94 | name, 95 | EnvVarSource { 96 | field_ref: Some(selector), 97 | ..Default::default() 98 | }, 99 | ) 100 | } 101 | 102 | fn set_env_from_field(&mut self, name: S, from: Option) -> Result<()> 103 | where 104 | S: AsRef, 105 | { 106 | match from { 107 | Some(from) => self.add_env_from_field(name, from), 108 | None => { 109 | self.drop_env(name); 110 | Ok(()) 111 | } 112 | } 113 | } 114 | 115 | fn add_env_from_field_path(&mut self, name: S1, path: S2) -> Result<()> 116 | where 117 | S1: AsRef, 118 | S2: Into, 119 | { 120 | self.add_env_from_field( 121 | name, 122 | ObjectFieldSelector { 123 | api_version: None, 124 | field_path: path.into(), 125 | }, 126 | ) 127 | } 128 | 129 | fn set_env_from_field_path(&mut self, name: S1, path: Option) -> Result<()> 130 | where 131 | S1: AsRef, 132 | S2: Into, 133 | { 134 | match path { 135 | Some(path) => self.add_env_from_field_path(name, path), 136 | None => { 137 | self.drop_env(name); 138 | Ok(()) 139 | } 140 | } 141 | } 142 | 143 | fn add_env_from_secret_selector( 144 | &mut self, 145 | name: S, 146 | selector: SecretKeySelector, 147 | ) -> Result<()> 148 | where 149 | S: AsRef, 150 | { 151 | self.add_env_from( 152 | name, 153 | EnvVarSource { 154 | secret_key_ref: Some(selector), 155 | ..Default::default() 156 | }, 157 | ) 158 | } 159 | 160 | fn set_env_from_secret_select( 161 | &mut self, 162 | name: S, 163 | from: Option, 164 | ) -> Result<()> 165 | where 166 | S: AsRef, 167 | { 168 | match from { 169 | Some(from) => self.add_env_from_secret_selector(name, from), 170 | None => { 171 | self.drop_env(name); 172 | Ok(()) 173 | } 174 | } 175 | } 176 | 177 | fn add_env_from_secret(&mut self, name: S1, secret_name: S2, key: S3) -> Result<()> 178 | where 179 | S1: AsRef, 180 | S2: ToString, 181 | S3: ToString, 182 | { 183 | self.add_env_from_secret_selector( 184 | name, 185 | SecretKeySelector { 186 | name: Some(secret_name.to_string()), 187 | key: key.to_string(), 188 | optional: None, 189 | }, 190 | ) 191 | } 192 | 193 | fn add_env_from_configmap_selector( 194 | &mut self, 195 | name: S, 196 | selector: ConfigMapKeySelector, 197 | ) -> Result<()> 198 | where 199 | S: AsRef, 200 | { 201 | self.add_env_from( 202 | name, 203 | EnvVarSource { 204 | config_map_key_ref: Some(selector), 205 | ..Default::default() 206 | }, 207 | ) 208 | } 209 | 210 | fn set_env_from_configmap_selector( 211 | &mut self, 212 | name: S, 213 | from: Option, 214 | ) -> Result<()> 215 | where 216 | S: AsRef, 217 | { 218 | match from { 219 | Some(from) => self.add_env_from_configmap_selector(name, from), 220 | None => { 221 | self.drop_env(name); 222 | Ok(()) 223 | } 224 | } 225 | } 226 | 227 | fn add_env_from_resource(&mut self, name: S, selector: ResourceFieldSelector) -> Result<()> 228 | where 229 | S: AsRef, 230 | { 231 | self.add_env_from( 232 | name, 233 | EnvVarSource { 234 | resource_field_ref: Some(selector), 235 | ..Default::default() 236 | }, 237 | ) 238 | } 239 | 240 | fn set_env_from_resource_selector( 241 | &mut self, 242 | name: S, 243 | from: Option, 244 | ) -> Result<()> 245 | where 246 | S: AsRef, 247 | { 248 | match from { 249 | Some(from) => self.add_env_from_resource(name, from), 250 | None => { 251 | self.drop_env(name); 252 | Ok(()) 253 | } 254 | } 255 | } 256 | } 257 | 258 | impl ApplyEnvironmentVariable for Vec { 259 | fn apply_env(&mut self, name: S, mutator: F) -> Result<()> 260 | where 261 | F: FnOnce(&mut EnvVar) -> Result<()>, 262 | S: AsRef, 263 | { 264 | let c = self.iter_mut().find(|c| c.name == name.as_ref()); 265 | match c { 266 | Some(c) => { 267 | mutator(c)?; 268 | } 269 | None => { 270 | let mut entry: EnvVar = Default::default(); 271 | entry.name = name.as_ref().to_string(); 272 | mutator(&mut entry)?; 273 | self.push(entry); 274 | } 275 | } 276 | Ok(()) 277 | } 278 | 279 | fn drop_env(&mut self, name: S) 280 | where 281 | S: AsRef, 282 | { 283 | self.retain(|env| env.name != name.as_ref()); 284 | } 285 | } 286 | 287 | impl ApplyEnvironmentVariable for Container { 288 | fn apply_env(&mut self, name: S, mutator: F) -> Result<()> 289 | where 290 | F: FnOnce(&mut EnvVar) -> Result<()>, 291 | S: AsRef, 292 | { 293 | self.env.use_or_create(|env| env.apply_env(name, mutator)) 294 | } 295 | 296 | fn drop_env(&mut self, name: S) 297 | where 298 | S: AsRef, 299 | { 300 | if let Some(env) = &mut self.env { 301 | env.drop_env(name); 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/install/container/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | mod args; 14 | mod env; 15 | mod port; 16 | mod volumes; 17 | 18 | pub use self::args::*; 19 | pub use self::env::*; 20 | pub use self::port::*; 21 | pub use self::volumes::*; 22 | pub use crate::install::resources::*; 23 | 24 | use crate::utils::UseOrCreate; 25 | 26 | use anyhow::Result; 27 | use k8s_openapi::api::apps::v1::Deployment; 28 | use k8s_openapi::api::core::v1::{Container, PodSpec, PodTemplateSpec}; 29 | 30 | pub trait ApplyContainer { 31 | fn apply_container(&mut self, name: &str, mutator: F) -> Result<()> 32 | where 33 | F: FnOnce(&mut Container) -> Result<()>; 34 | } 35 | 36 | pub trait RemoveContainer { 37 | /// removes all containers matching the predicate 38 | fn remove_containers(&mut self, predicate: F) -> usize 39 | where 40 | F: Fn(&Container) -> bool; 41 | 42 | /// remove a container by name 43 | fn remove_container_by_name>(&mut self, name: S) -> bool { 44 | self.remove_containers(|c| c.name == name.as_ref()) > 0 45 | } 46 | } 47 | 48 | impl ApplyContainer for Vec { 49 | fn apply_container(&mut self, name: &str, mutator: F) -> Result<()> 50 | where 51 | F: FnOnce(&mut Container) -> Result<()>, 52 | { 53 | let c = self.iter_mut().find(|c| c.name == name); 54 | match c { 55 | Some(c) => { 56 | mutator(c)?; 57 | } 58 | None => { 59 | let mut container: Container = Default::default(); 60 | container.name = name.into(); 61 | mutator(&mut container)?; 62 | self.push(container); 63 | } 64 | } 65 | 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl ApplyContainer for Option> { 71 | fn apply_container(&mut self, name: &str, mutator: F) -> Result<()> 72 | where 73 | F: FnOnce(&mut Container) -> Result<()>, 74 | { 75 | self.use_or_create(|containers| containers.apply_container(name, mutator)) 76 | } 77 | } 78 | 79 | impl ApplyContainer for PodTemplateSpec { 80 | fn apply_container(&mut self, name: &str, mutator: F) -> Result<()> 81 | where 82 | F: FnOnce(&mut Container) -> Result<()>, 83 | { 84 | self.spec 85 | .use_or_create(|spec| spec.containers.apply_container(name, mutator)) 86 | } 87 | } 88 | 89 | impl ApplyContainer for Deployment { 90 | fn apply_container(&mut self, name: &str, mutator: F) -> Result<()> 91 | where 92 | F: FnOnce(&mut Container) -> Result<()>, 93 | { 94 | self.spec 95 | .use_or_create(|spec| spec.template.apply_container(name, mutator)) 96 | } 97 | } 98 | 99 | impl RemoveContainer for Vec { 100 | fn remove_containers(&mut self, predicate: F) -> usize 101 | where 102 | F: Fn(&Container) -> bool, 103 | { 104 | let mut n: usize = 0; 105 | self.retain(|c| { 106 | if predicate(c) { 107 | n += 1; 108 | false 109 | } else { 110 | true 111 | } 112 | }); 113 | n 114 | } 115 | } 116 | 117 | impl RemoveContainer for Option> { 118 | fn remove_containers(&mut self, predicate: F) -> usize 119 | where 120 | F: Fn(&Container) -> bool, 121 | { 122 | if let Some(containers) = self { 123 | containers.remove_containers(predicate) 124 | } else { 125 | 0 126 | } 127 | } 128 | } 129 | 130 | impl RemoveContainer for PodTemplateSpec { 131 | fn remove_containers(&mut self, predicate: F) -> usize 132 | where 133 | F: Fn(&Container) -> bool, 134 | { 135 | if let Some(spec) = &mut self.spec { 136 | spec.remove_containers(predicate) 137 | } else { 138 | 0 139 | } 140 | } 141 | } 142 | 143 | impl RemoveContainer for PodSpec { 144 | fn remove_containers(&mut self, predicate: F) -> usize 145 | where 146 | F: Fn(&Container) -> bool, 147 | { 148 | self.containers.remove_containers(predicate) 149 | } 150 | } 151 | 152 | impl RemoveContainer for Deployment { 153 | fn remove_containers(&mut self, predicate: F) -> usize 154 | where 155 | F: Fn(&Container) -> bool, 156 | { 157 | if let Some(spec) = &mut self.spec { 158 | spec.template.remove_containers(predicate) 159 | } else { 160 | 0 161 | } 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod test { 167 | use super::*; 168 | use k8s_openapi::api::apps::v1::Deployment; 169 | 170 | /// test apply on different targets 171 | #[test] 172 | fn test_apply() { 173 | let mut d = Deployment::default(); 174 | d.apply_container("foo", |_| Ok(())).unwrap(); 175 | 176 | fn test(dm: &mut Deployment) { 177 | dm.apply_container("foo", |_| Ok(())).unwrap(); 178 | } 179 | 180 | test(&mut d); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/install/container/port.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use crate::utils::UseOrCreate; 14 | use anyhow::Result; 15 | use k8s_openapi::api::core::v1::{Container, ContainerPort}; 16 | 17 | pub trait ApplyPort { 18 | fn apply_port(&mut self, name: S, mutator: F) -> Result<()> 19 | where 20 | F: FnOnce(&mut ContainerPort) -> Result<()>, 21 | S: AsRef; 22 | 23 | fn add_port(&mut self, name: S, container_port: i32, protocol: Option) -> Result<()> 24 | where 25 | S: AsRef, 26 | { 27 | self.apply_port(name, |c| { 28 | c.container_port = container_port; 29 | c.protocol = protocol; 30 | Ok(()) 31 | }) 32 | } 33 | } 34 | 35 | impl ApplyPort for Vec { 36 | fn apply_port(&mut self, name: S, mutator: F) -> Result<()> 37 | where 38 | F: FnOnce(&mut ContainerPort) -> Result<()>, 39 | S: AsRef, 40 | { 41 | let c = self.iter_mut().find(|c| match &c.name { 42 | None => false, 43 | Some(s) => s.as_str() == name.as_ref(), 44 | }); 45 | match c { 46 | Some(c) => { 47 | mutator(c)?; 48 | } 49 | None => { 50 | let mut port: ContainerPort = Default::default(); 51 | port.name = Some(name.as_ref().to_string()); 52 | mutator(&mut port)?; 53 | self.push(port); 54 | } 55 | } 56 | Ok(()) 57 | } 58 | } 59 | 60 | impl ApplyPort for Container { 61 | fn apply_port(&mut self, name: S, mutator: F) -> Result<()> 62 | where 63 | F: FnOnce(&mut ContainerPort) -> Result<()>, 64 | S: AsRef, 65 | { 66 | self.ports 67 | .use_or_create(|ports| ports.apply_port(name, mutator)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/install/container/volumes.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use crate::utils::UseOrCreate; 14 | 15 | use anyhow::Result; 16 | 17 | use k8s_openapi::api::core::v1::{Container, PodSpec, PodTemplateSpec, Volume, VolumeMount}; 18 | 19 | pub trait ApplyVolume { 20 | fn apply_volume(&mut self, name: S, mutator: F) -> Result<()> 21 | where 22 | F: FnOnce(&mut Volume) -> Result<()>, 23 | S: AsRef; 24 | } 25 | 26 | pub trait DropVolume { 27 | fn drop_volume(&mut self, name: S) -> bool 28 | where 29 | S: AsRef; 30 | } 31 | 32 | impl ApplyVolume for Vec { 33 | fn apply_volume(&mut self, name: S, mutator: F) -> Result<()> 34 | where 35 | F: FnOnce(&mut Volume) -> Result<()>, 36 | S: AsRef, 37 | { 38 | let c = self.iter_mut().find(|c| &c.name == name.as_ref()); 39 | match c { 40 | Some(c) => { 41 | mutator(c)?; 42 | } 43 | None => { 44 | let mut item: Volume = Default::default(); 45 | item.name = name.as_ref().to_string(); 46 | mutator(&mut item)?; 47 | self.push(item); 48 | } 49 | } 50 | Ok(()) 51 | } 52 | } 53 | 54 | impl DropVolume for Vec { 55 | fn drop_volume(&mut self, name: S) -> bool 56 | where 57 | S: AsRef, 58 | { 59 | let start = self.len(); 60 | self.retain(|v| v.name != name.as_ref()); 61 | 62 | start != self.len() 63 | } 64 | } 65 | 66 | impl ApplyVolume for PodSpec { 67 | fn apply_volume(&mut self, name: S, mutator: F) -> Result<()> 68 | where 69 | F: FnOnce(&mut Volume) -> Result<()>, 70 | S: AsRef, 71 | { 72 | self.volumes 73 | .use_or_create(|volumes| volumes.apply_volume(name, mutator)) 74 | } 75 | } 76 | 77 | impl ApplyVolume for PodTemplateSpec { 78 | fn apply_volume(&mut self, name: S, mutator: F) -> Result<()> 79 | where 80 | F: FnOnce(&mut Volume) -> Result<()>, 81 | S: AsRef, 82 | { 83 | self.spec 84 | .use_or_create(|spec| spec.apply_volume(name, mutator)) 85 | } 86 | } 87 | 88 | impl DropVolume for PodSpec { 89 | fn drop_volume(&mut self, name: S) -> bool 90 | where 91 | S: AsRef, 92 | { 93 | if let Some(volumes) = &mut self.volumes { 94 | volumes.drop_volume(name) 95 | } else { 96 | false 97 | } 98 | } 99 | } 100 | 101 | impl DropVolume for PodTemplateSpec { 102 | fn drop_volume(&mut self, name: S) -> bool 103 | where 104 | S: AsRef, 105 | { 106 | if let Some(spec) = &mut self.spec { 107 | spec.drop_volume(name) 108 | } else { 109 | false 110 | } 111 | } 112 | } 113 | 114 | pub trait ApplyVolumeMount { 115 | fn apply_volume_mount(&mut self, name: S, mutator: F) -> Result<()> 116 | where 117 | F: FnOnce(&mut VolumeMount) -> Result<()>, 118 | S: AsRef; 119 | 120 | fn apply_volume_mount_simple( 121 | &mut self, 122 | name: S1, 123 | path: S2, 124 | read_only: bool, 125 | ) -> Result<()> 126 | where 127 | S1: AsRef, 128 | S2: ToString, 129 | { 130 | self.apply_volume_mount(name, |mount| { 131 | mount.mount_path = path.to_string(); 132 | mount.read_only = Some(read_only); 133 | mount.mount_propagation = None; 134 | mount.sub_path = None; 135 | mount.sub_path_expr = None; 136 | Ok(()) 137 | }) 138 | } 139 | } 140 | 141 | pub trait DropVolumeMount { 142 | fn drop_volume_mount(&mut self, name: S) -> bool 143 | where 144 | S: AsRef; 145 | } 146 | 147 | impl ApplyVolumeMount for Vec { 148 | fn apply_volume_mount(&mut self, name: S, mutator: F) -> Result<()> 149 | where 150 | F: FnOnce(&mut VolumeMount) -> Result<()>, 151 | S: AsRef, 152 | { 153 | let c = self.iter_mut().find(|c| &c.name == name.as_ref()); 154 | match c { 155 | Some(c) => { 156 | mutator(c)?; 157 | } 158 | None => { 159 | let mut item: VolumeMount = Default::default(); 160 | item.name = name.as_ref().to_string(); 161 | mutator(&mut item)?; 162 | self.push(item); 163 | } 164 | } 165 | Ok(()) 166 | } 167 | } 168 | 169 | impl DropVolumeMount for Vec { 170 | fn drop_volume_mount(&mut self, name: S) -> bool 171 | where 172 | S: AsRef, 173 | { 174 | let start = self.len(); 175 | self.retain(|v| v.name != name.as_ref()); 176 | 177 | start != self.len() 178 | } 179 | } 180 | 181 | impl ApplyVolumeMount for Container { 182 | fn apply_volume_mount(&mut self, name: S, mutator: F) -> Result<()> 183 | where 184 | F: FnOnce(&mut VolumeMount) -> Result<()>, 185 | S: AsRef, 186 | { 187 | self.volume_mounts 188 | .use_or_create(|volume_mounts| volume_mounts.apply_volume_mount(name, mutator)) 189 | } 190 | } 191 | 192 | impl DropVolumeMount for Container { 193 | fn drop_volume_mount(&mut self, name: S) -> bool 194 | where 195 | S: AsRef, 196 | { 197 | if let Some(volume_mounts) = &mut self.volume_mounts { 198 | volume_mounts.drop_volume_mount(name) 199 | } else { 200 | false 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/install/delete.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2021 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | use async_trait::async_trait; 15 | use either::Either::{Left, Right}; 16 | use futures::future::FutureExt; 17 | use kube::{ 18 | api::{DeleteParams, Preconditions}, 19 | Api, Error, Resource, 20 | }; 21 | use serde::de::DeserializeOwned; 22 | use std::fmt::Debug; 23 | 24 | #[async_trait] 25 | pub trait Delete { 26 | /// Optionally delete a resource. If the resource was already gone, this is not treated as an error. 27 | /// 28 | /// The function will return `true` if the resource was deleted (or already gone) and `false` if 29 | /// the resource is being delete. All other errors are returned unmodified. 30 | async fn delete_optionally(&self, name: S, dp: &DeleteParams) -> Result 31 | where 32 | S: AsRef + Send + Sync; 33 | 34 | async fn delete_conditionally(&self, name: S, f: F) -> Result 35 | where 36 | F: FnOnce(&R) -> Result + Send, 37 | E: From, 38 | S: AsRef + Send + Sync; 39 | } 40 | 41 | #[async_trait] 42 | impl Delete for Api 43 | where 44 | K: Resource + Clone + DeserializeOwned + Send + Debug, 45 | { 46 | async fn delete_optionally(&self, name: S, dp: &DeleteParams) -> Result 47 | where 48 | S: AsRef + Send + Sync, 49 | { 50 | Ok(self 51 | .delete(name.as_ref(), dp) 52 | .map(|future| { 53 | future 54 | .map(|either| match either { 55 | Left(_) => false, 56 | Right(_) => true, 57 | }) 58 | .or_else(|err| match err { 59 | Error::Api(cause) if cause.reason == "NotFound" => Ok(true), 60 | _ => Err(err), 61 | }) 62 | }) 63 | .await?) 64 | } 65 | 66 | async fn delete_conditionally(&self, name: S, f: F) -> Result 67 | where 68 | F: FnOnce(&K) -> Result + Send, 69 | E: From, 70 | S: AsRef + Send + Sync, 71 | { 72 | let resource = match self.get(name.as_ref()).await { 73 | Err(Error::Api(cause)) if cause.reason == "NotFound" => return Ok(false), 74 | result => result?, 75 | }; 76 | 77 | if f(&resource)? { 78 | let dp = DeleteParams { 79 | preconditions: Some(Preconditions { 80 | resource_version: resource.meta().resource_version.as_ref().cloned(), 81 | uid: resource.meta().uid.as_ref().cloned(), 82 | }), 83 | ..Default::default() 84 | }; 85 | 86 | self.delete_optionally(name, &dp).await?; 87 | 88 | Ok(true) 89 | } else { 90 | Ok(false) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/install/meta.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | use crate::utils::UseOrCreate; 15 | use anyhow::{anyhow, Error}; 16 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference}; 17 | use std::borrow::Cow; 18 | 19 | pub trait Meta { 20 | fn metadata(&self) -> &ObjectMeta; 21 | fn metadata_mut(&mut self) -> &mut ObjectMeta; 22 | 23 | fn kind(&self) -> Cow<'_, str>; 24 | fn api_version(&self) -> Cow<'_, str>; 25 | } 26 | 27 | pub trait OwnedBy { 28 | fn owned_by( 29 | &mut self, 30 | resource: &R, 31 | controller: bool, 32 | block_owner_deletion: Option, 33 | ) -> Result<(), anyhow::Error>; 34 | 35 | fn owned_by_controller(&mut self, resource: &R) -> Result<(), anyhow::Error> { 36 | self.owned_by(resource, true, None) 37 | } 38 | 39 | fn is_owned_by(&self, owner: &R, controller: Option) -> Result; 40 | 41 | fn is_owned_by_controller(&self, owner: &R) -> Result { 42 | self.is_owned_by(owner, Some(true)) 43 | } 44 | } 45 | 46 | pub trait SameOwner { 47 | fn is_same_owner(&self, other: &OwnerReference) -> bool { 48 | self.is_same_owner_opts(other, false) 49 | } 50 | 51 | fn is_same_owner_opts(&self, other: &OwnerReference, check_controller: bool) -> bool; 52 | } 53 | 54 | impl SameOwner for OwnerReference { 55 | fn is_same_owner_opts(&self, other: &OwnerReference, check_controller: bool) -> bool { 56 | if check_controller { 57 | // we check the controller first 58 | let self_controller = self.controller.unwrap_or(false); 59 | let other_controller = other.controller.unwrap_or(false); 60 | // if the controller flags don't match 61 | if self_controller != other_controller { 62 | // we can abort early 63 | return false; 64 | } 65 | } 66 | 67 | return self.kind == other.kind 68 | && self.api_version == other.api_version 69 | && self.name == other.name; 70 | } 71 | } 72 | 73 | pub trait AsOwner { 74 | fn as_owner( 75 | &self, 76 | controller: Option, 77 | block_owner_deletion: Option, 78 | ) -> Result; 79 | 80 | fn as_controller_owner(&self) -> Result { 81 | self.as_owner(Some(true), None) 82 | } 83 | } 84 | 85 | impl AsOwner for K 86 | where 87 | K: Meta, 88 | { 89 | fn as_owner( 90 | &self, 91 | controller: Option, 92 | block_owner_deletion: Option, 93 | ) -> Result { 94 | let name = self 95 | .metadata() 96 | .name 97 | .as_ref() 98 | .ok_or_else(|| anyhow!("Missing name"))? 99 | .clone(); 100 | let uid = self 101 | .metadata() 102 | .uid 103 | .as_ref() 104 | .ok_or_else(|| anyhow!("Missing UID"))? 105 | .clone(); 106 | 107 | Ok(OwnerReference { 108 | kind: self.kind().to_string(), 109 | api_version: self.api_version().to_string(), 110 | name, 111 | uid, 112 | controller, 113 | block_owner_deletion, 114 | }) 115 | } 116 | } 117 | 118 | impl Meta for K 119 | where 120 | K: kube::Resource, 121 | { 122 | fn metadata(&self) -> &ObjectMeta { 123 | self.meta() 124 | } 125 | 126 | fn metadata_mut(&mut self) -> &mut ObjectMeta { 127 | self.meta_mut() 128 | } 129 | 130 | fn kind(&self) -> Cow<'_, str> { 131 | Self::kind(&()) 132 | } 133 | 134 | fn api_version(&self) -> Cow<'_, str> { 135 | Self::api_version(&()) 136 | } 137 | } 138 | 139 | impl OwnedBy for K 140 | where 141 | K: Meta, 142 | R: Meta, 143 | { 144 | fn owned_by( 145 | &mut self, 146 | resource: &R, 147 | controller: bool, 148 | block_owner_deletion: Option, 149 | ) -> Result<(), anyhow::Error> { 150 | match (&self.metadata().namespace, &resource.metadata().namespace) { 151 | (None, None) => Ok(()), 152 | (Some(_), None) => Ok(()), 153 | 154 | (Some(obj_ns), Some(owner_ns)) => { 155 | if obj_ns == owner_ns { 156 | Ok(()) 157 | } else { 158 | Err(anyhow!( 159 | "If both objects are namespaced, they must belong to the same namespace" 160 | )) 161 | } 162 | } 163 | (None, Some(_)) => Err(anyhow!( 164 | "Cluster scoped object must not have a namespaced owner" 165 | )), 166 | }?; 167 | 168 | let owner = resource.as_owner(Some(controller), block_owner_deletion)?; 169 | 170 | let mut found = None; 171 | 172 | self.metadata_mut() 173 | .owner_references 174 | .use_or_create_err(|owners| { 175 | for (idx, o) in owners.iter().enumerate() { 176 | if owner.is_same_owner(&o) { 177 | found = Some(idx); 178 | } else if controller { 179 | match o.controller { 180 | Some(true) => Err(anyhow!("Object already has a controller")), 181 | _ => Ok(()), 182 | }?; 183 | } 184 | } 185 | 186 | match found { 187 | Some(idx) => { 188 | let o = &mut owners[idx]; 189 | o.controller = owner.controller; 190 | o.block_owner_deletion = owner.block_owner_deletion; 191 | } 192 | None => { 193 | owners.push(owner); 194 | } 195 | } 196 | 197 | Ok(()) 198 | })?; 199 | 200 | Ok(()) 201 | } 202 | 203 | fn is_owned_by(&self, owner: &R, controlled: Option) -> Result { 204 | let owner = owner.as_owner(controlled, None)?; 205 | 206 | if let Some(owner_refs) = &self.metadata().owner_references { 207 | for r in owner_refs { 208 | if r.is_same_owner_opts(&owner, controlled.is_some()) { 209 | return Ok(true); 210 | } 211 | } 212 | } 213 | 214 | Ok(false) 215 | } 216 | } 217 | 218 | #[cfg(test)] 219 | mod tests { 220 | 221 | use super::*; 222 | use k8s_openapi::api::core::v1::ConfigMap; 223 | 224 | /// Create a new config map for testing 225 | fn new_cm(namespace: Option<&str>, name: &str, uid: &str) -> ConfigMap { 226 | ConfigMap { 227 | metadata: ObjectMeta { 228 | name: Some(name.into()), 229 | namespace: namespace.map(|s| s.to_string()), 230 | uid: Some(uid.to_string()), 231 | ..Default::default() 232 | }, 233 | ..Default::default() 234 | } 235 | } 236 | 237 | #[test] 238 | fn test_owned_by() { 239 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 240 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 241 | 242 | let r = config_map_1.owned_by(&config_map_2, false, None); 243 | assert!(r.is_ok(), "Should be ok"); 244 | assert_eq!( 245 | 1, 246 | config_map_1 247 | .metadata 248 | .owner_references 249 | .unwrap_or_default() 250 | .len() 251 | ) 252 | } 253 | 254 | #[test] 255 | fn test_owned_by_multiple() { 256 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 257 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 258 | let config_map_3: ConfigMap = new_cm(Some("ns1"), "cm3", "789"); 259 | 260 | let r = config_map_1.owned_by(&config_map_2, false, None); 261 | assert!(r.is_ok(), "Should be ok"); 262 | let r = config_map_1.owned_by(&config_map_3, false, None); 263 | assert!(r.is_ok(), "Should be ok"); 264 | assert_eq!( 265 | 2, 266 | config_map_1 267 | .metadata 268 | .owner_references 269 | .unwrap_or_default() 270 | .len() 271 | ) 272 | } 273 | 274 | #[test] 275 | fn test_owned_by_multiple_replace() { 276 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 277 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 278 | let config_map_3: ConfigMap = new_cm(Some("ns1"), "cm3", "789"); 279 | 280 | let r = config_map_1.owned_by(&config_map_2, false, None); 281 | assert!(r.is_ok(), "Should be ok"); 282 | let r = config_map_1.owned_by(&config_map_3, false, None); 283 | assert!(r.is_ok(), "Should be ok"); 284 | 285 | let config_map_4: ConfigMap = new_cm(Some("ns1"), "cm3", "AAA"); 286 | let r = config_map_1.owned_by(&config_map_4, false, None); 287 | assert!(r.is_ok(), "Should be ok"); 288 | assert_eq!( 289 | 2, 290 | config_map_1 291 | .metadata 292 | .owner_references 293 | .unwrap_or_default() 294 | .len() 295 | ) 296 | } 297 | 298 | #[test] 299 | fn test_owned_by_controller() { 300 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 301 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 302 | 303 | let r = config_map_1.owned_by_controller(&config_map_2); 304 | assert!(r.is_ok(), "Should be ok"); 305 | assert_eq!( 306 | 1, 307 | config_map_1 308 | .metadata 309 | .owner_references 310 | .unwrap_or_default() 311 | .len() 312 | ) 313 | } 314 | 315 | #[test] 316 | fn test_owned_by_controller_only_one() { 317 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 318 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 319 | let config_map_3: ConfigMap = new_cm(Some("ns1"), "cm3", "789"); 320 | let config_map_4: ConfigMap = new_cm(Some("ns1"), "cm4", "012"); 321 | 322 | let r = config_map_1.owned_by_controller(&config_map_2); 323 | assert!(r.is_ok(), "Should be ok"); 324 | let r = config_map_1.owned_by(&config_map_3, false, None); 325 | assert!(r.is_ok(), "Should be ok"); 326 | let r = config_map_1.owned_by_controller(&config_map_4); 327 | assert!(r.is_err(), "Must not be ok"); 328 | assert_eq!( 329 | 2, 330 | config_map_1 331 | .metadata 332 | .owner_references 333 | .unwrap_or_default() 334 | .len() 335 | ) 336 | } 337 | 338 | #[test] 339 | fn test_is_owned_by() { 340 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 341 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 342 | 343 | let r = config_map_1.owned_by(&config_map_2, false, None); 344 | assert!(r.is_ok()); 345 | 346 | assert_eq!( 347 | true, 348 | config_map_1 349 | .is_owned_by(&config_map_2, Some(false)) 350 | .unwrap() 351 | ); 352 | assert_eq!( 353 | false, 354 | config_map_2 355 | .is_owned_by(&config_map_1, Some(false)) 356 | .unwrap() 357 | ); 358 | 359 | assert_eq!(true, config_map_1.is_owned_by(&config_map_2, None).unwrap()); 360 | assert_eq!( 361 | false, 362 | config_map_2.is_owned_by(&config_map_1, None).unwrap() 363 | ); 364 | } 365 | 366 | #[test] 367 | fn test_is_controlled_by() { 368 | let mut config_map_1: ConfigMap = new_cm(Some("ns1"), "cm1", "123"); 369 | let config_map_2: ConfigMap = new_cm(Some("ns1"), "cm2", "456"); 370 | let config_map_3: ConfigMap = new_cm(Some("ns1"), "cm3", "789"); 371 | 372 | config_map_1.owned_by_controller(&config_map_2).unwrap(); 373 | config_map_1.owned_by(&config_map_3, false, None).unwrap(); 374 | 375 | assert_eq!( 376 | true, 377 | config_map_1.is_owned_by_controller(&config_map_2).unwrap() 378 | ); 379 | assert_eq!( 380 | false, 381 | config_map_1.is_owned_by_controller(&config_map_3).unwrap() 382 | ); 383 | 384 | assert_eq!(true, config_map_1.is_owned_by(&config_map_2, None).unwrap()); 385 | assert_eq!(true, config_map_1.is_owned_by(&config_map_3, None).unwrap()); 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/install/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | pub mod config; 14 | pub mod container; 15 | mod delete; 16 | pub mod meta; 17 | mod resources; 18 | mod value; 19 | 20 | pub use self::delete::*; 21 | pub use self::resources::*; 22 | pub use self::value::*; 23 | -------------------------------------------------------------------------------- /src/install/resources.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use crate::utils::UseOrCreate; 14 | 15 | use k8s_openapi::api::core::v1::{Container, ResourceRequirements}; 16 | use k8s_openapi::apimachinery::pkg::api::resource::Quantity; 17 | 18 | pub trait SetResources { 19 | fn set_resources( 20 | &mut self, 21 | resource_type: S1, 22 | request: Option, 23 | limit: Option, 24 | ) where 25 | S1: Into, 26 | S2: Into, 27 | S3: Into; 28 | } 29 | 30 | impl SetResources for ResourceRequirements { 31 | fn set_resources( 32 | &mut self, 33 | resource_type: S1, 34 | request: Option, 35 | limit: Option, 36 | ) where 37 | S1: Into, 38 | S2: Into, 39 | S3: Into, 40 | { 41 | let resource_type = resource_type.into(); 42 | match request { 43 | Some(request) => self.requests.use_or_create(|requests| { 44 | requests.insert(resource_type.clone(), Quantity(request.into())); 45 | }), 46 | None => { 47 | if let Some(requests) = &mut self.requests { 48 | requests.remove(&resource_type); 49 | } 50 | } 51 | }; 52 | match limit { 53 | Some(limit) => self.limits.use_or_create(|limits| { 54 | limits.insert(resource_type, Quantity(limit.into())); 55 | }), 56 | None => { 57 | if let Some(limits) = &mut self.limits { 58 | limits.remove(&resource_type); 59 | } 60 | } 61 | }; 62 | } 63 | } 64 | 65 | impl SetResources for Container { 66 | fn set_resources( 67 | &mut self, 68 | resource_type: S1, 69 | request: Option, 70 | limit: Option, 71 | ) where 72 | S1: Into, 73 | S2: Into, 74 | S3: Into, 75 | { 76 | self.resources.use_or_create(|resources| { 77 | resources.set_resources(resource_type, request, limit); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/install/value.rs: -------------------------------------------------------------------------------- 1 | use crate::install::container::ApplyEnvironmentVariable; 2 | use anyhow::Result; 3 | use async_trait::async_trait; 4 | use core::fmt::{self, Formatter}; 5 | use k8s_openapi::api::core::v1::{ 6 | ConfigMap, ConfigMapKeySelector, EnvVar, EnvVarSource, Secret, SecretKeySelector, 7 | }; 8 | use kube::{Api, Resource}; 9 | use serde::{ 10 | de::{self, DeserializeOwned, MapAccess, Visitor}, 11 | {Deserialize, Deserializer, Serialize}, 12 | }; 13 | use std::fmt::Debug; 14 | 15 | #[cfg(feature = "schemars")] 16 | use schemars::{ 17 | gen::SchemaGenerator, 18 | schema::{ 19 | InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation, 20 | }, 21 | JsonSchema, 22 | }; 23 | 24 | #[derive(Debug, Clone, PartialEq, Serialize)] 25 | #[serde(rename_all = "camelCase")] 26 | pub enum ValueOrReference { 27 | Value(String), 28 | Secret(SecretKeySelector), 29 | ConfigMap(ConfigMapKeySelector), 30 | } 31 | 32 | #[cfg(feature = "schemars")] 33 | mod schema { 34 | use schemars::schema::*; 35 | 36 | pub(crate) fn required(name: &str) -> Schema { 37 | Schema::Object(SchemaObject { 38 | object: Some(Box::new(ObjectValidation { 39 | required: { 40 | let mut r = schemars::Set::new(); 41 | r.insert(name.into()); 42 | r 43 | }, 44 | ..Default::default() 45 | })), 46 | ..Default::default() 47 | }) 48 | } 49 | } 50 | 51 | #[cfg(feature = "schemars")] 52 | impl JsonSchema for ValueOrReference { 53 | fn schema_name() -> String { 54 | "ValueOrReference".into() 55 | } 56 | 57 | fn json_schema(gen: &mut SchemaGenerator) -> Schema { 58 | Schema::Object(SchemaObject { 59 | instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), 60 | object: Some(Box::new(ObjectValidation { 61 | properties: { 62 | let mut p = schemars::Map::new(); 63 | p.insert( 64 | "value".into(), 65 | Schema::Object(SchemaObject { 66 | instance_type: Some(SingleOrVec::Single(Box::new( 67 | InstanceType::String, 68 | ))), 69 | ..Default::default() 70 | }), 71 | ); 72 | p.insert("secret".into(), ::json_schema(gen)); 73 | p.insert("configMap".into(), ::json_schema(gen)); 74 | p 75 | }, 76 | ..Default::default() 77 | })), 78 | subschemas: Some(Box::new(SubschemaValidation { 79 | one_of: Some(vec![ 80 | schema::required("value"), 81 | schema::required("secret"), 82 | schema::required("configMap"), 83 | ]), 84 | ..Default::default() 85 | })), 86 | ..Default::default() 87 | }) 88 | } 89 | } 90 | 91 | #[async_trait] 92 | pub trait Reader { 93 | /// Read a value from a configmap. Only returns `None` if the selector was optional. 94 | async fn read_configmap(&self, selector: &ConfigMapKeySelector) -> Result>; 95 | /// Read a value from a secret. Only returns `None` if the selector was optional. 96 | async fn read_secret(&self, selector: &SecretKeySelector) -> Result>; 97 | } 98 | 99 | pub struct KubeReader<'a> { 100 | configmaps: &'a Api, 101 | secrets: &'a Api, 102 | } 103 | 104 | impl<'a> KubeReader<'a> { 105 | pub fn new(configmaps: &'a Api, secrets: &'a Api) -> Self { 106 | Self { 107 | configmaps, 108 | secrets, 109 | } 110 | } 111 | 112 | fn no_result(optional: bool, ty: &str, name: &str, key: &str) -> Result> { 113 | if optional { 114 | Ok(None) 115 | } else { 116 | anyhow::bail!("Missing key '{}' in {} '{}'", key, ty, name) 117 | } 118 | } 119 | 120 | async fn read( 121 | ty: &str, 122 | api: &Api, 123 | name: Option<&str>, 124 | key: &str, 125 | optional: Option, 126 | extractor: F, 127 | ) -> Result> 128 | where 129 | T: Resource + DeserializeOwned + Clone + Debug, 130 | F: FnOnce(T, &str) -> Option, 131 | { 132 | if let Some(name) = name { 133 | let optional = optional.unwrap_or_default(); 134 | 135 | match api.get(&name).await { 136 | Ok(resource) => match extractor(resource, key) { 137 | Some(value) => Ok(Some(value)), 138 | None => Self::no_result(optional, ty, name, key), 139 | }, 140 | Err(kube::Error::Api(err)) if err.reason == "NotFound" => { 141 | Self::no_result(optional, ty, name, key) 142 | } 143 | Err(err) => Err(err.into()), 144 | } 145 | } else { 146 | Ok(None) 147 | } 148 | } 149 | } 150 | 151 | #[async_trait] 152 | impl<'a> Reader for KubeReader<'a> { 153 | async fn read_configmap(&self, selector: &ConfigMapKeySelector) -> Result> { 154 | Self::read( 155 | "ConfigMap", 156 | &self.configmaps, 157 | selector.name.as_ref().map(|s| s.as_str()), 158 | &selector.key, 159 | selector.optional, 160 | |resource, key| resource.data.and_then(|data| data.get(key).cloned()), 161 | ) 162 | .await 163 | } 164 | 165 | async fn read_secret(&self, selector: &SecretKeySelector) -> Result> { 166 | Self::read( 167 | "Secret", 168 | &self.secrets, 169 | selector.name.as_ref().map(|s| s.as_str()), 170 | &selector.key, 171 | selector.optional, 172 | |resource, key| { 173 | resource.data.and_then(|data| { 174 | data.get(key) 175 | .cloned() 176 | .and_then(|s| String::from_utf8(s.0).ok()) 177 | }) 178 | }, 179 | ) 180 | .await 181 | } 182 | } 183 | 184 | impl ValueOrReference { 185 | /// apply the value (or reference) to an env-var 186 | pub fn apply_to_envvar(&self, env: &mut EnvVar) { 187 | match self { 188 | Self::Value(value) => { 189 | env.value = Some(value.into()); 190 | env.value_from = None; 191 | } 192 | Self::ConfigMap(selector) => { 193 | env.value = None; 194 | env.value_from = Some(EnvVarSource { 195 | config_map_key_ref: Some(selector.clone()), 196 | field_ref: None, 197 | resource_field_ref: None, 198 | secret_key_ref: None, 199 | }); 200 | } 201 | Self::Secret(selector) => { 202 | env.value = None; 203 | env.value_from = Some(EnvVarSource { 204 | config_map_key_ref: None, 205 | field_ref: None, 206 | resource_field_ref: None, 207 | secret_key_ref: Some(selector.clone()), 208 | }); 209 | } 210 | } 211 | } 212 | 213 | /// Apply the value as an environment variable to a ['ApplyEnvironmentVariable'], e.g. a ['Container']. 214 | pub fn apply_to_env(&self, env: &mut E, name: S) 215 | where 216 | E: ApplyEnvironmentVariable, 217 | S: AsRef, 218 | { 219 | env.apply_env(name, |envvar| { 220 | self.apply_to_envvar(envvar); 221 | Ok(()) 222 | }) 223 | // we can unwrap here as we are not returning an error in our mutator 224 | .unwrap(); 225 | } 226 | 227 | /// Read the actual value. 228 | /// 229 | /// This may either return the value directly, or do a remote call to read the value. 230 | pub async fn read_value(&self, reader: &R) -> Result> 231 | where 232 | R: Reader, 233 | { 234 | match self { 235 | Self::Value(value) => Ok(Some(value.clone())), 236 | Self::ConfigMap(selector) => reader.read_configmap(selector).await, 237 | Self::Secret(selector) => reader.read_secret(selector).await, 238 | } 239 | } 240 | } 241 | 242 | impl<'de> Deserialize<'de> for ValueOrReference { 243 | fn deserialize(deserializer: D) -> Result>::Error> 244 | where 245 | D: Deserializer<'de>, 246 | { 247 | struct ValueVisitor; 248 | impl<'de> Visitor<'de> for ValueVisitor { 249 | type Value = ValueOrReference; 250 | 251 | fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { 252 | formatter.write_str("string or map") 253 | } 254 | 255 | fn visit_str(self, value: &str) -> Result { 256 | Ok(ValueOrReference::Value(value.to_string())) 257 | } 258 | 259 | fn visit_map(self, mut map: V) -> Result 260 | where 261 | V: MapAccess<'de>, 262 | { 263 | if let Some(key) = map.next_key::()? { 264 | match key.as_str() { 265 | "value" => Ok(ValueOrReference::Value(map.next_value()?)), 266 | "configMap" => Ok(ValueOrReference::ConfigMap(map.next_value()?)), 267 | "secret" => Ok(ValueOrReference::Secret(map.next_value()?)), 268 | t => Err(de::Error::unknown_variant( 269 | t, 270 | &["value", "configMap", "secret"], 271 | )), 272 | } 273 | } else { 274 | Err(de::Error::custom("No value type present")) 275 | } 276 | } 277 | } 278 | 279 | deserializer.deserialize_any(ValueVisitor) 280 | } 281 | } 282 | 283 | #[cfg(test)] 284 | mod test { 285 | use super::*; 286 | use anyhow::Result; 287 | use serde_json::{json, Value}; 288 | 289 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] 290 | #[serde(rename_all = "camelCase")] 291 | pub struct MyCrd { 292 | pub field_one: ValueOrReference, 293 | } 294 | 295 | fn test_combination(crd: MyCrd, value: Value) -> Result<()> { 296 | let enc = serde_json::to_value(&crd)?; 297 | 298 | println!("Encoded: {}", enc); 299 | // test encoding of crd 300 | assert_eq!(enc, value); 301 | // test decoding of crd 302 | assert_eq!(crd, serde_json::from_value(value)?); 303 | 304 | Ok(()) 305 | } 306 | 307 | #[test] 308 | fn test_value_legacy() -> Result<()> { 309 | test_combination( 310 | MyCrd { 311 | field_one: ValueOrReference::Value("foo".to_string()), 312 | }, 313 | json!({ 314 | "fieldOne": "foo", 315 | }), 316 | )?; 317 | 318 | Ok(()) 319 | } 320 | 321 | #[test] 322 | fn test_configmap() -> Result<()> { 323 | test_combination( 324 | MyCrd { 325 | field_one: ValueOrReference::ConfigMap(ConfigMapKeySelector { 326 | name: Some("foo".to_string()), 327 | key: "bar".to_string(), 328 | ..Default::default() 329 | }), 330 | }, 331 | json!({ 332 | "fieldOne": { 333 | "configMap": { 334 | "name": "foo", 335 | "key": "bar", 336 | } 337 | } 338 | }), 339 | )?; 340 | 341 | Ok(()) 342 | } 343 | 344 | #[test] 345 | fn test_secret() -> Result<()> { 346 | test_combination( 347 | MyCrd { 348 | field_one: ValueOrReference::Secret(SecretKeySelector { 349 | name: Some("foo".to_string()), 350 | key: "bar".to_string(), 351 | ..Default::default() 352 | }), 353 | }, 354 | json!({ 355 | "fieldOne": { 356 | "secret": { 357 | "name": "foo", 358 | "key": "bar", 359 | } 360 | } 361 | }), 362 | )?; 363 | 364 | Ok(()) 365 | } 366 | 367 | #[test] 368 | fn test_value() -> Result<()> { 369 | test_combination( 370 | MyCrd { 371 | field_one: ValueOrReference::Value("fooBar".into()), 372 | }, 373 | json!({ 374 | "fieldOne": { 375 | "value": "fooBar" 376 | } 377 | }), 378 | )?; 379 | 380 | Ok(()) 381 | } 382 | 383 | #[test] 384 | fn test_wrong_type() -> Result<()> { 385 | let crd: serde_json::Result = serde_json::from_value(json!({"fieldOne": { 386 | "foo": "bar", 387 | }})); 388 | 389 | assert!(crd.is_err()); 390 | 391 | Ok(()) 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | pub mod conditions; 14 | pub mod install; 15 | pub mod process; 16 | pub mod selectors; 17 | pub mod tracker; 18 | pub mod utils; 19 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use anyhow::Result; 14 | 15 | use kube::{ 16 | api::{ObjectMeta, PostParams}, 17 | Api, Error, Resource, 18 | }; 19 | use serde::{de::DeserializeOwned, Serialize}; 20 | use std::fmt::Debug; 21 | 22 | pub enum Outcome { 23 | Created(T), 24 | Updated(T), 25 | Unchanged(T), 26 | } 27 | 28 | impl Outcome { 29 | pub fn resource(self) -> T { 30 | match self { 31 | Self::Created(r) => r, 32 | Self::Updated(r) => r, 33 | Self::Unchanged(r) => r, 34 | } 35 | } 36 | } 37 | 38 | /// Create or update a Kubernetes resource. 39 | pub async fn create_or_update_by( 40 | api: &Api, 41 | namespace: Option, 42 | name: S2, 43 | creator: C, 44 | eq: Eq, 45 | mutator: F, 46 | ) -> Result, E> 47 | where 48 | T: Resource + Clone + Debug + DeserializeOwned + Serialize, 49 | S1: ToString, 50 | S2: AsRef, 51 | C: FnOnce(ObjectMeta) -> T, 52 | F: FnOnce(T) -> Result, 53 | Eq: FnOnce(&T, &T) -> bool, 54 | E: From, 55 | { 56 | match api.get(name.as_ref()).await { 57 | Err(Error::Api(ae)) if ae.code == 404 => { 58 | log::debug!("CreateOrUpdate - Err(Api(404))"); 59 | let object: T = creator(ObjectMeta { 60 | namespace: namespace.map(|s| s.to_string()), 61 | name: Some(name.as_ref().to_string()), 62 | ..Default::default() 63 | }); 64 | let object = mutator(object)?; 65 | api.create(&PostParams::default(), &object).await?; 66 | Ok(Outcome::Created(object)) 67 | } 68 | Err(e) => { 69 | log::info!("Error - {}", e); 70 | Err(e)? 71 | } 72 | Ok(object) => { 73 | log::debug!("CreateOrUpdate - Ok(...)"); 74 | let new_object = mutator(object.clone())?; 75 | 76 | // only update when necessary 77 | if !eq(&object, &new_object) { 78 | log::debug!("CreateOrUpdate - Changed -> replacing"); 79 | api.replace(name.as_ref(), &PostParams::default(), &new_object) 80 | .await?; 81 | Ok(Outcome::Updated(new_object)) 82 | } else { 83 | Ok(Outcome::Unchanged(new_object)) 84 | } 85 | } 86 | } 87 | } 88 | 89 | /// Create or update a Kubernetes resource. 90 | pub async fn create_or_update( 91 | api: &Api, 92 | namespace: Option, 93 | name: S2, 94 | mutator: F, 95 | ) -> Result, E> 96 | where 97 | T: Resource + Clone + Debug + DeserializeOwned + Serialize + PartialEq + Default, 98 | S1: ToString, 99 | S2: AsRef, 100 | F: FnOnce(T) -> Result, 101 | E: From, 102 | { 103 | create_or_update_by( 104 | api, 105 | namespace, 106 | name, 107 | |meta| { 108 | let mut object: T = Default::default(); 109 | *object.meta_mut() = meta; 110 | object 111 | }, 112 | |this, that| this == that, 113 | mutator, 114 | ) 115 | .await 116 | } 117 | -------------------------------------------------------------------------------- /src/selectors.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | use std::collections::BTreeMap; 15 | 16 | pub trait ToSelector { 17 | /// Convert to a valid selector expression 18 | fn to_selector(&self) -> String; 19 | } 20 | 21 | impl ToSelector for BTreeMap 22 | where 23 | S1: ToString, 24 | S2: AsRef, 25 | { 26 | /// For a map, we generate an "and" expression, consisting of all key/value pairs. 27 | fn to_selector(&self) -> String { 28 | self.iter() 29 | .map(|(k, v)| k.to_string() + "=" + v.as_ref()) 30 | .collect::>() 31 | .join(",") 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | 38 | use super::*; 39 | 40 | #[test] 41 | fn test_empty() { 42 | let labels = BTreeMap::::new(); 43 | assert_eq!("", labels.to_selector()); 44 | } 45 | 46 | #[test] 47 | fn test_single() { 48 | let mut labels = BTreeMap::new(); 49 | labels.insert("foo", "bar"); 50 | assert_eq!("foo=bar", labels.to_selector()); 51 | } 52 | 53 | #[test] 54 | fn test_multiple() { 55 | let mut labels = BTreeMap::new(); 56 | labels.insert("foo", "bar"); 57 | labels.insert("bar", "baz"); 58 | let sel = labels.to_selector(); 59 | 60 | // the map doesn't provide an order, so we need to check for both variants 61 | assert!(sel == "foo=bar,bar=baz" || sel == "bar=baz,foo=bar"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/tracker.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2022 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use k8s_openapi::{ 14 | api::core::v1::{ConfigMap, Secret}, 15 | ByteString, 16 | }; 17 | use sha1::{Digest, Sha1}; 18 | use std::{ 19 | collections::BTreeMap, 20 | fmt::{Display, Formatter}, 21 | }; 22 | 23 | /// Tracking content changes of configurations. 24 | /// 25 | /// This is useful for things like ConfigMaps and Secrets, where a change in content 26 | /// should trigger a redeployment. The config tracker keeps an internal hash, which, 27 | /// for example, can be applied to the annotation of a PodSpec. A change in content will 28 | /// result a changed hash, and thus a change in the PodSpec, resulting in a redeployment. 29 | pub struct ConfigTracker { 30 | sha: Sha1, 31 | } 32 | 33 | pub trait Trackable { 34 | fn track_with(&self, tracker: &mut ConfigTracker); 35 | } 36 | 37 | impl ConfigTracker { 38 | pub fn new() -> Self { 39 | ConfigTracker { sha: Sha1::new() } 40 | } 41 | 42 | pub fn track(&mut self, data: D) 43 | where 44 | D: AsRef<[u8]>, 45 | { 46 | self.sha.update(data.as_ref()); 47 | } 48 | 49 | pub fn current_hash(&self) -> String { 50 | format!("{:x}", self.sha.clone().finalize()) 51 | } 52 | 53 | /// Freeze the current tracker state and return it. 54 | pub fn freeze(self) -> TrackerState { 55 | TrackerState(self.current_hash()) 56 | } 57 | } 58 | 59 | #[derive(Clone, Debug, PartialEq, Eq)] 60 | pub struct TrackerState(pub String); 61 | 62 | impl AsRef<[u8]> for TrackerState { 63 | fn as_ref(&self) -> &[u8] { 64 | self.0.as_bytes() 65 | } 66 | } 67 | 68 | impl From for String { 69 | fn from(state: TrackerState) -> Self { 70 | state.0 71 | } 72 | } 73 | 74 | impl Display for TrackerState { 75 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 76 | f.write_str(&self.0) 77 | } 78 | } 79 | 80 | impl Trackable for TrackerState { 81 | fn track_with(&self, tracker: &mut ConfigTracker) { 82 | tracker.track(self.0.as_bytes()) 83 | } 84 | } 85 | 86 | impl Trackable for BTreeMap { 87 | fn track_with(&self, tracker: &mut ConfigTracker) { 88 | for (_, v) in self.iter() { 89 | tracker.track(v.as_bytes()); 90 | } 91 | } 92 | } 93 | 94 | impl Trackable for BTreeMap { 95 | fn track_with(&self, tracker: &mut ConfigTracker) { 96 | for (_, v) in self.iter() { 97 | tracker.track(v.0.as_slice()); 98 | } 99 | } 100 | } 101 | 102 | impl Trackable for Secret { 103 | fn track_with(&self, tracker: &mut ConfigTracker) { 104 | if let Some(data) = &self.data { 105 | data.track_with(tracker); 106 | } 107 | } 108 | } 109 | 110 | impl Trackable for ConfigMap { 111 | fn track_with(&self, tracker: &mut ConfigTracker) { 112 | if let Some(data) = &self.data { 113 | data.track_with(tracker); 114 | } 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod test { 120 | use super::*; 121 | 122 | #[test] 123 | fn test() { 124 | let tracker = ConfigTracker::new(); 125 | assert_eq!( 126 | "da39a3ee5e6b4b0d3255bfef95601890afd80709", 127 | tracker.current_hash() 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Jens Reimann and others. 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | use anyhow::Result; 14 | 15 | /// Use the value of something optional, or create it first. 16 | pub trait UseOrCreate { 17 | fn use_or_create(&mut self, f: F) -> R 18 | where 19 | F: FnOnce(&mut T) -> R; 20 | 21 | fn use_or_create_err(&mut self, f: F) -> Result<()> 22 | where 23 | F: FnOnce(&mut T) -> Result<()>, 24 | { 25 | self.use_or_create(|value| f(value)) 26 | } 27 | } 28 | 29 | /// Implementation for `Option`s which wrap `Default`s. 30 | impl UseOrCreate for Option 31 | where 32 | T: Default, 33 | { 34 | fn use_or_create(&mut self, f: F) -> R 35 | where 36 | F: FnOnce(&mut T) -> R, 37 | { 38 | match self { 39 | Some(value) => f(value), 40 | None => { 41 | let mut value = Default::default(); 42 | let result = f(&mut value); 43 | self.replace(value); 44 | result 45 | } 46 | } 47 | } 48 | } 49 | 50 | /// Implementation for `Option`s which wrap `Default`s. 51 | impl UseOrCreate for &mut Option 52 | where 53 | T: Default, 54 | { 55 | fn use_or_create(&mut self, f: F) -> R 56 | where 57 | F: FnOnce(&mut T) -> R, 58 | { 59 | match self { 60 | Some(value) => f(value), 61 | None => { 62 | let mut value = Default::default(); 63 | let result = f(&mut value); 64 | self.replace(value); 65 | result 66 | } 67 | } 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | 74 | use super::*; 75 | 76 | #[derive(Default, Debug)] 77 | struct Example { 78 | foo: String, 79 | } 80 | 81 | #[test] 82 | fn test_with_none() { 83 | let mut v: Option = None; 84 | v.use_or_create(|v| { 85 | v.foo = "bar".to_string(); 86 | }); 87 | 88 | assert!(v.is_some()); 89 | assert_eq!(v.unwrap().foo, "bar"); 90 | } 91 | 92 | #[test] 93 | fn test_with_some() { 94 | let mut v: Option = Some(Example { foo: "foo".into() }); 95 | v.use_or_create(|v| { 96 | assert_eq!(v.foo, "foo"); 97 | v.foo = "bar".to_string(); 98 | }); 99 | 100 | assert!(v.is_some()); 101 | assert_eq!(v.unwrap().foo, "bar"); 102 | } 103 | } 104 | --------------------------------------------------------------------------------