Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add multi-part yaml support to GetChanges for the bundle facade #13448

Merged
merged 8 commits into from Nov 4, 2021

Conversation

cderici
Copy link
Member

@cderici cderici commented Oct 22, 2021

Description

pylibjuju uses the bundle facade to get the changes for a bundle to be deployed. However, currently the GetChanges function that the facade uses only handles single-part yaml files, so this PR adds the support for multi-part yaml handling (which the Juju cli uses) to enable the use of overlays in bundles.

QA steps

  $ cd juju/apiserver/facades/client/bundle/
  $ go test -gocheck.f TestGetChangesWithOverlaysV6
  $ go test -gocheck.f TestGetChangesWithOverlaysV1_5

Notes & Discussion

This PR is needed for juju/python-libjuju#566, which adds the support for overlays in deploying bundles.

Copy link
Contributor

@achilleasa achilleasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code as-is should work fine for the majority of bundles. However, there are some (non-trivial) edge cases that you need to address before landing this:

  1. Juju deploy allows you to specify a base bundle (+optional overlay(s) if you use the multi-doc syntax) plus any number of explicitly defined overlay files (e.g. juju deploy ./multidoc.yaml --overlay foo.yaml --overlay bar.yaml). You can technically concatenate them client-side before calling the facade (just add them as extra docs after the base). However, (there is a tricky bit here:
  2. Bundles support an "include" directive (search for include-file here). The include path is relative to the overlay location which is why you see this mechanism with multiple bundle data sources. Since the facade logic cannot report back errors like "give me file X" you will need to emulate this logic and expand include-{file,base64} directives client-side where you can access the files.
  3. Bundles/overlays can reference local charms (again, paths are relative to the thing that references them). I am not 100% sure how this can be made to work easily with the python client.

For point 2, you can take a look at the QA steps here to get a better idea about how it is supposed to work.

Note however that when the server-side bundle deployment get implemented, this will all become much easier for both the cli and pylibjuju as they just need to implement a simple challenge/response protocol and the controller will do all the grunt work.

@cderici
Copy link
Member Author

cderici commented Oct 22, 2021

Changes, along with their passing tests are posted on juju/python-libjuju#566 for all the three points:

  1. Juju deploy allows you to specify a base bundle (+optional overlay(s) if you use the multi-doc syntax) plus any number of explicitly defined overlay files (e.g. juju deploy ./multidoc.yaml --overlay foo.yaml --overlay bar.yaml). You can technically concatenate them client-side before calling the facade (just add them as extra docs after the base). However, (there is a tricky bit here:

See the test named test_deploy_bundle_with_multiple_overlays_with_include_files in the tests file. Corresponding yamls for the overlays are posted as well.

  1. Bundles support an "include" directive (search for include-file here). The include path is relative to the overlay location which is why you see this mechanism with multiple bundle data sources. Since the facade logic cannot report back errors like "give me file X" you will need to emulate this logic and expand include-{file,base64} directives client-side where you can access the files.

See the tests named test_deploy_local_bundle_include_file and test_deploy_local_bundle_include_base64 in the tests file.

  1. Bundles/overlays can reference local charms (again, paths are relative to the thing that references them). I am not 100% sure how this can be made to work easily with the python client.

Turns out this is already working in pylibjuju, see this test, where the local.yaml file contents is:

applications:
  test1:
    charm: "../charm"
    num_units: 1
  test2:
    charm: "../charm.charm"
    num_units: 1

So as you suggested, include-file and include-base64 directives are being handled in the client-side 👍

@cderici
Copy link
Member Author

cderici commented Oct 22, 2021

@hpidcock hpidcock added the 2.9 label Oct 25, 2021
Copy link
Member

@SimonRichardson SimonRichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to bump the API. Current implementations might break if they see a multipart yaml doc and we don't want to do that. So the change is good if we add a new API revision.

@cderici
Copy link
Member Author

cderici commented Oct 25, 2021

We need to bump the API. Current implementations might break if they see a multipart yaml doc and we don't want to do that. So the change is good if we add a new API revision.

I don't think I follow, the only change this has is for GetChanges to take into account all the parts of a multipart yaml when returning a set of Changes, rather than the current behaviour of processing only the first part and ignoring the rest. So whoever's currently using it with a single part yaml will still be able to do that. And whoever wants to use it with a multipart yaml will still get a set of Changes. I guess I don't know what exactly this breaks.

Copy link
Member

@SimonRichardson SimonRichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there. The facades are bumped correctly, we just need to wire up the different function calls depending on the version of the API.

@@ -219,7 +237,8 @@ type validators struct {
func getBundleChanges(args params.BundleChangesParams,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for the v1-v5 of the api, we want to call the old getBundleChanges. For the v6 of the api we want to call this new method.

@@ -213,6 +213,109 @@ func (s *bundleSuite) TestGetChangesSuccessV2(c *gc.C) {
c.Assert(r.Errors, gc.IsNil)
}

func (s *bundleSuite) TestGetChangesSuccessV2WithOverlays(c *gc.C) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please rename to TestGetChangesSuccessWithOverlays. V2 is confusing as V5 fails with overlays.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure why I named it V2 tbh, must be a copy paste overlook, good catch!

`,
}
api := s.makeAPI(c)
apiv1 := &bundle.APIv1{&bundle.APIv2{&bundle.APIv3{&bundle.APIv4{&bundle.APIv5{api}}}}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test fails when: apiv5 := &bundle.APIv5{api}. Looks like we're calling the wrong getChanges(). Will need a test for v1, v5 and the most recent version.

r, err := apiv1.GetChanges(args)
c.Assert(err, jc.ErrorIsNil)
// This is the inverted test of TestGetChangesSuccessV2WithOverlays
c.Assert(r.Changes, gc.Not(jc.DeepEquals), []*params.BundleChange{{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this an explicit check for content. Not here could be almost empty.

@cderici
Copy link
Member Author

cderici commented Oct 26, 2021

@hmlanigan thanks for pointing out the issues! Hope I was able to make things a bit clearer:

  $ cd juju/apiserver/facades/client/bundle/
  $ go test -gocheck.f TestGetChangesWithOverlaysV6
  $ go test -gocheck.f TestGetChangesWithOverlaysV1_5

Copy link
Member

@SimonRichardson SimonRichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm approving to unlock the request changes blocker.

I haven't Q&A'd this yet though.

@@ -166,7 +183,7 @@ func NewBundleAPIv1(
if err != nil {
return nil, errors.Trace(err)
}
return &APIv1{&APIv2{&APIv3{&APIv4{&APIv5{api}}}}}, nil
return &APIv1{&APIv2{&APIv3{&APIv4{&APIv5{&APIv6{api}}}}}}, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like fun...

return err
},
}
return b.getChanges(args, vs, func(changes []bundlechanges.Change, results *params.BundleChangesResults) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to duplicate all this code makes it really hard to track what calls what.

How about we change the b.getChanges signature to include a boolean flag (or alternatively, a typed enum for more clarity) which indicates the parse mode (e.g. includeOverlays) to use?

Then you can extract the common GetChanges logic into a private method which accepts this flag and just provide two tiny stubs for APIV5 and BundleAPI which pass the right value through.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the duplication adds more indirection on the control flow. I initially tried to duplicate only the b.getChanges but it didn't work for reasons related to Go semantics that I don't understand yet.

However, if I'm understanding your proposal correctly, I'm also doubtful that adding another mechanic to separate functionality between versions would make this less confusing. Especially when more version breaks are added in the future, this might potentially turn into (already existing) duplicate codes with big switches in some of the functions.

Also, code duplication is often really bad for like maintainance/debug etc, but this duplication is like grabbing a snapshot of the current version and moving on, so day 2 ops are still gonna be done on the func (b *BundleAPI) GetChanges (unless it's specifically about <=APIv5). On the other hand, adding anohter argument would sort of cram all the logics into one spot, thereby forcing anyone maintaining/improving it in the future to understand all of it and make sure they're not breaking any other version logic.

Copy link
Contributor

@achilleasa achilleasa Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion was something along these lines:

func (b *APIv5) GetChanges(args params.BundleChangesParams) (params.BundleChangesResults, error) {
 return b.doGetChanges(args, false)
}

// Versions 6+
func (b *BundleAPI) GetChanges(args params.BundleChangesParams) (params.BundleChangesResults, error) {
 return b.doGetChanges(args, true)
}

// You could also pass validators as an arg and use the same method for pre V5 facades
func (b *BundleAPI) doGetChanges(args params.BundleChangesParams, expandOverlays bool) (params.BundleChangesResults, error) {
    vs := validators{
		verifyConstraints: func(s string) error {
			_, err := constraints.Parse(s)
			return err
		},
		verifyStorage: func(s string) error {
			_, err := storage.ParseConstraints(s)
			return err
		},
		verifyDevices: func(s string) error {
			_, err := devices.ParseConstraints(s)
			return err
		},
	}
	return b.getBundleChanges(args, vs, expandOverlays, mapBundleChanges)
}

func mapBundleChanges(changes []bundlechanges.Change, results *params.BundleChangesResults) error {
  results.Changes = make([]*params.BundleChange, len(changes))
  for i, c := range changes {
	  var guiArgs []interface{}
	  switch c := c.(type) {
	  case *bundlechanges.AddApplicationChange:
		  guiArgs = c.GUIArgsWithDevices()
	  default:
		  guiArgs = c.GUIArgs()
	  }
	  results.Changes[i] = &params.BundleChange{
		  Id:       c.Id(),
		  Method:   c.Method(),
		  Args:     guiArgs,
		  Requires: c.Requires(),
	  }
  }
  return nil
}

func (b *BundleAPI) getBundleChanges(...){
  // merge getChanges + getBundleChanges logic
}

The way that the facade abuses inheritance should allow this to work but as you can see the code is much more compact. Also, note that when we move to server-side bundle expansion this facade will become obsolete as we will be using the same code path for both the juju cli and pylibjuju and the controller will take care of calculating and applying the required changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@achilleasa Ah ok I see what you're saying, I agree, this looks much better, I'll see what I can do, thanks for the input 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to add this handy snippet if you want to add some more context instead of passing true/false:

const (
   ignoreOverlays = false
   includeOverlays = true
)

...

func (b *APIv5) GetChanges(args params.BundleChangesParams) (params.BundleChangesResults, error) {
 return b.doGetChanges(args, ignoreOverlays)
}

// Versions 6+
func (b *BundleAPI) GetChanges(args params.BundleChangesParams) (params.BundleChangesResults, error) {
 return b.doGetChanges(args, includeOverlays)
}

@@ -196,7 +244,8 @@ func (b *APIv1) GetChanges(args params.BundleChangesParams) (params.BundleChange
},
verifyDevices: nil,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line can be omitted as the default zero value is nil.

@@ -196,7 +244,8 @@ func (b *APIv1) GetChanges(args params.BundleChangesParams) (params.BundleChange
},
verifyDevices: nil,
}
return getChanges(args, vs, func(changes []bundlechanges.Change, results *params.BundleChangesResults) error {

postProcess := func(changes []bundlechanges.Change, results *params.BundleChangesResults) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend extracting this, and the v5 below (L270) to standalone functions to improve readability.

apiserver/facades/client/bundle/bundle.go Outdated Show resolved Hide resolved
Copy link
Contributor

@achilleasa achilleasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for applying the extra changes to clean up the code. Happy to see this landed if CI is green.

@cderici
Copy link
Member Author

cderici commented Nov 4, 2021

$$merge$$

@jujubot jujubot merged commit 8a7c9d7 into juju:2.9 Nov 4, 2021
@juanmanuel-tirado
Copy link
Contributor

JUJU-142

jujubot added a commit to juju/python-libjuju that referenced this pull request Nov 8, 2021
#566

### Description

This PR adds the support for overlays in bundle deployments.

Fixes #510 

This PR relies on a change in Juju's api for getting changes for bundles with overlays (multi-part yaml support), juju/juju#13448.

Jira card [#142](https://warthogs.atlassian.net/browse/JUJU-142)



### QA Steps

`tests/integration/test_model.py` includes some new tests for the added support.

```
tox -e integration -- tests/integration/test_model.py
```

### Notes & Discussion

Please do not merge yet, as a couple of small things need to be done/added for this to be ready to land:

- [x] Add a PR on Juju for `GetChange` juju/juju#13448
- [x] Land that PR on Juju
- [x] Charmstore bundles with `--overlay` argument, along with its test
- [x] A test for a multi-part overlay as an `--overlay` argument to a local bundle being deployed
- [x] A test for a multi-part overlay as an `--overlay` argument to a charmstore bundle being deployed
- [x] Make sure that we resolve and inline `config: include-file://` and `config: include-base64://` here in `pylibjuju` side
@wallyworld wallyworld mentioned this pull request Nov 26, 2021
jujubot added a commit that referenced this pull request Nov 26, 2021
#13528

Merge 2.9

#13461 Enhanced logging detail when starting side-car agents
#13462 Context queue
#13464 Improve run hook error when charm is missing
#13468 Skip check for microk8s user group on macOS
#13472 Update to latest version of Pebble (exec terminal/interactive params)
#13448 add multi-part yaml support to GetChanges for the bundle facade
#13465 [JUJU-112] manifests and blobs api for registries
#13460 Allow NotifyTarget to return an error
#13482 [JUJU-136] enable juju_machine_lock to display unit machine lock status.
#13477 Added lower bound for run_action wait timeout.
#13490 [JUJU-121] Adds support for auto create of instance profiles.
#13425 [2.9] Expose supported features in show model output
#13426 Remove deprecated juju cli flags
#13422 Provision OpenStack instances with NICs for bindings
#13423 Fix add-machine command help for zones constraint
#13424 Fixed a typo in README.md
#13513 [JUJU-223] Fix panic when retrieving application config
#13517 [JUJU-227] Stop the CAAS storage provisioner worker if the application is dead;
#13519 [JUJU-240] Use the storage prefix when selecting pvcs for sidecar charms
#13520 [JUJU-245] Do not error with permission denied if a controller tries to access removed storage
#13523 Logging for link-layer device updates
#13501 [JUJU-197] Raft lease retries with exceeded dropped
#13514 [JUJU-230] Add additional unit test for WatchAllModels api
#13512 [JUJU-208] Lease Diff
#13515 Remove unused ExportModel method
#13516 [JUJU-235] Remove bogus attachment dead warnings
#13518 [JUJU-237] Fix the failure to update homebrew on none juju/juju
#13498 [JUJU-113] Refactor upgrade controller command
#13502 [JUJU-199] Remove an an old legacy cmr fallback and add upgrade step
#13506 [JUJU-207] Apply synchronously, respond asynchronously
#13508 [JUJU-176] Fix mongo service enable by using start not restart.
#13510 [JUJU-221] Fix vol attachment life asserts for force remove
#13511 [JUJU-215] Ignore notfound error from fetching pod status for updating unit status;
#13500 [JUJU-192] Use yaml.safe_load instead of yaml.load
#13503 [JUJU-202] Lease manager test fix and trace logging
#13504 [JUJU-195] Format logs for mongo version more nicely
#13505 [JUJU-193] [2.9] Populate supported assumes features for k8s provider
#13480 Added Jammy series as supported.
#13488 [JUJU-117] Don't bake defaults when updating config.
#13492 [JUJU-180] Correct affected unit reporting in upgrade-series
#13495 [JUJU-186] Raft client logging levels
#13496 [JUJU-187] Improve the help text for --keep-broken flag
#13493 [JUJU-185] Use the correct ingress resource version for the k8s cluster
#13491 [JUJU-178] Preserve old semantics for run-action concerning wait argument
#13489 [JUJU-177] Check for available disk space before doing a juju backup

```
# Conflicts:
# apiserver/facades/agent/provisioner/provisioninginfo_test.go
# apiserver/facades/client/application/application.go
# apiserver/facades/client/application/application_unit_test.go
# apiserver/facades/client/backups/create.go
# apiserver/facades/client/client/client.go
# apiserver/facades/client/client/client_test.go
# apiserver/facades/client/client/statushistory_test.go
# cmd/juju/action/runaction.go
# cmd/juju/action/runaction_test.go
# cmd/juju/action/waitflag.go
# cmd/juju/commands/upgrademodel.go
# cmd/juju/commands/upgrademodel_test.go
# cmd/jujud/agent/agenttest/agent.go
# go.mod
# go.sum
# mongo/mongo.go
# provider/azure/environ.go
# provider/azure/export_test.go
# provider/azure/storage.go
# provider/azure/storage_test.go
# scripts/win-installer/setup.iss
# snap/snapcraft.yaml
# state/backups/backups.go
# state/backups/backups_test.go
# state/backups/db.go
# state/backups/db_dump_test.go
# state/backups/export_test.go
# state/backups/files_test.go
# version/version.go
```

## QA steps

See PRs

[JUJU-112]: https://warthogs.atlassian.net/browse/JUJU-112?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[JUJU-136]: https://warthogs.atlassian.net/browse/JUJU-136?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[JUJU-121]: https://warthogs.atlassian.net/browse/JUJU-121?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[JUJU-223]: https://warthogs.atlassian.net/browse/JUJU-223?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[JUJU-227]: https://warthogs.atlassian.net/browse/JUJU-227?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
@cderici cderici deleted the bundle-facade-get-changes-multi-yaml branch February 16, 2022 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
7 participants