github.com/sleungcy-sap/cli@v7.1.0+incompatible/doc/adr/0005-v7-push-transforms-manifest-before-applying.md (about) 1 # 5. V7 `cf push` Transforms Manifest Before Applying 2 3 Date: 2019-09-16 4 5 ## Status 6 7 Accepted 8 9 ## Context 10 11 > _The issue motivating this decision, and any context that influences or constrains the decision._ 12 13 **NOTE:** This ADR builds on ideas introduced in [the last `push` refactor](/doc/adr/0004-v7-push-refactor.md). If you haven't read that ADR, please do so before reading this one. 14 15 As the V3 Acceleration Team was working on the V7 CLI version of `cf push`, we found that the implementation made certain features difficult (or impossible) to implement. We refactored the first part of `cf push` as described below to solve some of these problems. 16 17 ### The dream of the server-side manifest 18 19 In December 2018, the CAPI, CLI, and V3 Acceleration teams began discussing the idea of "server-side manifests", in which CAPI would build endpoints that received a manifest and made the necessary changes on the API side. Previously, the `push` manifest file was a concept invented and implemented fully by the CLI. 20 21 The results of this work were the V3 [app manifest](https://v3-apidocs.cloudfoundry.org#app-manifest) and [space manifest](https://v3-apidocs.cloudfoundry.org#space-manifest) endpoints. 22 23 One part of the dream of server-side manifests was that the CLI would no longer need to have any knowledge of manifest contents. The `push` command would pass manifest contents directly to the API without ever parsing the YAML. The API would apply the manifest as given, and the CLI would then upload source code for staging. 24 25 The goal of this was to allow CAPI to add additional manifest features (i.e. properties) without requiring any implementation changes on the CLI side. 26 27 See here for more on this topic, including the original goals of server-side manifests: 28 * [V3 Server-Side Multi-App Manifests](https://docs.google.com/document/d/1z-0Ev-QCtuoT8nJCoNaVJmkMPNk4PYi_VGWzc8CwBIs/edit#) 29 * [Server Side Manifest Exploration](https://docs.google.com/document/d/1HKNz5Qaza9fx8QSQp4oWqzcg3Sv1tkyEhm_6qQFaQAM/edit#heading=h.qt2a8po833hn) 30 31 ### Adding support for diffing 32 33 At this point in time, V7 `push` does not print diff output like it does in V6. This is a feature of V6 `push` that we got a lot of positive feedback about, and it is the last piece of `push` we need to implement for parity with V6. 34 35 The diff output is meant to show the difference between the current state of the pushed apps and the desired state (as represented by the manifest and flag overrides), like this: 36 37 ```diff 38 $ cf push -i 3 39 Pushing from manifest to org org / space space as admin... 40 Using manifest file /home/pivotal/workspace/cf-acceptance-tests/assets/dora/manifest.yml 41 Getting app info... 42 Updating app with these attributes... 43 name: dora 44 path: /home/pivotal/go/src/github.com/cloudfoundry/cf-acceptance-tests/assets/dora 45 command: bundle exec rackup config.ru -p $PORT 46 disk quota: 1G 47 health check type: port 48 - instances: 2 49 + instances: 3 50 memory: 51 52 stack: cflinuxfs3 53 routes: 54 dora.sheer-shark.lite.cli.fun 55 ``` 56 57 When we set out to explore adding the diff output to V7, we found there was **no straightforward way to do so**. In V6, we were already parsing the whole manifest and "actualizing" each property individually, but with V7, we intentionally didn't parse the whole manifest, so we did not have knowledge of the full desired state. Without this knowledge, we could not do the comparison necessary to output the diff. 58 59 ### Unfixable bugs due to overrides being handled too late 60 61 There was a whole class of bugs that could not be fixed in V7 `push` before this refactor. These were cases where a manifest property value is invalid, but the corresponding flag override is acceptable. For example, you might have `memory_in_mb: 2000GB` in your manifest, which is over your quota, but you push with `-m 20MB`, which is fine. 62 63 Prior to this change, manifest properties and flag overrides were handled in separate API calls. Manifest properties were applied first exactly as they are in the manifest file. Then if the apply-manifest request was successful, the flag overrides would be handled (e.g. by sending a request to scale the memory to the number specified by the `-m` flag). But if the manifest was invalid (e.g. had a memory setting that would put an app over its quota), the apply-manifest step would fail before even getting to the override step. 64 65 We believe this was confusing and unexpected behavior for users: if a user passes `-m 20MB`, they would expect that to be the only number that mattered (not the memory value they're overriding in the manifest). 66 67 Here is an example of such a bug: 68 * [**CLI** user should not get a failed push when flag overrides are below quota limits but manifest values are above](https://www.pivotaltracker.com/story/show/167576661) 69 70 ## Decision 71 72 > _The change that we're proposing or have agreed to implement._ 73 74 We refactored the first half of V7 `push` (all of the steps that come before uploading source code and creating a new droplet). Read on for details. 75 76 ### How did `push` work before this change? 77 78 Before this change, `push` preserved manifest properties exactly as-is and sent these to the apply-manifest endpoint. 79 The flag overrides would be handled after applying the manifest was complete. 80 81 ### How does `push` work after this change? 82 83 Now, `push` parses the manifest into an in-memory representation of the manifest. It transforms its representation of the manifest based on the given flag overrides, and then generates new YAML to send to the apply-manifest endpoint. 84 85 The effect of this is that we eliminate the extra steps of "fixing" the app after the apply-manifest step runs. For example, we no longer need to send additional requests to scale an app's process when the `-i 4` flag is given. Instead, the manifest that we send to the API will have been modified to say `instances: 4`. See below for a detailed explanation of how this works. 86 87 In addition, the API is now fully responsible for handling the logic of validating and resolving conflicts between manifest properties. 88 89 ### Case study: Lifecycle of a flag override 90 91 To see how this is implemented, we will follow the path that a flag override takes from the user, through the code, to the API. 92 93 Imagine a user runs `cf push -i 4`, with a manifest that looks like: 94 95 ```yaml 96 applications: 97 - name: dora 98 instances: 2 99 ``` 100 101 The `PushCommand` parses the manifest as-written and the given flag overrides. It passes them into `HandleFlagOverrides`, which returns a transformed manifest: 102 103 ```go 104 func (cmd PushCommand) Execute(args []string) error { 105 //... 106 transformedManifest, err := cmd.Actor.HandleFlagOverrides(baseManifest, flagOverrides) 107 if err != nil { 108 return err 109 } 110 //... 111 } 112 ``` 113 114 The `HandleFlagOverrides` method is another example of the hexagonal pattern established in [this refactor](/doc/adr/0004-v7-push-refactor.md). It passes the manifest through a sequence of functions (`TransformManifestSequence`): 115 116 ```go 117 func (actor Actor) HandleFlagOverrides(baseManifest pushmanifestparser.Manifest, flagOverrides FlagOverrides) (pushmanifestparser.Manifest, error) { 118 newManifest := baseManifest 119 120 for _, transformPlan := range actor.TransformManifestSequence { // <== sequence of transform functions 121 var err error 122 newManifest, err = transformPlan(newManifest, flagOverrides) 123 if err != nil { 124 return pushmanifestparser.Manifest{}, err 125 } 126 } 127 128 return newManifest, nil 129 } 130 ``` 131 132 Here is the list of functions in the transform sequence: 133 134 ```go 135 actor.TransformManifestSequence = []HandleFlagOverrideFunc{ 136 // app name override must come first, so it can trim the manifest 137 // from multiple apps down to just one 138 HandleAppNameOverride, 139 140 HandleInstancesOverride, // <== instances transform function! 141 HandleStartCommandOverride, 142 HandleHealthCheckTypeOverride, 143 HandleHealthCheckEndpointOverride, 144 HandleHealthCheckTimeoutOverride, 145 HandleMemoryOverride, 146 HandleDiskOverride, 147 HandleNoRouteOverride, 148 HandleRandomRouteOverride, 149 150 // this must come after all routing related transforms 151 HandleDefaultRouteOverride, 152 153 HandleDockerImageOverride, 154 HandleDockerUsernameOverride, 155 HandleStackOverride, 156 HandleBuildpacksOverride, 157 HandleStrategyOverride, 158 HandleAppPathOverride, 159 HandleDropletPathOverride, 160 } 161 ``` 162 163 This is the `HandleInstancesOverride` method, which is responsible for transforming the manifest based on the `-i/--instances` flag, if given: 164 165 ```go 166 func HandleInstancesOverride(manifest pushmanifestparser.Manifest, overrides FlagOverrides) (pushmanifestparser.Manifest, error) { 167 if overrides.Instances.IsSet { 168 if manifest.ContainsMultipleApps() { 169 return manifest, translatableerror.CommandLineArgsWithMultipleAppsError{} 170 } 171 172 webProcess := manifest.GetFirstAppWebProcess() 173 if webProcess != nil { 174 webProcess.Instances = &overrides.Instances.Value 175 } else { 176 app := manifest.GetFirstApp() 177 app.Instances = &overrides.Instances.Value 178 } 179 } 180 181 return manifest, nil 182 } 183 ``` 184 185 After the manifest is transformed, we generate the following new YAML that is sent to the apply-manifest endpoint: 186 187 ```yaml 188 applications: 189 - name: dora 190 instances: 4 # <== updated! 191 ``` 192 193 From the API's point of view, there are no "flag overrides". The override behavior is resolved fully on the CLI side before the manifest is ever applied. 194 195 ## Consequences 196 197 > _What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated._ 198 199 ### Cleaner separation of concerns between CLI and API 200 201 One key outcome is that we have achieved a clearer separation of concerns between the CLI and the API when it comes to the manifest. The idea of "flag overrides" has always been a CLI-only concept, but there were cases where the responsibilities were getting blurred. 202 203 For example, consider `no-route`, which is available as a flag (`--no-route`) or as a manifest property (`no-route: true`). Before this refactor, in order to apply the manifest exactly as written but still allow the overriding behavior that the CLI required, we introduced `no_route` as a query parameter on the apply-manifest endpoint. So, when `--no-route` was given, the resulting request would look like `POST /v3/spaces/:guid/apply_manifest?no_route=true`. Although this worked, it was an example of a specific, one-off solution to a broader problem. We did not want to add query parameters for every overridable manifest property, and it was an instance of making the API overly-tailored to the CLI's use case. 204 205 With this change, the `--no-route` override is handled fully on the CLI side, before applying the manifest, since flag overrides are the CLI's business. We were able to remove the `no_route` query parameter from the API endpoint and clean up some related code on the API side. 206 207 ### Server-side manifests: closer to or further from the dream? 208 209 If the goal of server-side manifests was to push as much as possible of the work related to validating/applying/resolving configuration changes to the API side, then this refactor gets us much closer to that goal. The CLI now leans fully on the API to apply the manifest, rather than applying the manifest and then "correcting" the configuration with additional API calls. 210 211 However, if the goal of server-side manifests was for the CLI to know as little as possible about the contents of the manifest, then this refactor is a deliberate departure from that goal. In order to apply the flag overrides, the CLI must parse almost the entire manifest. 212 213 #### Risks 214 215 With server-side manifests, we wanted to allow CAPI to introduce new manifest properties that could be leveraged by users (in their manifests) without requiring code changes in the CLI. The **biggest risk** is that after this refactor, CAPI could make a change to the manifest specification that would break users' `push` experience until the CLI releases a new version. 216 217 #### Mitigations 218 219 We accept the risk described above for these reasons: 220 - We designed the CLI's new manifest parser to preserve any unrecognized YAML properties in the in-memory representation of the YAML, so they will be sent along to the API in the end. This means CAPI is free to make additive changes to the manifest specification, and users can leverage them without needing any CLI changes. 221 - If CAPI makes a _breaking_ change to the manifest specification, this will impact the CLI. However, we discussed this with the CAPI team, and they are unlikely to do so anytime soon, since it will also force any users to refactor their manifests (and require changes in any other clients dependent on the manifest spec). If they do want to make a breaking change, they will likely introduce the idea of _versioned manifests_ and continue to support multiple manifest specifications for some time. 222 223 ### Fewer leftovers when `push` fails 224 Before the refactor, failed pushes could result in state changes. These changes would not get rolled back after the `push` exited with an error. 225 226 Setup: 227 Suppose the CLI user started with no existing app and a space memory quota of 500 MB. 228 If they supplied a manifest: 229 ```yaml 230 applications: 231 - name: dora 232 instances: 50 233 memory_in_mb: 10 234 ``` 235 236 And they pushed with this command: 237 `cf push -m 20MB` 238 239 240 Before this refactor: 241 `push` would apply the manifest to create 50 instances, with 10 MB allotted per instance. In this case, the allotted memory would be within the quota, so the manifest would be applied successfully. 242 Once apply manifest succeeded, a separate API call would apply the flag override to allot 20MB per instance instead. However in this case, `push` would fail with an error indicating that the desired memory is over quota. 243 However the 50 instances created would stick around despite the failed `push`. 244 245 246 After this refactor: 247 `push` transforms the in-memory representation of the manifest to the following: 248 ```yaml 249 applications: 250 - name: dora 251 instances: 50 252 memory_in_mb: 20 253 ``` 254 `push` would apply the manifest, and would fail with a validation error indicating that the desired memory is over quota. Therefore we never even get to the step of scaling app instances. 255 256 ### Fewer API requests per app 257 258 A nice side-effect of this refactor is that the number of API calls required to push an app decreases considerably. Previously, to apply the manifest properties, we had to make one call to apply the manifest and then `n` calls for each app that we're pushing (where `n` is roughly the number of flag overrides). Now, we only have to make one call to apply the manifest for all pushed apps. 259 260 This means that `push` is faster, more resilient to flaky network connections (less likely to fail in the middle due to a poor connection), and requires less code to implement. 261 262 ### Maintainability 263 264 This refactor builds on the hexagonal architecture established in [this push refactor](/doc/adr/0004-v7-push-refactor.md). This makes `push` more consistent and easier to reason about. 265 266 ### Diff possibility 267 268 Now, we have a simpler path towards implementing the diff output feature in V7. We now have a representation of the full desired manifest, after flag overrides have been applied. We can make [a request to the API for the current manifest](https://v3-apidocs.cloudfoundry.org/version/release-candidate/#generate-an-app-manifest) and compare the two manifests. 269 270 ### Better captures user intent of flag overrides superceding manifest properties 271 272 We believe that if a user runs `cf push -i 4` with a manifest that looks like this: 273 ```yaml 274 applications: 275 - name: dora 276 instances: 1 277 ``` 278 ...then their intention is really to push with 4 instances. Before the refactor, we would create one instance first, and then scale it up to 4. With the refactor, we feel we are better capturing user intent from the outset (without the intermediate step where an "incorrect" manifest is applied). 279