github.com/cloudfoundry/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