diff --git a/changelog/fragments/config-samples-kustomize-scaffold.yaml b/changelog/fragments/config-samples-kustomize-scaffold.yaml new file mode 100644 index 00000000000..2378bdcd84f --- /dev/null +++ b/changelog/fragments/config-samples-kustomize-scaffold.yaml @@ -0,0 +1,17 @@ +entries: + - description: > + Added a scaffold marker `+kubebuilder:scaffold:manifestskustomizesamples` to `config/samples/kustomization.yaml` + that allows updates without overwriting the entire file. + kind: change + breaking: true + migration: + header: Add the samples scaffold marker to your `config/samples/kustomization.yaml` + body: > + Add the `+kubebuilder:scaffold:manifestskustomizesamples` to your `config/samples/kustomization.yaml` + file like so (using an example sample file): + + ```yaml + resources: + - cache_v1alpha1_memcached.yaml + # +kubebuilder:scaffold:manifestskustomizesamples + ``` diff --git a/internal/plugins/ansible/v1/api.go b/internal/plugins/ansible/v1/api.go index 69f71cc0e5c..07397774555 100644 --- a/internal/plugins/ansible/v1/api.go +++ b/internal/plugins/ansible/v1/api.go @@ -117,7 +117,8 @@ func (p *createAPIPlugin) Run() error { // SDK phase 2 plugins. func (p *createAPIPlugin) runPhase2() error { - return manifests.RunCreateAPI(p.config) + gvk := p.createOptions.GVK + return manifests.RunCreateAPI(p.config, config.GVK{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind}) } func (p *createAPIPlugin) Validate() error { diff --git a/internal/plugins/golang/v2/api.go b/internal/plugins/golang/v2/api.go index e1b4a6ee9cd..6a2c5337244 100644 --- a/internal/plugins/golang/v2/api.go +++ b/internal/plugins/golang/v2/api.go @@ -39,6 +39,11 @@ func (p *createAPIPlugin) InjectConfig(c *config.Config) { } func (p *createAPIPlugin) Run() error { + // Run() may add a new resource to the config, so we can compare resources before/after to get the new resource. + oldResources := make(map[config.GVK]struct{}, len(p.config.Resources)) + for _, r := range p.config.Resources { + oldResources[r] = struct{}{} + } if err := p.CreateAPI.Run(); err != nil { return err } @@ -49,11 +54,21 @@ func (p *createAPIPlugin) Run() error { return nil } + // Find the new resource. Here we shouldn't worry about checking if one was found, since downstream + // plugins will do so. + var newResource config.GVK + for _, r := range p.config.Resources { + if _, hasResource := oldResources[r]; !hasResource { + newResource = r + break + } + } + // Run SDK phase 2 plugins. - return p.runPhase2() + return p.runPhase2(newResource) } // SDK phase 2 plugins. -func (p *createAPIPlugin) runPhase2() error { - return manifests.RunCreateAPI(p.config) +func (p *createAPIPlugin) runPhase2(gvk config.GVK) error { + return manifests.RunCreateAPI(p.config, gvk) } diff --git a/internal/plugins/helm/v1/api.go b/internal/plugins/helm/v1/api.go index 29d3dc1cfc7..83873cd656b 100644 --- a/internal/plugins/helm/v1/api.go +++ b/internal/plugins/helm/v1/api.go @@ -137,7 +137,8 @@ func (p *createAPIPlugin) Run() error { // SDK phase 2 plugins. func (p *createAPIPlugin) runPhase2() error { - return manifests.RunCreateAPI(p.config) + gvk := p.createOptions.GVK + return manifests.RunCreateAPI(p.config, config.GVK{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind}) } // Validate perform the required validations for this plugin diff --git a/internal/plugins/manifests/api.go b/internal/plugins/manifests/api.go index d2c5a3c2d83..c6f9eabee1d 100644 --- a/internal/plugins/manifests/api.go +++ b/internal/plugins/manifests/api.go @@ -17,41 +17,53 @@ package manifests import ( "fmt" - "path/filepath" - "strings" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/model/file" - "github.com/operator-framework/operator-sdk/internal/plugins/util/kustomize" + "github.com/operator-framework/operator-sdk/internal/kubebuilder/machinery" ) -// sampleKustomizationFragment is a template for samples/kustomization.yaml. -const sampleKustomizationFragment = `## This file is auto-generated, do not modify ## -resources: -` +// RunCreateAPI runs the manifests SDK phase 2 plugin. +func RunCreateAPI(cfg *config.Config, gvk config.GVK) error { -// RunCreateAPI perform the SDK plugin-specific scaffolds. -func RunCreateAPI(cfg *config.Config) error { - - // Write CR paths to the samples' kustomization file. This file has a - // "do not modify" comment so it is safe to overwrite. - samplesKustomization := sampleKustomizationFragment - for _, gvk := range cfg.Resources { - samplesKustomization += fmt.Sprintf("- %s\n", makeCRFileName(gvk)) - } - kpath := filepath.Join("config", "samples") - if err := kustomize.Write(kpath, samplesKustomization); err != nil { + if err := newAPIScaffolder(cfg, gvk).scaffold(); err != nil { return err } return nil } -// todo(camilamacedo86): Now that we have the Kubebuilder scaffolding machinery included in our repo, we could make -// this an actual template that supports both file.Template and file.Inserter for init and create api, respectively. -// More info: https://github.com/operator-framework/operator-sdk/issues/3370 -// makeCRFileName returns a Custom Resource example file name in the same format -// as kubebuilder's CreateAPI plugin for a gvk. -func makeCRFileName(gvk config.GVK) string { - return fmt.Sprintf("%s_%s_%s.yaml", gvk.Group, gvk.Version, strings.ToLower(gvk.Kind)) +type apiScaffolder struct { + config *config.Config + gvk config.GVK +} + +func newAPIScaffolder(config *config.Config, gvk config.GVK) *apiScaffolder { + return &apiScaffolder{ + config: config, + gvk: gvk, + } +} + +func (s *apiScaffolder) newUniverse() *model.Universe { + return model.NewUniverse( + model.WithConfig(s.config), + ) +} + +func (s *apiScaffolder) scaffold() error { + var builders []file.Builder + // If the gvk is non-empty, add relevant builders. + if s.gvk.Group != "" || s.gvk.Version != "" || s.gvk.Kind != "" { + builders = append(builders, &kustomization{GroupVersionKind: s.gvk}) + } + + err := machinery.NewScaffold().Execute(s.newUniverse(), builders...) + if err != nil { + return fmt.Errorf("error scaffolding manifests: %v", err) + } + + return nil } diff --git a/internal/plugins/manifests/samples.go b/internal/plugins/manifests/samples.go new file mode 100644 index 00000000000..65e9ca5f5d0 --- /dev/null +++ b/internal/plugins/manifests/samples.go @@ -0,0 +1,75 @@ +// Copyright 2020 The Operator-SDK 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 manifests + +import ( + "fmt" + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &kustomization{} +var _ file.Inserter = &kustomization{} + +// kustomization scaffolds or updates the kustomization.yaml in config/samples. +type kustomization struct { + file.TemplateMixin + + // GroupVersionKind is the sample's gvk to add to this scaffold. + GroupVersionKind config.GVK +} + +// SetTemplateDefaults implements file.Template +func (f *kustomization) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "samples", "kustomization.yaml") + } + + f.TemplateBody = fmt.Sprintf(kustomizationTemplate, file.NewMarkerFor(f.Path, samplesMarker)) + + return nil +} + +const ( + samplesMarker = "manifestskustomizesamples" +) + +// GetMarkers implements file.Inserter +func (f *kustomization) GetMarkers() []file.Marker { + return []file.Marker{file.NewMarkerFor(f.Path, samplesMarker)} +} + +const samplesCodeFragment = `- %s +` + +// makeCRFileName returns a Custom Resource example file name in the same format +// as kubebuilder's CreateAPI plugin for a gvk. +func makeCRFileName(gvk config.GVK) string { + return fmt.Sprintf("%s_%s_%s.yaml", gvk.Group, gvk.Version, strings.ToLower(gvk.Kind)) +} + +// GetCodeFragments implements file.Inserter +func (f *kustomization) GetCodeFragments() file.CodeFragmentsMap { + return file.CodeFragmentsMap{ + file.NewMarkerFor(f.Path, samplesMarker): []string{fmt.Sprintf(samplesCodeFragment, makeCRFileName(f.GroupVersionKind))}, + } +} + +const kustomizationTemplate = `## Append samples you want in your CSV to this file as resources ## +resources: +%s +`