kubebuilder icon indicating copy to clipboard operation
kubebuilder copied to clipboard

:sparkles: implement `create api --workspace` flag for go1.18 style go.work support

Open jakobmoellerdev opened this issue 3 years ago • 2 comments
trafficstars

This PR attempts to create a backwards-compatible go.work support in go/v3 / go/v4-alpha: Fixes https://github.com/kubernetes-sigs/kubebuilder/issues/2627

The basic implementation uses a flag called --workspace that can be used in create API like

kubebuilder create api --version v1alpha1 --kind TestKind --group group --workspace, which leads to a go.work being created with the following scaffold:

go 1.18

use ./api/v1alpha1

//+kubebuilder:scaffold:apis

use .

It also changes the PostScaffolding behavior of the create apicommand by using go mod sync instead of go mod tidy to automatically resolve all modules in the workspace. This allows us to not rely on replace statements which would be harder to scaffold and maintain. Obvious downside to this is that it supports go 1.18 and above only.

I want to use this PR first in a draft state to collect input and feedback before progressing it and writing tests.

All existing Makefile commands as well as the already scaffolded go.modstays compatible, however go mod tidywill not work on the core module on its own. (since go.work is necessary to resolve the other modules relative to the base module). Notice that for external developers, you will be able to download the module as normal and also depend on the API modules individually without actually having to import the controller.

jakobmoellerdev avatar Aug 16 '22 20:08 jakobmoellerdev

Hi @jakobmoellersap. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

k8s-ci-robot avatar Aug 16 '22 20:08 k8s-ci-robot

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: jakobmoellersap Once this PR has been reviewed and has the lgtm label, please assign pwittrock for approval by writing /assign @pwittrock in a comment. For more information see:The Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

k8s-ci-robot avatar Aug 16 '22 20:08 k8s-ci-robot

/cc @camilamacedo86

jakobmoellerdev avatar Aug 18 '22 14:08 jakobmoellerdev

/ok-to-test

camilamacedo86 avatar Aug 23 '22 03:08 camilamacedo86

/test pull-kubebuilder-e2e-k8s-1-24-1

jakobmoellerdev avatar Aug 26 '22 07:08 jakobmoellerdev

/retest-required

jakobmoellerdev avatar Aug 26 '22 07:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-23-3

jakobmoellerdev avatar Aug 26 '22 07:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-22-1

jakobmoellerdev avatar Aug 26 '22 08:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-21-2

jakobmoellerdev avatar Aug 26 '22 08:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-19-16

jakobmoellerdev avatar Aug 26 '22 08:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-20-7

jakobmoellerdev avatar Aug 26 '22 08:08 jakobmoellerdev

Putting this as ready to review, please provide feedback before I proceed further

jakobmoellerdev avatar Aug 26 '22 10:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-19-16

jakobmoellerdev avatar Aug 26 '22 11:08 jakobmoellerdev

/test pull-kubebuilder-e2e-k8s-1-19-16

jakobmoellerdev avatar Aug 26 '22 13:08 jakobmoellerdev

/hold

there is one big thing open that is requiring the go.mod file contents in the root

jakobmoellerdev avatar Aug 26 '22 14:08 jakobmoellerdev

@jakobmoellersap I like the ideas behind this change, and I think the work you have done looks great. I just have a few questions:

  • My understanding is that this introduces a breaking change for the create api subcommand for the go/v3 plugin. Is this correct? If so, I think there is probably some hesitation in going this route since the go/v3 scaffolding logic is already widely used by both Kubebuilder and Operator SDK users when building Go based operators.
  • Does this have to be done in the scaffolding of the create api subcommand?
    • Have we considered potentially using the edit subcommand to facilitate these changes? I think if we could make the changes in the edit subcommand we wouldn't have to implement any breaking changes to the existing create api logic and rather just introduce a new non-breaking change to the edit subcommand.

everettraven avatar Aug 26 '22 16:08 everettraven

@everettraven Thanks so much for taking a look!

  • My understanding is that this introduces a breaking change for the create api subcommand for the go/v3 plugin. Is this correct? If so, I think there is probably some hesitation in going this route since the go/v3 scaffolding logic is already widely used by both Kubebuilder and Operator SDK users when building Go based operators.

