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

WIP - KB declarative pattern external plugin #300

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions plugin/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module sigs.k8s.io/kubebuilder-declarative-pattern/plugin

go 1.19

require (
github.com/spf13/afero v1.9.5
github.com/spf13/pflag v1.0.5
sigs.k8s.io/kubebuilder/v3 v3.10.0
)

require (
github.com/gobuffalo/flect v1.0.2 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.0 // indirect
)
470 changes: 470 additions & 0 deletions plugin/go.sum

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions plugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2023 The Kubernetes 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 (
"sigs.k8s.io/kubebuilder-declarative-pattern/plugin/v1/cmd"
)

func main() {
cmd.Run()
}
89 changes: 89 additions & 0 deletions plugin/v1/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Copyright 2023 The Kubernetes 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 cmd

import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"os"

"sigs.k8s.io/kubebuilder-declarative-pattern/plugin/v1/scaffolds"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin/external"
)

const (
commandInit = "init"
commandApi = "create api"
commandFlags = "flags"
)

func returnError(err error) {
response := external.PluginResponse{
Error: true,
ErrorMsgs: []string{err.Error()},
}

output, err := json.Marshal(response)
if err != nil {
log.Fatalf("encountered error serializing output: %s | output: %s", err.Error(), output)
}
fmt.Printf("%s", output)
}

// Run will run the steps defined by the plugin
func Run() {
// Kubebuilder makes requests to external plugin by writing STDIN.
reader := bufio.NewReader(os.Stdin)

input, err := io.ReadAll(reader)
if err != nil {
returnError(fmt.Errorf("encountered error reading from STDIN: %+v", err))
}

// Parsing request sent by kubebuilder into a PluginRequest instance.
pluginRequest := &external.PluginRequest{}

if err = json.Unmarshal(input, pluginRequest); err != nil {
returnError(err)
}

// Run logic based on the command executed by Kubebuilder.
var response external.PluginResponse
switch pluginRequest.Command {
case commandInit:
response = scaffolds.InitCmd(pluginRequest)
case commandApi:
response = scaffolds.ApiCmd(pluginRequest)
case commandFlags:
response = FlagsCmd(pluginRequest)
default:
response = external.PluginResponse{
Error: true,
ErrorMsgs: []string{"unknown subcommand:" + pluginRequest.Command},
}
}

output, err := json.Marshal(response)
if err != nil {
returnError(err)
}

fmt.Printf("%s", output)
}
54 changes: 54 additions & 0 deletions plugin/v1/cmd/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Copyright 2023 The Kubernetes 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 cmd

import (
"github.com/spf13/pflag"
"sigs.k8s.io/kubebuilder-declarative-pattern/plugin/v1/scaffolds"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin/external"
)

func FlagsCmd(pr *external.PluginRequest) external.PluginResponse {
pluginResponse := external.PluginResponse{
APIVersion: "v1",
Command: "flags",
Universe: pr.Universe,
Flags: []external.Flag{},
}

flagsToParse := pflag.NewFlagSet("flags", pflag.ContinueOnError)
flagsToParse.Bool("init", false, "sets the init flag to true")
flagsToParse.Bool("api", false, "sets the api flag to true")

flagsToParse.Parse(pr.Args)

initFlag, _ := flagsToParse.GetBool("init")
apiFlag, _ := flagsToParse.GetBool("api")

if initFlag {
pluginResponse.Flags = scaffolds.InitFlags
} else if apiFlag {
pluginResponse.Flags = scaffolds.ApiFlags
} else {
pluginResponse.Error = true
pluginResponse.ErrorMsgs = []string{
"unrecognized flag",
}
}

return pluginResponse
}
91 changes: 91 additions & 0 deletions plugin/v1/scaffolds/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright 2023 The Kubernetes 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 scaffolds

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/afero"
"sigs.k8s.io/kubebuilder-declarative-pattern/plugin/v1/scaffolds/internal/templates"
"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin/external"
)

var ApiFlags = []external.Flag{}

