feat: Add knb plugin build tool (#1226)

* feat: Add knb plugin build tool

* fix: Fix lint errors

* fix: Fix trailing whitespaces in readme

* fix: Fix error msg

* fix: Fix all error msg

* fix: Move knb to sub-dir tools/knb

* fix: Add new dir to sources

* fix: Reflect review feedback

* fix: Reflect review feedback in readme

* fix: Code formatting

* fix: Fix gosec errors
This commit is contained in:
David Simansky 2021-03-08 09:22:21 +01:00 committed by GitHub
parent d49194648b
commit d44f25d350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 562 additions and 1 deletions

2
.gitignore vendored
View File

@ -16,6 +16,8 @@ cover.html
/kn
/kn-*
tools/knb/knb
# emacs tempfiles
\#*
*~

View File

@ -16,7 +16,7 @@
set -o pipefail
source_dirs="cmd pkg test lib"
source_dirs="cmd pkg test lib tools"
# Store for later
if [ -z "$1" ]; then

112
tools/knb/README.md Normal file
View File

@ -0,0 +1,112 @@
# knb - kn builder
CLI tool to enhance plugin build experience for [Knative Client](https://github.com/knative/client).
### Build
```bash
go build
```
### Install
```bash
go get knative.dev/client/tools/knb
```
### Usage
##### Create custom `kn` distribution
The `knb` can be used to generate enhanced customized `kn` source files with inlined plugins.
Create configuration file `.kn.yaml` in a root directory of `knative/client` that should specify at least `name, module, version` coordinates of the plugin.
Executing `knb plugin distro` command will generate the required go files and add dependency to `go.mod`.
Example of `.kn.yaml`
```yaml
plugins:
- name: kn-plugin-source-kafka
module: knative.dev/kn-plugin-source-kafka
pluginImportPath: knative.dev/kn-plugin-source-kafka/plugin
version: v0.19.0
replace:
- module: golang.org/x/sys
version: v0.0.0-20200302150141-5c8b2ff67527
```
Required:
* name
* module - go module name to be used for import and in go.mod file
* version - accepted values are git tag or branch name of go module.
Optional:
* pluginImportPath - import path override, default `$module/plugin`
* replace - go module replacement defined by `module,version`.
Execute command
```bash
knb plugin distro
```
Build `kn`
```bash
./hack/build.sh
```
##### Enable plugin inline feature
The `knb` can be used to generate required go files to inline any `kn` plugin.
```bash
knb plugin init --name kn-source-kafka --cmd source,kafka --description "Some plugin"
```
##### List of commands
Plugin level commands
```
Manage kn plugins.
Usage:
knb plugin [command]
Available Commands:
distro Generate required files to build `kn` with inline plugins.
init Generate required resource to inline plugin.
Flags:
-h, --help help for plugin
Use "knb plugin [command] --help" for more information about a command.
```
```
Generate required files to build `kn` with inline plugins.
Usage:
knb plugin distro [flags]
Flags:
-c, --config kn.yaml Path to kn.yaml config file (default ".kn.yaml")
-h, --help help for distro
```
```
Generate required resource to inline plugin.
Usage:
knb plugin init [flags]
Flags:
--cmd kn service log Defines command parts to execute plugin from kn. E.g kn service log can be achieved with `--cmd service,log`.
--description string Description of a plugin.
-h, --help help for init
--import string Import path of plugin.
--name string Name of a plugin.
--output-dir string Output directory to write plugin.go file. (default "plugin")
```

31
tools/knb/knb.go Normal file
View File

@ -0,0 +1,31 @@
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"knative.dev/client/tools/knb/pkg/root"
)
func main() {
if err := root.NewKnBuilderCmd().Execute(); err != nil {
fmt.Printf("ERROR: %v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,166 @@
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
)
// DistroConfig represents yaml configuration struct
type DistroConfig struct {
Plugins []Plugin `yaml:"plugins"`
}
// NewDistroGenerateCmd represents plugin distro command
func NewDistroGenerateCmd() *cobra.Command {
var config string
var generateCmd = &cobra.Command{
Use: "distro",
Short: "Generate required files to build `kn` with inline plugins.",
RunE: func(cmd *cobra.Command, args []string) error {
if !fileExists(config) {
return fmt.Errorf("kn distro configuration file '%s' doesn't exist", config)
}
if !fileExists("cmd/kn/main.go") {
return fmt.Errorf("cmd/kn/main.go doesn't exist, make sure the command is executed in knative/client root directory")
}
fmt.Println("Generating customized kn distro:")
registerFile := filepath.Join("pkg", "kn", "root", "plugin_register.go")
if fileExists(registerFile) {
fmt.Println("⚠️ plugin_register.go file already exists, trying to append imports")
}
rawConf, err := ioutil.ReadFile(config)
if err != nil {
return err
}
conf := &DistroConfig{}
if err := yaml.Unmarshal(rawConf, conf); err != nil {
return err
}
fmt.Println("✔ config file '" + config + "' processed")
for _, p := range conf.Plugins {
if err := processPlugin(p, registerFile); err != nil {
return err
}
}
//nolint:gosec // Expected go cmd.
if err := exec.Command("gofmt", "-s", "-w", registerFile).Run(); err != nil {
return fmt.Errorf("gofmt failed: %w", err)
}
return nil
},
}
generateCmd.Flags().StringVarP(&config, "config", "c", ".kn.yaml", "Path to `kn.yaml` config file")
return generateCmd
}
// fileExists util func to check if file exists
func fileExists(file string) bool {
_, err := os.Stat(file)
return err == nil
}
// processPlugin does required import changes in plugin_register.go and go.mod file
func processPlugin(p Plugin, registerFile string) error {
importPath := p.PluginImportPath
if importPath == "" {
importPath = p.Module + "/plugin"
}
if err := appendImport(registerFile, importPath); err != nil {
return err
}
if err := processModuleRequire(p); err != nil {
return err
}
if err := processModuleReplace(p); err != nil {
return err
}
return nil
}
// processModuleRequire adds provided plugin module to go.mod require section
func processModuleRequire(plugin Plugin) error {
//nolint:gosec // Expected go cmd.
_, err := exec.Command("go", "mod", "edit", "-require", plugin.Module+"@"+plugin.Version).Output()
if err != nil {
return fmt.Errorf("go mod edit -require failed: %w", err)
}
fmt.Println("✔ go.mod require updated")
return nil
}
// processModuleReplace adds provided module overrides to go.mod replace section
func processModuleReplace(plugin Plugin) error {
if len(plugin.Replace) > 0 {
for _, r := range plugin.Replace {
//nolint:gosec // Expected go cmd.
_, err := exec.Command("go", "mod", "edit", "-replace", r.Module+"="+r.Module+"@"+r.Version).Output()
if err != nil {
return fmt.Errorf("go mod edit -replace failed: %w", err)
}
fmt.Println("✔ go.mod replace updated")
}
}
return nil
}
// appendImport adds specified importPath to plugin registration file.
// New file is initialized if it doesn't exist.
// Warning message is displayed if the plugin import is already present.
func appendImport(file, importPath string) error {
if _, err := os.Stat(file); os.IsNotExist(err) {
f, err := os.Create(file)
if err != nil {
return err
}
t, err := template.New("register").Parse(registerTemplate)
if err != nil {
return err
}
fmt.Println("✔ " + importPath + " added to plugin_register.go")
return t.Execute(f, importPath)
}
content, err := ioutil.ReadFile(file)
if err != nil {
return err
}
if strings.Contains(string(content), importPath) {
fmt.Println("⚠️ " + importPath + " is already present, no changes made")
return nil
}
hook := "// Add #plugins# import here. Don't remove this line, it triggers an automatic replacement."
content = bytes.Replace(content, []byte(hook), []byte(fmt.Sprintf("%s\n _ \"%s\"", hook, importPath)), 1)
fmt.Println("✔ " + importPath + " added to plugin_register.go")
//nolint:gosec // Generate file keeps the same permissions as rest of sources.
return ioutil.WriteFile(file, content, 0644)
}

View File

@ -0,0 +1,71 @@
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
import (
"fmt"
"os"
"path/filepath"
"text/template"
"github.com/spf13/cobra"
)
// NewPluginInitCmd represents plugin init command
func NewPluginInitCmd() *cobra.Command {
plugin := &Plugin{}
var outputDir string
var registerCmd = &cobra.Command{
Use: "init",
Short: "Generate required resource to inline plugin.",
RunE: func(cmd *cobra.Command, args []string) error {
err := os.MkdirAll(outputDir, os.ModePerm)
if err != nil {
return err
}
outputFile := filepath.Join(outputDir, "plugin.go")
if _, err := os.Stat(outputFile); err == nil {
return fmt.Errorf("file '%s' already exists", outputFile)
}
fmt.Println("Generating plugin inline file:")
f, err := os.Create(outputFile)
if err != nil {
return err
}
t, err := template.New("init").Parse(pluginTemplate)
if err != nil {
return err
}
err = t.Execute(f, plugin)
if err != nil {
return err
}
fmt.Println("✔ plugin inline file generated " + outputFile)
err = f.Close()
return err
},
}
registerCmd.Flags().StringVar(&plugin.Name, "name", "", "Name of a plugin.")
registerCmd.Flags().StringVar(&plugin.Description, "description", "", "Description of a plugin.")
registerCmd.Flags().StringVar(&plugin.PluginImportPath, "import", "", "Import path of plugin.")
registerCmd.Flags().StringVar(&outputDir, "output-dir", "plugin", "Output directory to write plugin.go file.")
registerCmd.Flags().StringSliceVar(&plugin.CmdParts, "cmd", []string{}, "Defines command parts to execute plugin from kn. "+
"E.g. `kn service log` can be achieved with `--cmd service,log`.")
return registerCmd
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
import "github.com/spf13/cobra"
// Plugin represents plugin configuration data struct
type Plugin struct {
Name string `yaml:"name"`
Description string `yaml:"description,omitempty"`
Module string `yaml:"module"`
Version string `yaml:"version"`
PluginImportPath string `yaml:"pluginImportPath,omitempty"`
CmdParts []string `yaml:"cmdParts,omitempty"`
Replace []Replace `yaml:"replace,omitempty"`
}
// Plugin represents go module replacement declaration
type Replace struct {
Module string `yaml:"module"`
Version string `yaml:"version"`
}
// NewPluginCmd represents plugin command group
func NewPluginCmd() *cobra.Command {
var pluginCmd = &cobra.Command{
Use: "plugin",
Short: "Manage kn plugins.",
}
pluginCmd.AddCommand(NewPluginInitCmd())
pluginCmd.AddCommand(NewDistroGenerateCmd())
return pluginCmd
}

View File

@ -0,0 +1,97 @@
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
const registerTemplate = `
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package root
import (
// Add #plugins# import here. Don't remove this line, it triggers an automatic replacement.
_ "{{.}}"
)
// RegisterInlinePlugins is an empty function which however forces the
// compiler to run all init() methods of the registered imports
func RegisterInlinePlugins() {}`
const pluginTemplate = `
package plugin
import (
"errors"
"os"
{{if .PluginImportPath}}"{{.PluginImportPath }}"{{else}}//TODO: add plugin import{{end}}
"knative.dev/client/pkg/kn/plugin"
)
func init() {
plugin.InternalPlugins = append(plugin.InternalPlugins, &inlinedPlugin{})
}
type inlinedPlugin struct{}
// Name is a plugin's name
func (p *inlinedPlugin) Name() string {
return "{{.Name}}"
}
// Execute represents the plugin's entrypoint when called through kn
func (p *inlinedPlugin) Execute(args []string) error {
//TODO: implement plugin command execution
//cmd := root.NewPluginCommand()
//oldArgs := os.Args
//defer (func() {
// os.Args = oldArgs
//})()
//os.Args = append([]string{"{{.Name}}"}, args...)
//return cmd.Execute()
return errors.New("plugin execution is not implemented yet")
}
// Description is displayed in kn's plugin section
func (p *inlinedPlugin) Description() (string, error) {
{{if .Description}}return "{{.Description}}", nil{{else}}//TODO: add description
return "", nil{{end}}
}
// CommandParts defines for plugin is executed from kn
func (p *inlinedPlugin) CommandParts() []string {
return []string{ {{- range $i,$v := .CmdParts}}{{if $i}}, {{end}}"{{.}}"{{end -}} }
}
// Path is empty because its an internal plugins
func (p *inlinedPlugin) Path() string {
return ""
}`

View File

@ -0,0 +1,35 @@
/*
Copyright 2021 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package root
import (
"knative.dev/client/tools/knb/pkg/plugin"
"github.com/spf13/cobra"
)
// NewKnBuilderCmd represent root command level
func NewKnBuilderCmd() *cobra.Command {
var rootCmd = &cobra.Command{
Use: "knb",
Short: "Manage and build kn inline plugins.",
SilenceErrors: true,
SilenceUsage: true,
}
rootCmd.AddCommand(plugin.NewPluginCmd())
return rootCmd
}