I tried to make the entire change backwards-compatible with v3. You should be able to still run all tests and commands like before and you will not even see a difference. It will only show up once you call kubebuilder init --workspace. This is because I changed my original approach from having the flag in the create api command to now be in init instead. There are two changes necessary in the Go API of the module which I tried to solve by creating a second method (for initializing the Scaffolder) with one more parameter and referencing the old methods as deprecated. Since the additional parameter is bool you can even call it with the default value of false and it will behave as the old API. This is also why all API compatibility tests are still green and all cluster tests run just fine.

  • Does this have to be done in the scaffolding of the create api subcommand?

No => you are right it, doesn't have to be in the create api. Compared to the very beginning, I changed the main scaffolding of the go.work file to be included in the init command. the create api commands will only alter their behavior when a go.work file is detected.

  • Have we considered potentially using the edit subcommand to facilitate these changes? I think if we could make the changes in the edit subcommand we wouldn't have to implement any breaking changes to the existing create api logic and rather just introduce a new non-breaking change to the edit subcommand.

I mentioned this as well in the issue and honestly I would like to use it, but I think there are some drawbacks due to the existing kubebuilder scaffolding infrastructure:

  • It is not compatible with the scaffolding API and we would have to use an extra scaffolding that is not done before in kubebuilder based on a command. (right now we only use go mod tidy and make commands and we only do it in PreScaffold or PostScaffold after the actual scaffolding already ran)
  • If we use the in-code dependency we will have to include it in-code as a dependency and work with their API, namely we would need a combination of the contents of RepositoryMixIn and TemplateMixIn. I think this is an option as well! I am open for feedback here. We would not have to use a kubebuilder scaffolding annotation that way.

Your comment regarding using kubebuilder edit is totally valid though, and I think I should consider putting the flag there instead of on kubebuilder init.

jakobmoellerdev avatar Aug 26 '22 18:08 jakobmoellerdev

One more addition here that I want to put up for discussion: I tried to make this run with purely go.mod and replace directives. However there is a huge problem with this: You cannot reference a module that has not yet been pushed to the remote (e.g. Github).

For example:

module github.com/jakobmoellersap/test.com

go 1.18

require (
        sigs.k8s.io/controller-runtime v0.12.2
        github.com/jakobmoellersap/test.com/api/v1alpha1 0.0.0 //replace with released version later on
        //+kubebuilder:scaffold:module-dependency
)

together with

replace github.com/jakobmoellersap/test.com/api/v1alpha1 => ./api/v1alpha1

will break even though we replaced it, no matter if you use go mod tidy or go work sync. This is because of the following error:

go: github.com/jakobmoellersap/test.com/api/[email protected]: reading github.com/jakobmoellersap/test.com/api/v1alpha1/go.mod at revision api/v1alpha1/v0.0.0: git ls-remote -q origin in modcachepath/cache/vcs/d8b5eb86b10c53eaa4c311cd6dd4ef713ab591d0cf9c05f7709ed82b6de97ab5: exit status 128:
        remote: Repository not found.
        fatal: repository 'https://github.com/jakobmoellersap/test.com/' not found

jakobmoellerdev avatar Aug 26 '22 19:08 jakobmoellerdev

@jakobmoellersap thanks for taking the time to respond to my previous comment, this all makes sense now. Apologies for missing a couple of the things you mentioned already describing in the issue :sweat_smile: .

I am leaning towards the idea of introducing the functionality into the kubebuilder edit subcommand for the go/v3 plugin. To me this seems a little bit like wanting the ability to "edit" the default scaffolding so that Go workspaces can be utilized - kind of similar to how the kubebuilder edit subcommand can be used to enable multigroup functionality. I am okay with having to make some changes to the scaffolding setup of the edit command to accommodate this type of change under the hood (I think all the subcommands should be able to implement the same scaffolding logic).

One thing I will note, is that if using Go workspaces is a good idea to start setting up as a best practice for the way that operators are scaffolded, I would be okay with adjusting the go/v4-alpha plugin to start using Go workspaces by default (unless we are concerned about the need to use go/v4-alpha scaffolds with a Go version < 1.18).

What do you think?

everettraven avatar Aug 26 '22 19:08 everettraven

Thanks for the insight and review guys, I will adopt the PR to make use of the kubebuilder edit command and the PROJECT configuration file.

jakobmoellerdev avatar Aug 29 '22 04:08 jakobmoellerdev

After spending more time into research, I think workspaces also can have major disadvantages. The best practice way of go is still to use one module per repository. That is to say that I think the pure best practice here would be to

  • create separate repositories and check them into vcs separately
  • maintain two modules but have to undergo a lot of hands-on effort as you will need to check one module into VCS before checking in the other (otherwise the problem I described above becomes present)