var ApiMeta = plugin.SubcommandMetadata{
Description: "Scaffold a Kubernetes API with the declarative plugin",
Examples: "kubebuilder create api --plugins=go/v4,declarative/v1",
}

const (
exampleManifestVersion = "0.0.1"
)

// ApiCmd handles all the logic for the `create api` subcommand
func ApiCmd(pr *external.PluginRequest) external.PluginResponse {
pluginResponse := external.PluginResponse{
Command: "create api",
Universe: pr.Universe,
}

// TODO(@em-r): add logic for handling `create api` command
// GVK are required to scaffold templates, can be retrieved from PluginRequest.Args

return pluginResponse
}

func apiScaffold() error {
// Load the boilerplate
boilerplate, err := os.ReadFile(filepath.Join("hack", "boilerplate.go.txt"))
if err != nil {
return fmt.Errorf("error updating scaffold: unable to load boilerplate: %w", err)
}

fs := machinery.Filesystem{
FS: afero.NewOsFs(),
}

// Initialize the machinery.Scaffold that will write the files to disk
scaffold := machinery.NewScaffold(fs,
machinery.WithBoilerplate(string(boilerplate)),
)

//nolint:staticcheck
err = scaffold.Execute(
&templates.Types{},
&templates.Controller{},
&templates.Channel{ManifestVersion: exampleManifestVersion},
&templates.Manifest{ManifestVersion: exampleManifestVersion},
)

if err != nil {
return fmt.Errorf("error updating scaffold: %w", err)
}

// Update Dockerfile
// nolint:staticcheck
err = updateDockerfile()
if err != nil {
return err
}

return nil
}
103 changes: 103 additions & 0 deletions plugin/v1/scaffolds/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2023 The Kubernetes 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 scaffolds

import (
"fmt"
"os"
"path/filepath"
"strings"

"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin/external"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
)

var InitFlags = []external.Flag{}

var InitMeta = plugin.SubcommandMetadata{
Description: "Initialize a new project with declarative plugin",
Examples: `
Scaffold with go/v4:
$ kubebuilder init --plugins go/v4,declarative/v1
`,
}

// InitCmd handles all the logic for the `init` subcommand
func InitCmd(pr *external.PluginRequest) external.PluginResponse {
pluginResponse := external.PluginResponse{
APIVersion: "v1",
Command: "init",
Universe: pr.Universe,
}

if err := updateDockerfile(); err != nil {
pluginResponse.Error = true
pluginResponse.ErrorMsgs = append(pluginResponse.ErrorMsgs, err.Error())
}

if content, err := os.ReadFile("Dockerfile"); err != nil {
pluginResponse.Error = true
pluginResponse.ErrorMsgs = append(pluginResponse.ErrorMsgs, err.Error())

} else {
pluginResponse.Universe["Dockerfile"] = string(content)
}

return pluginResponse
}

// updateDockerfile will add channels staging required for declarative plugin
func updateDockerfile() error {
// fmt.Println("updating Dockerfile to add channels/ directory in the image")
dockerfile := filepath.Join("Dockerfile")
controllerPath := "internal/controller/"

// nolint:lll
err := insertCodeIfDoesNotExist(dockerfile,
fmt.Sprintf("COPY %s %s", controllerPath, controllerPath),
"\n# https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern/blob/master/docs/addon/walkthrough/README.md#adding-a-manifest\n# Stage channels and make readable\nCOPY channels/ /channels/\nRUN chmod -R a+rx /channels/")
if err != nil {
return err
}

err = insertCodeIfDoesNotExist(dockerfile,
"COPY --from=builder /workspace/manager .",
"\n# copy channels\nCOPY --from=builder /channels /channels\n")
if err != nil {
return err
}

return nil
}

// insertCodeIfDoesNotExist insert code if it does not already exists
func insertCodeIfDoesNotExist(filename, target, code string) error {
// false positive
// nolint:gosec
contents, err := os.ReadFile(filename)
if err != nil {
return err
}

idx := strings.Index(string(contents), code)
if idx != -1 {
return nil
}

return util.InsertCode(filename, target, code)
}
Loading