diff --git a/docs/book/src/plugins/extending/external-plugins.md b/docs/book/src/plugins/extending/external-plugins.md index a957f88fcc8..95262c54399 100644 --- a/docs/book/src/plugins/extending/external-plugins.md +++ b/docs/book/src/plugins/extending/external-plugins.md @@ -30,13 +30,13 @@ structures. `PluginRequest` contains the data collected from the CLI and any previously executed plugins. Kubebuilder sends this data as a JSON object to the external plugin via `stdin`. -**Example `PluginRequest` (triggered by `kubebuilder init --plugins sampleexternalplugin/v1 --domain my.domain`):** +**Example `PluginRequest` (triggered by `kubebuilder edit --plugins sampleexternalplugin/v1`):** ```json { "apiVersion": "v1alpha1", - "args": ["--domain", "my.domain"], - "command": "init", + "args": [], + "command": "edit", "universe": {} } ``` @@ -49,13 +49,14 @@ structures. ```json { "apiVersion": "v1alpha1", - "command": "init", + "command": "edit", "metadata": { - "description": "The `init` subcommand initializes a project via Kubebuilder. It scaffolds a single file: `initFile`.", - "examples": "kubebuilder init --plugins sampleexternalplugin/v1 --domain my.domain" + "description": "The `edit` subcommand adds Prometheus ServiceMonitor configuration for monitoring your operator.", + "examples": "kubebuilder edit --plugins sampleexternalplugin/v1" }, "universe": { - "initFile": "A file created with the `init` subcommand." + "config/prometheus/monitor.yaml": "# Prometheus ServiceMonitor manifest...", + "config/prometheus/kustomization.yaml": "resources:\n - monitor.yaml\n" }, "error": false, "errorMsgs": [] @@ -120,26 +121,15 @@ Otherwise, Kubebuilder would search for the plugins in a default path based on y You can now use it by calling the CLI commands: ```sh -# Initialize a new project with the external plugin named `sampleplugin` -kubebuilder init --plugins sampleplugin/v1 +# Update the project configuration with the sample external plugin +# The sampleexternalplugin adds Prometheus ServiceMonitor configuration +kubebuilder edit --plugins sampleexternalplugin/v1 -# Display help information of the `init` subcommand of the external plugin -kubebuilder init --plugins sampleplugin/v1 --help +# Display help information for the edit subcommand +kubebuilder edit --plugins sampleexternalplugin/v1 --help -# Create a new API with the above external plugin with a customized flag `number` -kubebuilder create api --plugins sampleplugin/v1 --number 2 - -# Create a webhook with the above external plugin with a customized flag `hooked` -kubebuilder create webhook --plugins sampleplugin/v1 --hooked - -# Update the project configuration with the above external plugin -kubebuilder edit --plugins sampleplugin/v1 - -# Create new APIs with external plugins v1 and v2 by respecting the plugin chaining order -kubebuilder create api --plugins sampleplugin/v1,sampleplugin/v2 - -# Create new APIs with the go/v4 plugin and then pass those files to the external plugin by respecting the plugin chaining order -kubebuilder create api --plugins go/v4,sampleplugin/v1 +# Plugin chaining example: Use go/v4 plugin first, then apply external plugin +kubebuilder edit --plugins go/v4,sampleexternalplugin/v1 ``` ## Further resources diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/cmd.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/cmd.go index f64d0e43a66..c70eb45b3a1 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/cmd.go +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/cmd.go @@ -59,19 +59,10 @@ func Run() { // Run logic depending on the command that is requested by Kubebuilder switch pluginRequest.Command { - // the `init` subcommand is often used when initializing a new project - case "init": - response = scaffolds.InitCmd(pluginRequest) - // the `create api` subcommand is often used after initializing a project - // with the `init` subcommand to create a controller and CRDs for a - // provided group, version, and kind - case "create api": - response = scaffolds.ApiCmd(pluginRequest) - // the `create webhook` subcommand is often used after initializing a project - // with the `init` subcommand to create a webhook for a provided - // group, version, and kind - case "create webhook": - response = scaffolds.WebhookCmd(pluginRequest) + // the `edit` subcommand is used to add optional features to an existing project + // This is a realistic use case for external plugins - adding optional monitoring + case "edit": + response = scaffolds.EditCmd(pluginRequest) // the `flags` subcommand is used to customize the flags that // the Kubebuilder cli will bind for use with this plugin case "flags": diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/flags.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/flags.go index d259926bbcc..5773e12f068 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/flags.go +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/flags.go @@ -37,12 +37,8 @@ func flagsCmd(pr *external.PluginRequest) external.PluginResponse { } switch pr.Command { - case "init": - pluginResponse.Flags = scaffolds.InitFlags - case "create api": - pluginResponse.Flags = scaffolds.ApiFlags - case "create webhook": - pluginResponse.Flags = scaffolds.WebhookFlags + case "edit": + pluginResponse.Flags = scaffolds.EditFlags default: pluginResponse.Error = true pluginResponse.ErrorMsgs = []string{ diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/metadata.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/metadata.go index cd31799a51d..675d1bd1944 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/metadata.go +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/metadata.go @@ -39,9 +39,7 @@ func metadataCmd(pr *external.PluginRequest) external.PluginResponse { // Here is an example of parsing multiple flags from a Kubebuilder external plugin request flagsToParse := pflag.NewFlagSet("flagsFlags", pflag.ContinueOnError) - flagsToParse.Bool("init", false, "sets the init flag to true") - flagsToParse.Bool("api", false, "sets the api flag to true") - flagsToParse.Bool("webhook", false, "sets the webhook flag to true") + flagsToParse.Bool("edit", false, "sets the edit flag to true") if err := flagsToParse.Parse(pr.Args); err != nil { pluginResponse.Error = true @@ -51,22 +49,14 @@ func metadataCmd(pr *external.PluginRequest) external.PluginResponse { return pluginResponse } - initFlag, _ := flagsToParse.GetBool("init") - apiFlag, _ := flagsToParse.GetBool("api") - webhookFlag, _ := flagsToParse.GetBool("webhook") + editFlag, _ := flagsToParse.GetBool("edit") // The Phase 2 Plugins implementation will only ever pass a single boolean flag - // argument in the JSON request `args` field. The flag will be `--init` if it is - // attempting to get the flags for the `init` subcommand, `--api` for `create api`, - // `--webhook` for `create webhook`, and `--edit` for `edit` - if initFlag { + // argument in the JSON request `args` field. The flag will be `--edit` for `edit` + if editFlag { // Populate the JSON response `metadata` field with a description - // and examples for the `init` subcommand - pluginResponse.Metadata = scaffolds.InitMeta - } else if apiFlag { - pluginResponse.Metadata = scaffolds.ApiMeta - } else if webhookFlag { - pluginResponse.Metadata = scaffolds.WebhookMeta + // and examples for the `edit` subcommand + pluginResponse.Metadata = scaffolds.EditMeta } else { pluginResponse.Error = true pluginResponse.ErrorMsgs = []string{ diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod index f95b27dc6bd..4807c077f5a 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod @@ -3,15 +3,20 @@ module v1 go 1.24.5 require ( + github.com/spf13/afero v1.15.0 github.com/spf13/pflag v1.0.10 sigs.k8s.io/kubebuilder/v4 v4.9.0 ) +replace sigs.k8s.io/kubebuilder/v4 => ../../../../../../../ + require ( github.com/gobuffalo/flect v1.0.3 // indirect - github.com/spf13/afero v1.15.0 // indirect + github.com/kr/text v0.2.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/mod v0.28.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.37.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum index 337edc95c60..bd0912e777c 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum @@ -1,5 +1,6 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,12 +14,18 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= @@ -49,10 +56,10 @@ golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/kubebuilder/v4 v4.9.0 h1:9e9LnQy/wQ24IZDIqye6iZZFOB9aKNyNfjnfsy3S8cw= -sigs.k8s.io/kubebuilder/v4 v4.9.0/go.mod h1:Xql7wLeyXBQ4lJJdi1Pl8T/DeV4UXpA1kaOEumN0pzY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/internal/test/plugins/prometheus/kustomization.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/internal/test/plugins/prometheus/kustomization.go new file mode 100644 index 00000000000..efa320ef666 --- /dev/null +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/internal/test/plugins/prometheus/kustomization.go @@ -0,0 +1,59 @@ +/* +Copyright 2022 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 prometheus + +// PrometheusKustomization represents the kustomization.yaml for Prometheus resources +type PrometheusKustomization struct { + Path string + Content string +} + +// NewPrometheusKustomization creates a new kustomization.yaml for Prometheus resources +func NewPrometheusKustomization() *PrometheusKustomization { + return &PrometheusKustomization{ + Path: "config/prometheus/kustomization.yaml", + Content: prometheusKustomizationTemplate, + } +} + +const prometheusKustomizationTemplate = `resources: + - monitor.yaml +` + +// DefaultKustomizationPatch represents a patch to config/default/kustomization.yaml +type DefaultKustomizationPatch struct { + Path string + Content string +} + +// NewDefaultKustomizationPatch creates a patch comment for the default kustomization.yaml +func NewDefaultKustomizationPatch() *DefaultKustomizationPatch { + return &DefaultKustomizationPatch{ + Path: "config/default/kustomization_prometheus_patch.yaml", + Content: defaultKustomizationPatchTemplate, + } +} + +const defaultKustomizationPatchTemplate = `# [PROMETHEUS] To enable prometheus monitor, uncomment the following line in config/default/kustomization.yaml: +# +# In the resources section, add: +# - ../prometheus +# +# This will include the Prometheus ServiceMonitor in your deployment. +# Make sure you have the Prometheus Operator installed in your cluster. +# +# For more information, see: https://github.com/prometheus-operator/prometheus-operator +` diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/internal/test/plugins/prometheus/servicemonitor.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/internal/test/plugins/prometheus/servicemonitor.go new file mode 100644 index 00000000000..903bfccd9c9 --- /dev/null +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/internal/test/plugins/prometheus/servicemonitor.go @@ -0,0 +1,103 @@ +/* +Copyright 2022 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 prometheus + +import "fmt" + +// ServiceMonitor represents a Prometheus ServiceMonitor manifest +type ServiceMonitor struct { + Path string + Content string +} + +// ServiceMonitorOptions allows configuration of the ServiceMonitor +type ServiceMonitorOptions func(*ServiceMonitor) + +// WithDomain sets the domain for the ServiceMonitor +func WithDomain(domain string) ServiceMonitorOptions { + return func(sm *ServiceMonitor) { + sm.Content = fmt.Sprintf(serviceMonitorTemplate, domain, domain) + } +} + +// WithProjectName sets the project name for the ServiceMonitor +func WithProjectName(projectName string) ServiceMonitorOptions { + return func(sm *ServiceMonitor) { + // Project name can be used for labels or naming + // For now, we'll use it in a future iteration if needed + } +} + +// NewServiceMonitor creates a new ServiceMonitor manifest +func NewServiceMonitor(opts ...ServiceMonitorOptions) *ServiceMonitor { + sm := &ServiceMonitor{ + Path: "config/prometheus/monitor.yaml", + } + + for _, opt := range opts { + opt(sm) + } + + // Set default content if not set by options + if sm.Content == "" { + sm.Content = fmt.Sprintf(serviceMonitorTemplate, "example.com", "example.com") + } + + return sm +} + +const serviceMonitorTemplate = `# Prometheus Monitor Service (Metrics) +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: %s + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager + +--- +# Prometheus ServiceMonitor +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: %s + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager +` diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/api.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/api.go deleted file mode 100644 index 5d13d62631e..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/api.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2022 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" - - "v1/scaffolds/internal/templates/api" - - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v4/pkg/plugin" - "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" -) - -var ApiFlags = []external.Flag{ - { - Name: "number", - Default: "1", - Type: "int", - Usage: "set a number to be added to the scaffolded apiFile.txt", - }, - { - Name: "group", - Default: "", - Type: "string", - Usage: "API group name (e.g., 'example')", - }, - { - Name: "version", - Default: "", - Type: "string", - Usage: "API version (e.g., 'v1alpha1')", - }, - { - Name: "kind", - Default: "", - Type: "string", - Usage: "API kind (e.g., 'ExampleKind')", - }, -} - -var ApiMeta = plugin.SubcommandMetadata{ - Description: "The `create api` subcommand of the sampleexternalplugin is meant to create an api for a project via Kubebuilder. It scaffolds a single file: `apiFile.txt`", - Examples: ` - Scaffold with the defaults: - $ kubebuilder create api --plugins sampleexternalplugin/v1 --group samplegroup --version v1 --kind SampleKind - - Scaffold with a specific number in the apiFile.txt file: - $ kubebuilder create api --plugins sampleexternalplugin/v1 --number 2 --group samplegroup --version v1 --kind SampleKind - `, -} - -// ApiCmd handles all the logic for the `create api` subcommand of this sample external plugin -func ApiCmd(pr *external.PluginRequest) external.PluginResponse { - pluginResponse := external.PluginResponse{ - APIVersion: "v1alpha1", - Command: "create api", - Universe: pr.Universe, - } - - flags := pflag.NewFlagSet("apiFlags", pflag.ContinueOnError) - flags.Int("number", 1, "set a number to be added in the scaffolded apiFile.txt") - flags.String("group", "", "API group name") - flags.String("version", "", "API version") - flags.String("kind", "", "API kind") - - if err := flags.Parse(pr.Args); err != nil { - pluginResponse.Error = true - pluginResponse.ErrorMsgs = []string{ - fmt.Sprintf("failed to parse flags: %s", err.Error()), - } - return pluginResponse - } - - number, _ := flags.GetInt("number") - group, _ := flags.GetString("group") - version, _ := flags.GetString("version") - kind, _ := flags.GetString("kind") - - // Validate GVK inputs - if group == "" || version == "" || kind == "" { - pluginResponse.Error = true - pluginResponse.ErrorMsgs = []string{ - "--group, --version, and --kind are required flags", - } - return pluginResponse - } - - // Scaffold API file using all values - apiFile := api.NewApiFile( - api.WithNumber(number), - api.WithGroup(group), - api.WithVersion(version), - api.WithKind(kind), - ) - - // Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin. - // This universe is a key:value mapping of filename:contents. Here we are adding the file - // "apiFile.txt" to the universe with some content. When this is returned Kubebuilder will - // take all values within the "universe" and write them to the user's filesystem. - pluginResponse.Universe[apiFile.Name] = apiFile.Contents - return pluginResponse -} diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/edit.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/edit.go new file mode 100644 index 00000000000..fad5e80b8ae --- /dev/null +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/edit.go @@ -0,0 +1,125 @@ +/* +Copyright 2022 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 ( + "errors" + "fmt" + "os" + + "github.com/spf13/afero" + + "v1/internal/test/plugins/prometheus" + + "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" + "sigs.k8s.io/kubebuilder/v4/pkg/machinery" + "sigs.k8s.io/kubebuilder/v4/pkg/plugin" + "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" +) + +var EditFlags = []external.Flag{} + +var EditMeta = plugin.SubcommandMetadata{ + Description: "The `edit` subcommand of the sampleexternalplugin adds Prometheus ServiceMonitor configuration for monitoring your operator", + Examples: ` + Add Prometheus monitoring to your project: + $ kubebuilder edit --plugins sampleexternalplugin/v1 + `, +} + +// EditCmd handles all the logic for the `edit` subcommand of this sample external plugin +func EditCmd(pr *external.PluginRequest) external.PluginResponse { + pluginResponse := external.PluginResponse{ + APIVersion: "v1alpha1", + Command: "edit", + Universe: pr.Universe, + } + + // Load PROJECT config to get domain and other metadata + projectConfig, err := loadProjectConfig() + if err != nil { + pluginResponse.Error = true + pluginResponse.ErrorMsgs = []string{ + fmt.Sprintf("failed to load PROJECT config: %s", err.Error()), + } + return pluginResponse + } + + // Create ServiceMonitor manifest + serviceMonitor := prometheus.NewServiceMonitor( + prometheus.WithDomain(projectConfig.Domain), + prometheus.WithProjectName(projectConfig.ProjectName), + ) + + // Create Kustomization for Prometheus resources + kustomization := prometheus.NewPrometheusKustomization() + + // Create Kustomization patch for default + kustomizationPatch := prometheus.NewDefaultKustomizationPatch() + + // Add files to universe + pluginResponse.Universe[serviceMonitor.Path] = serviceMonitor.Content + pluginResponse.Universe[kustomization.Path] = kustomization.Content + pluginResponse.Universe[kustomizationPatch.Path] = kustomizationPatch.Content + + return pluginResponse +} + +// ProjectConfig represents the minimal PROJECT file structure we need +type ProjectConfig struct { + Domain string + ProjectName string + Repo string +} + +// loadProjectConfig reads the PROJECT file using the kubebuilder config API. +func loadProjectConfig() (*ProjectConfig, error) { + store := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()}) + if err := store.Load(); err != nil { + if errors.Is(err, os.ErrNotExist) { + return &ProjectConfig{ + Domain: "example.com", + ProjectName: "project", + Repo: "example.com/project", + }, nil + } + return nil, fmt.Errorf("failed to load PROJECT file: %w", err) + } + + cfg := store.Config() + if cfg == nil { + return nil, fmt.Errorf("PROJECT file is empty or invalid") + } + + domain := cfg.GetDomain() + if domain == "" { + domain = "example.com" + } + projectName := cfg.GetProjectName() + if projectName == "" { + projectName = "project" + } + repo := cfg.GetRepository() + if repo == "" { + repo = fmt.Sprintf("%s/%s", domain, projectName) + } + + return &ProjectConfig{ + Domain: domain, + ProjectName: projectName, + Repo: repo, + }, nil +} diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/init.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/init.go deleted file mode 100644 index 67525fdd102..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/init.go +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2022 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" - - "v1/scaffolds/internal/templates" - - "github.com/spf13/pflag" - - "sigs.k8s.io/kubebuilder/v4/pkg/plugin" - "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" -) - -var InitFlags = []external.Flag{ - { - Name: "domain", - Type: "string", - Default: "example.domain.com", - Usage: "sets the domain added in the scaffolded initFile.txt", - }, -} - -var InitMeta = plugin.SubcommandMetadata{ - Description: "The `init` subcommand of the sampleexternalplugin is meant to initialize a project via Kubebuilder. It scaffolds a single file: `initFile.txt`", - Examples: ` - Scaffold with the defaults: - $ kubebuilder init --plugins sampleexternalplugin/v1 - - Scaffold with a specific domain: - $ kubebuilder init --plugins sampleexternalplugin/v1 --domain sample.domain.com - `, -} - -// InitCmd handles all the logic for the `init` subcommand of this sample external plugin -func InitCmd(pr *external.PluginRequest) external.PluginResponse { - pluginResponse := external.PluginResponse{ - APIVersion: "v1alpha1", - Command: "init", - Universe: pr.Universe, - } - - // Here is an example of parsing a flag from a Kubebuilder external plugin request - flags := pflag.NewFlagSet("initFlags", pflag.ContinueOnError) - flags.String("domain", "example.domain.com", "sets the domain added in the scaffolded initFile.txt") - if err := flags.Parse(pr.Args); err != nil { - pluginResponse.Error = true - pluginResponse.ErrorMsgs = []string{ - fmt.Sprintf("failed to parse flags: %s", err.Error()), - } - return pluginResponse - } - domain, _ := flags.GetString("domain") - - initFile := templates.NewInitFile(templates.WithDomain(domain)) - - // Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin. - // This universe is a key:value mapping of filename:contents. Here we are adding the file - // "initFile.txt" to the universe with some content. When this is returned Kubebuilder will - // take all values within the "universe" and write them to the user's filesystem. - pluginResponse.Universe[initFile.Name] = initFile.Contents - - return pluginResponse -} diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/api/apiFile.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/api/apiFile.go deleted file mode 100644 index 58d895c2403..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/api/apiFile.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2022 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 api - -import "fmt" - -// ApiFile represents the apiFile.txt -type ApiFile struct { - Name string - Contents string - number int - group string - version string - kind string -} - -// ApiFileOptions is a way to set configurable options for the API file -type ApiFileOptions func(af *ApiFile) - -// WithNumber sets the number to be used in the resulting ApiFile -func WithNumber(number int) ApiFileOptions { - return func(af *ApiFile) { - af.number = number - } -} - -// WithGroup sets the group value -func WithGroup(group string) ApiFileOptions { - return func(af *ApiFile) { - af.group = group - } -} - -// WithVersion sets the version value -func WithVersion(version string) ApiFileOptions { - return func(af *ApiFile) { - af.version = version - } -} - -// WithKind sets the kind value -func WithKind(kind string) ApiFileOptions { - return func(af *ApiFile) { - af.kind = kind - } -} - -// NewApiFile returns a new ApiFile with -func NewApiFile(opts ...ApiFileOptions) *ApiFile { - apiFile := &ApiFile{ - Name: "apiFile.txt", - } - - for _, opt := range opts { - opt(apiFile) - } - - apiFile.Contents = fmt.Sprintf(apiFileTemplate, - apiFile.number, apiFile.group, apiFile.version, apiFile.kind) - - return apiFile -} - -const apiFileTemplate = `A simple text file created with the create api subcommand -NUMBER: %d -GROUP: %s -VERSION: %s -KIND: %s` diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/initFile.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/initFile.go deleted file mode 100644 index e2bbdd43b1f..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/initFile.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2022 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 templates - -import "fmt" - -// InitFile represents the InitFile.txt -type InitFile struct { - Name string - Contents string - domain string -} - -// InitFileOptions is a way to set configurable options for the Init file -type InitFileOptions func(inf *InitFile) - -// WithDomain sets the number to be used in the resulting InitFile -func WithDomain(domain string) InitFileOptions { - return func(inf *InitFile) { - inf.domain = domain - } -} - -// NewInitFile returns a new InitFile with -func NewInitFile(opts ...InitFileOptions) *InitFile { - initFile := &InitFile{ - Name: "initFile.txt", - } - - for _, opt := range opts { - opt(initFile) - } - - initFile.Contents = fmt.Sprintf(initFileTemplate, initFile.domain) - - return initFile -} - -const initFileTemplate = "A simple text file created with the `init` subcommand\nDOMAIN: %s" diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/webhook/webhookFile.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/webhook/webhookFile.go deleted file mode 100644 index ea1a4045e60..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/internal/templates/webhook/webhookFile.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2022 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 webhook - -// WebhookFile represents the WebhookFile.txt -type WebhookFile struct { - Name string - Contents string - hooked bool -} - -// WebhookFileOptions is a way to set configurable options for the Webhook file -type WebhookFileOptions func(wf *WebhookFile) - -// WithHooked sets whether or not to add `HOOKED` to a new line in the resulting WebhookFile -func WithHooked(hooked bool) WebhookFileOptions { - return func(wf *WebhookFile) { - wf.hooked = hooked - } -} - -// NewWebhookFile returns a new WebhookFile with -func NewWebhookFile(opts ...WebhookFileOptions) *WebhookFile { - webhookFile := &WebhookFile{ - Name: "webhookFile.txt", - } - - for _, opt := range opts { - opt(webhookFile) - } - - webhookFile.Contents = WebhookFileDefaultMessage - - if webhookFile.hooked { - webhookFile.Contents += WebhookFileHookedMessage - } - - return webhookFile -} - -const WebhookFileDefaultMessage = "A simple text file created with the `create webhook` subcommand" -const WebhookFileHookedMessage = "\nHOOKED!" diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/webhook.go b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/webhook.go deleted file mode 100644 index ec13dc56205..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/webhook.go +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2022 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" - - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v4/pkg/plugin" - "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" -) - -var WebhookFlags = []external.Flag{ - { - Name: "hooked", - Type: "bool", - Default: "false", - Usage: "add the word `hooked` to the end of the scaffolded webhookFile.txt", - }, -} - -var WebhookMeta = plugin.SubcommandMetadata{ - Description: "The `create webhook` subcommand of the sampleexternalplugin is meant to create a webhook for a project via Kubebuilder. It scaffolds a single file: `webhookFile.txt`", - Examples: ` - Scaffold with the defaults: - $ kubebuilder create webhook --plugins sampleexternalplugin/v1 - - Scaffold with the text "HOOKED!" in the webhookFile.txt file: - $ kubebuilder create webhook --plugins sampleexternalplugin/v1 --hooked - `, -} - -// WebhookCmd handles all the logic for the `create webhook` subcommand of this sample external plugin -func WebhookCmd(pr *external.PluginRequest) external.PluginResponse { - pluginResponse := external.PluginResponse{ - APIVersion: "v1alpha1", - Command: "create webhook", - Universe: pr.Universe, - } - - // Here is an example of parsing a flag from a Kubebuilder external plugin request - flags := pflag.NewFlagSet("apiFlags", pflag.ContinueOnError) - flags.Bool("hooked", false, "add the word `hooked` to the end of the scaffolded webhookFile.txt") - if err := flags.Parse(pr.Args); err != nil { - pluginResponse.Error = true - pluginResponse.ErrorMsgs = []string{ - fmt.Sprintf("failed to parse flags: %s", err.Error()), - } - return pluginResponse - } - hooked, _ := flags.GetBool("hooked") - - msg := "A simple text file created with the `create webhook` subcommand" - if hooked { - msg += "\nHOOKED!" - } - - // Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin. - // This universe is a key:value mapping of filename:contents. Here we are adding the file - // "webhookFile.txt" to the universe with some content. When this is returned Kubebuilder will - // take all values within the "universe" and write them to the user's filesystem. - pluginResponse.Universe["webhookFile.txt"] = msg - - return pluginResponse -} diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/test/test.sh b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/test/test.sh index 3e55684d2be..63f803cb651 100755 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/test/test.sh +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/test/test.sh @@ -22,5 +22,10 @@ cd testdata/testplugin rm -rf * # Run Kubebuilder commands inside the testplugin directory -kubebuilder init --plugins sampleexternalplugin/v1 --domain sample.domain.com -kubebuilder create api --plugins sampleexternalplugin/v1 --number 2 --group samplegroup --version v1 --kind SampleKind +kubebuilder init --plugins go/v4 --domain sample.domain.com --repo sample.domain.com/test-operator +kubebuilder edit --plugins sampleexternalplugin/v1 + +# Ensure Prometheus assets were scaffolded +test -f config/prometheus/monitor.yaml +test -f config/prometheus/kustomization.yaml +test -f config/default/kustomization_prometheus_patch.yaml diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/PROJECT b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/PROJECT deleted file mode 100644 index 6f68fdd05c2..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/PROJECT +++ /dev/null @@ -1,8 +0,0 @@ -# Code generated by tool. DO NOT EDIT. -# This file is used to track the info used to scaffold your project -# and allow the plugins properly work. -# More info: https://book.kubebuilder.io/reference/project-config.html -cliVersion: 4.6.0 -layout: -- sampleexternalplugin/v1 -version: "3" diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/README.md b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/README.md new file mode 100644 index 00000000000..c10369377bd --- /dev/null +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/README.md @@ -0,0 +1 @@ +This directory is intentionally left empty. The sample external plugin test creates a temporary Kubebuilder project here at runtime and cleans it afterward. diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/apiFile.txt b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/apiFile.txt deleted file mode 100644 index d8d1881855d..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/apiFile.txt +++ /dev/null @@ -1,5 +0,0 @@ -A simple text file created with the create api subcommand -NUMBER: 2 -GROUP: samplegroup -VERSION: v1 -KIND: SampleKind \ No newline at end of file diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/initFile.txt b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/initFile.txt deleted file mode 100644 index de958e019b3..00000000000 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/testdata/testplugin/initFile.txt +++ /dev/null @@ -1,2 +0,0 @@ -A simple text file created with the `init` subcommand -DOMAIN: sample.domain.com \ No newline at end of file