I used the following resources as references: https://github.com/pachyderm/pachyderm/pull/3870 https://github.com/golang/go/wiki/Modules#should-i-have-multiple-modules-in-a-single-repository https://github.com/golang/go/issues/27056 https://go.googlesource.com/proposal/+/master/design/45713-workspace.md#rationale-the-file https://go.dev/doc/modules/managing-source https://go.dev/doc/modules/developing https://go.dev/doc/modules/version-numbers

I also looked way deeper into the go docs now on recommendations of multi-modules, and to be honest, what we are trying here with the PR is probably not an easy to achieve solution in light of the go ecosystem:

From the golang wiki:

Two example scenarios where it can make sense to have more than one go.mod in a repository:

if you have usage examples where the examples themselves have a complex set of dependencies (e.g., perhaps you have a small package but include an example of using your package with kubernetes). In that case, it can make sense for your repository to have an example or _example directory with its own go.mod, such as shown here.

if you have a repository with a complex set of dependencies, but you have a client API with a smaller set of dependencies. In some cases, it might make sense to have an api or clientapi or similar directory with its own go.mod, or to separate out that clientapi into its own repository.

However, for both of those cases, if you are considering creating a multi-module repository for performance or download size for a large set of indirect dependencies, you are strongly encouraged to first try with a GOPROXY, which will be enabled by default in Go 1.13. Using a GOPROXY mostly equals any performance benefits or dependency download size benefits that might otherwise come from creating a multi-module repository.

What I can safely say by now is that go.work files are not meant to be checked into VCS, but rather replace the local replace directives one has to issue if working with multiple modules locally. This also means we're not talking about a pure best practice solution anymore. Instead the recommendation is to use a completely different repository.

I will also not be able to make the main go.mod file work independently without a go.mod file containing a proper replace directive. But we cannot add the replace directive without first checking in the API Module

TLDR: I have a problem now since I think we cannot solve this issue without interacting with the VCS first in the edit command to first create the API Module before initializing the second module and then issuing a replace statement.

The only way I can see currently to solve this is to also interact with the VCS, see https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository

jakobmoellerdev avatar Aug 29 '22 06:08 jakobmoellerdev

My understanding now is that the go.work workspaces are meant only for local development purposes as a substitute for the replace directives that one would put into their go.mod files when working with multiple modules. To me, this means that it doesn't quite solve the problem of wanting to be able to have the api sub-module be its own importable module with a smaller set of dependencies.

It is also my understanding that the best practice in its purest form is to have separate repositories - which I don't think will be reasonably feasible.

@jakobmoellersap Looking through some of the resources that you included, I feel it may be best to stray away from the Go workspaces since I don't think it quite solves the problem referenced in #2627. It also seems that it is likely our best route to go the direction of introducing the go.mod to the api sub-module to make it its own separate module. This ends up in the creation of a multi-module repository which while isn't absolute best practice, is acceptable when necessary. As mentioned in the Go wiki's "should I have multiple modules in the same repository?" section:

if you have a repository with a complex set of dependencies, but you have a client API with a smaller set of dependencies. In some cases, it might make sense to have an api or clientapi or similar directory with its own go.mod, or to separate out that clientapi into its own repository.

I think our use case aligns with this point and would warrant the creation of a multi-module repository. If we do this at the time of scaffolding we shouldn't have to do any weird VCS manipulation. I think we would fall under the first problem class mentioned in the Go wiki's "is it possible to add a module to a multi-module repository?" section:

The first class: the package to which the module is being added to is not in version control yet (a new package). This case is straightforward: add the package and the go.mod in the same commit, tag the commit, and push.

TLDR: My suggestion is we drop the Go workspaces approach and instead try the multi-module route that was originally suggested in #2627

@camilamacedo86 @jakobmoellersap What do you think?

everettraven avatar Aug 29 '22 13:08 everettraven

@everettraven Agree with all of the above, except for

I think our use case aligns with this point and would warrant the creation of a multi-module repository. If we do this at the time of scaffolding we shouldn't have to do any weird VCS manipulation.

I think the first case, while straight-forward, is a bit misleading to read:

The first class: the package to which the module is being added to is not in version control yet (a new package). This case is straightforward: add the package and the go.mod in the same commit, tag the commit, and push.

This means that to make this work correctly we would still have to create a tag for the release of the module something like v1.0.0-alpha1 and push it to make the go.mod work. Before the git push, go mod tidy will not work since the VCS cannot be targeted and the go commands will throw an error trying to resolve the module.

So the workflow for multi-module creation would be:

  1. kubebuilder init
  2. kubebuilder edit --multi-module-project=true => this will lead to the go mod entry being created as a dependency.
  3. go mod tidy => fails because the module is not available in VCS (even when trying to use a replace directive)
  4. git commit -am "kubebuilder multi-module project initialization"
  5. git tag api/v1alpha1/v1.0.0-alpha1
  6. git push
  7. go mod tidy => now succeds, and can also be used together with replace statement

This is the same problem I encountered in https://github.com/kubernetes-sigs/kubebuilder/pull/2880#issuecomment-1228829539

What would be your suggestion to solve this @everettraven @camilamacedo86 ? If we do not interact with VCS here after creating the second module and depending on it, I think there is no way for kubebuilder to ensure project integrity (or that go mod tidy will run)

I think still that we should close this PR as I do not think workspaces are the way forward here. Question is how else are we gonna achieve this.

jakobmoellerdev avatar Aug 30 '22 05:08 jakobmoellerdev

Hi @jakobmoellersap,

Regards your comment: https://github.com/kubernetes-sigs/kubebuilder/pull/2880#issuecomment-1231172728 Why that will fails? Why we need to scaffold the files without any api scaffold? Also, I do not think that we need to git add at all we just need to install the module inside of the api dir before running go mod tidy for the project.

Why not: Create a new plugin that enables it then when we call init|edit with plugins=multi-module/v1alpha we will:

  • a) update the Dockerfile and track it in the Project file (such as it is done with all plugins)
  • b) in this case this plugin must be tracked under the layout such as we do with the declarative one. see: https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v3-addon-and-grafana/PROJECT#L4
  • c) check if has Resources created if not stop if yes, then scaffold the files on the api dir

Therefore, when we call the create api (we do not need to pass the multi-module/v1alpha ) the create api subcommand implemented for this plugin will be called in the chain always then we can:

  • a) the templates under creating api should be configured with f.IfExistsAction = machinery.SkipFile so that if they are created already we will not overwrite them. example: https://github.com/kubernetes-sigs/kubebuilder/blob/a44f85fab72ebbe5bb4f6dfbff9f12838c62a25b/pkg/plugins/golang/declarative/v1/scaffolds/internal/templates/channel.go#L44

WDYT?

c/c @everettraven

camilamacedo86 avatar Aug 30 '22 10:08 camilamacedo86

@camilamacedo86 I think I misunderstood you earlier and retried the approach manually.

The main question for me now is in the replace statement. Is it acceptable to have a replace directive like

require github.com/jakobmoellersap/someothertest.com/api/v1alpha1 v0.0.1
replace github.com/jakobmoellersap/someothertest.com/api/v1alpha1 => ./api/v1alpha1

in the main go.mod. This will allow the resolution to work but it will also mean that there can be a discrepancy between the "real" v0.0.1 and the one replaced locally. If i do not issue the replace statement, the module will not run.

If it is, then I think we can go ahead with a separate plugin as mentioned above

jakobmoellerdev avatar Aug 30 '22 13:08 jakobmoellerdev

@jakobmoellersap @camilamacedo86 I think having it be a new plugin is reasonable. I think it would be okay to have that replace directive added to the main go.mod. I do wonder if we go that route if we should leave a comment above the replace directive mentioning why it is there and when it may be appropriate to comment it out (or remove it)

everettraven avatar Aug 30 '22 13:08 everettraven

@everettraven @camilamacedo86 I created the new flow with a new Plugin here: https://github.com/kubernetes-sigs/kubebuilder/pull/2901,

please have a look if you find time.

Side-Note: I think we can take our learnings here and actually use a go.work file to create a local development context (which we put in .gitignore). Then we do not have to use the replace-statement. We could then use the proper build without go.work in the dockerfile but would have to put a disclaimer in the plugin that this won't work until the release tags are set properly. For now I will leave that out and use a simple replace in the go.mod

jakobmoellerdev avatar Aug 30 '22 17:08 jakobmoellerdev

Closing here in favor of the next PR as this will surely not be merged.

jakobmoellerdev avatar Aug 31 '22 07:08 jakobmoellerdev