diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_test.go index f66f9c450f3..665460fbdb9 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_test.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_test.go @@ -42,9 +42,10 @@ var ( csvBasesDir = filepath.Join(csvDir, "bases") csvNewLayoutBundleDir = filepath.Join(csvDir, "output") - goTestDataDir = filepath.Join(testDataDir, "go") - goStaticDir = filepath.Join(goTestDataDir, "static") - goBasicOperatorPath = filepath.Join(goStaticDir, "basic.operator.yaml") + goTestDataDir = filepath.Join(testDataDir, "go") + goStaticDir = filepath.Join(goTestDataDir, "static") + goBasicOperatorPath = filepath.Join(goStaticDir, "basic.operator.yaml") + goMultiVersionOperatorPath = filepath.Join(goStaticDir, "basic.multiversion.operator.yaml") ) var ( @@ -59,252 +60,320 @@ var ( // Updated CSVs newCSV, newCSVUIMeta *v1alpha1.ClusterServiceVersion newCSVUIMetaStr string -) - -var _ = BeforeSuite(func() { - col = &collector.Manifests{} - collectManifestsFromFileHelper(col, goBasicOperatorPath) - initTestCSVsHelper() -}) + // Base Multiversion CSV + baseMultiVersionCSV *v1alpha1.ClusterServiceVersion -var _ = Describe("Generating a ClusterServiceVersion", func() { - format.TruncatedDiff = true - format.UseStringerRepresentation = true - - var ( - g Generator - buf *bytes.Buffer - operatorName = "memcached-operator" - zeroZeroOne = "0.0.1" - zeroZeroTwo = "0.0.2" - ) + // Multiversion CSVs + multiVersionCSVStr string +) +var _ = Describe("Testing CRDs with single version", func() { BeforeEach(func() { - buf = &bytes.Buffer{} + col = &collector.Manifests{} + collectManifestsFromFileHelper(col, goBasicOperatorPath) + initTestCSVsHelper() }) - Describe("for a Go project", func() { + var _ = Describe("Generating a ClusterServiceVersion", func() { + format.TruncatedDiff = true + format.UseStringerRepresentation = true - Context("with correct Options", func() { + var ( + g Generator + buf *bytes.Buffer + operatorName = "memcached-operator" + zeroZeroOne = "0.0.1" + zeroZeroTwo = "0.0.2" + ) - var ( - tmp string - err error - ) + BeforeEach(func() { + buf = &bytes.Buffer{} + }) - BeforeEach(func() { - tmp, err = ioutil.TempDir(".", "") - Expect(err).ToNot(HaveOccurred()) - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSVUIMeta} + Describe("for a Go project", func() { + + Context("with correct Options", func() { + + var ( + tmp string + err error + ) + + BeforeEach(func() { + tmp, err = ioutil.TempDir(".", "") + Expect(err).ToNot(HaveOccurred()) + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSVUIMeta} + }) + + AfterEach(func() { + if tmp != "" { + os.RemoveAll(tmp) + } + col.ClusterServiceVersions = nil + }) + + It("should write a ClusterServiceVersion manifest to an io.Writer", func() { + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + opts := []Option{ + WithWriter(buf), + } + Expect(g.Generate(opts...)).ToNot(HaveOccurred()) + Expect(buf.String()).To(MatchYAML(newCSVUIMetaStr)) + }) + It("should write a ClusterServiceVersion manifest to a bundle file", func() { + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + opts := []Option{ + WithBundleWriter(tmp), + } + Expect(g.Generate(opts...)).ToNot(HaveOccurred()) + outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName)) + Expect(outputFile).To(BeAnExistingFile()) + Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr)) + }) + It("should write a ClusterServiceVersion manifest to a package file", func() { + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + opts := []Option{ + WithPackageWriter(tmp), + } + Expect(g.Generate(opts...)).ToNot(HaveOccurred()) + outputFile := filepath.Join(tmp, g.Version, makeCSVFileName(operatorName)) + Expect(outputFile).To(BeAnExistingFile()) + Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr)) + }) }) - AfterEach(func() { - if tmp != "" { - os.RemoveAll(tmp) - } - col.ClusterServiceVersions = nil + Context("with incorrect Options", func() { + + BeforeEach(func() { + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + }) + + It("should return an error without any Options", func() { + opts := []Option{} + Expect(g.Generate(opts...)).To(MatchError(noGetWriterError)) + }) }) - It("should write a ClusterServiceVersion manifest to an io.Writer", func() { - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } - opts := []Option{ - WithWriter(buf), - } - Expect(g.Generate(opts...)).ToNot(HaveOccurred()) - Expect(buf.String()).To(MatchYAML(newCSVUIMetaStr)) + Context("to create a new ClusterServiceVersion", func() { + It("should return a default object when no base is supplied", func() { + col.ClusterServiceVersions = nil + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*(bases.New(operatorName))} + csvExp, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(csvExp)) + }) + + It("should return a default object", func() { + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSV} + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(newCSV)) + }) + It("should return an object with '.spec.replaces' and '.metadata.annotations['olm.skipRange']'", func() { + baseCSVUIMetaIn := baseCSVUIMeta.DeepCopy() + baseCSVUIMetaIn.GetAnnotations()["olm.skipRange"] = "<0.0.2" + baseCSVUIMetaIn.Spec.Replaces = "memcached-operator.v0.0.2" + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSVUIMetaIn} + g = Generator{ + OperatorName: operatorName, + Version: "0.0.3", + Collector: col, + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + csvExp := newCSVUIMeta.DeepCopy() + csvExp.SetName("memcached-operator.v0.0.3") + csvExp.GetAnnotations()["olm.skipRange"] = "<0.0.2" + csvExp.Spec.Replaces = "memcached-operator.v0.0.2" + csvExp.Spec.Version.Patch = 3 + Expect(csv).To(Equal(csvExp)) + }) + It("should return a new object with version set", func() { + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSVUIMeta} + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: col, + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(newCSVUIMeta)) + }) + It("should return a new object with base version and name preserved", func() { + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*newCSVUIMeta} + g = Generator{ + OperatorName: operatorName, + Collector: col, + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(newCSVUIMeta)) + }) }) - It("should write a ClusterServiceVersion manifest to a bundle file", func() { - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } - opts := []Option{ - WithBundleWriter(tmp), - } - Expect(g.Generate(opts...)).ToNot(HaveOccurred()) - outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName)) - Expect(outputFile).To(BeAnExistingFile()) - Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr)) + + Context("to update an existing ClusterServiceVersion", func() { + It("should return an updated object", func() { + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroOne, + Collector: &collector.Manifests{ + ClusterServiceVersions: []v1alpha1.ClusterServiceVersion{*newCSVUIMeta}, + }, + } + // Update the input's and expected CSV's Deployment image. + collectManifestsFromFileHelper(g.Collector, goBasicOperatorPath) + Expect(len(g.Collector.Deployments)).To(BeNumerically(">=", 1)) + imageTag := "controller:v" + g.Version + modifyDepImageHelper(&g.Collector.Deployments[0].Spec, imageTag) + updatedCSV := updateCSV(newCSVUIMeta, modifyCSVDepImageHelper(imageTag)) + + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(updatedCSV)) + + // verify if conversion webhooks are added + Expect(len(csv.Spec.WebhookDefinitions)).NotTo(Equal(0)) + Expect(containsConversionWebhookDefinition(csv.Spec.WebhookDefinitions)).To(BeTrue()) + }) }) - It("should write a ClusterServiceVersion manifest to a package file", func() { - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } - opts := []Option{ - WithPackageWriter(tmp), - } - Expect(g.Generate(opts...)).ToNot(HaveOccurred()) - outputFile := filepath.Join(tmp, g.Version, makeCSVFileName(operatorName)) - Expect(outputFile).To(BeAnExistingFile()) - Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr)) + + Context("to upgrade an existing ClusterServiceVersion", func() { + It("should return an upgraded object", func() { + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*newCSVUIMeta} + g = Generator{ + OperatorName: operatorName, + Version: zeroZeroTwo, + Collector: col, + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(upgradeCSV(newCSVUIMeta, g.OperatorName, g.Version))) + }) }) }) + }) - Context("with incorrect Options", func() { + var _ = Describe("Generation requires interaction", func() { + var ( + testExistingPath = filepath.Join(csvBasesDir, "memcached-operator.clusterserviceversion.yaml") + testNotExistingPath = filepath.Join(csvBasesDir, "notexist.clusterserviceversion.yaml") + ) - BeforeEach(func() { - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } + Context("when base path does not exist", func() { + By("turning interaction off explicitly") + It("returns false", func() { + Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveHardOff)).To(BeFalse()) }) - - It("should return an error without any Options", func() { - opts := []Option{} - Expect(g.Generate(opts...)).To(MatchError(noGetWriterError)) + By("turning interaction off implicitly") + It("returns true", func() { + Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveSoftOff)).To(BeTrue()) }) - }) - - Context("to create a new ClusterServiceVersion", func() { - It("should return a default object when no base is supplied", func() { - col.ClusterServiceVersions = nil - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } - csv, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*(bases.New(operatorName))} - csvExp, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - Expect(csv).To(Equal(csvExp)) + By("turning interaction on explicitly") + It("returns true", func() { + Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveOnAll)).To(BeTrue()) }) + }) - It("should return a default object", func() { - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSV} - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } - csv, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - Expect(csv).To(Equal(newCSV)) + Context("when base path does exist", func() { + By("turning interaction off explicitly") + It("returns false", func() { + Expect(requiresInteraction(testExistingPath, projutil.InteractiveHardOff)).To(BeFalse()) }) - It("should return an object with '.spec.replaces' and '.metadata.annotations['olm.skipRange']'", func() { - baseCSVUIMetaIn := baseCSVUIMeta.DeepCopy() - baseCSVUIMetaIn.GetAnnotations()["olm.skipRange"] = "<0.0.2" - baseCSVUIMetaIn.Spec.Replaces = "memcached-operator.v0.0.2" - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSVUIMetaIn} - g = Generator{ - OperatorName: operatorName, - Version: "0.0.3", - Collector: col, - } - csv, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - csvExp := newCSVUIMeta.DeepCopy() - csvExp.SetName("memcached-operator.v0.0.3") - csvExp.GetAnnotations()["olm.skipRange"] = "<0.0.2" - csvExp.Spec.Replaces = "memcached-operator.v0.0.2" - csvExp.Spec.Version.Patch = 3 - Expect(csv).To(Equal(csvExp)) + By("turning interaction off implicitly") + It("returns false", func() { + Expect(requiresInteraction(testExistingPath, projutil.InteractiveSoftOff)).To(BeFalse()) }) - It("should return a new object with version set", func() { - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseCSVUIMeta} - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: col, - } - csv, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - Expect(csv).To(Equal(newCSVUIMeta)) - }) - It("should return a new object with base version and name preserved", func() { - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*newCSVUIMeta} - g = Generator{ - OperatorName: operatorName, - Collector: col, - } - csv, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - Expect(csv).To(Equal(newCSVUIMeta)) + By("turning interaction on explicitly") + It("returns true", func() { + Expect(requiresInteraction(testExistingPath, projutil.InteractiveOnAll)).To(BeTrue()) }) }) + }) - Context("to update an existing ClusterServiceVersion", func() { - It("should return an updated object", func() { - g = Generator{ - OperatorName: operatorName, - Version: zeroZeroOne, - Collector: &collector.Manifests{ - ClusterServiceVersions: []v1alpha1.ClusterServiceVersion{*newCSVUIMeta}, - }, - } - // Update the input's and expected CSV's Deployment image. - collectManifestsFromFileHelper(g.Collector, goBasicOperatorPath) - Expect(len(g.Collector.Deployments)).To(BeNumerically(">=", 1)) - imageTag := "controller:v" + g.Version - modifyDepImageHelper(&g.Collector.Deployments[0].Spec, imageTag) - updatedCSV := updateCSV(newCSVUIMeta, modifyCSVDepImageHelper(imageTag)) - - csv, err := g.generate() +}) + +var _ = Describe("Testing CRDs with multiple version", func() { + BeforeEach(func() { + col = &collector.Manifests{} + collectManifestsFromFileHelper(col, goMultiVersionOperatorPath) + initTestMultiVersionCSVHelper() + }) + + var _ = Describe("Generating a clusterserviceVersion", func() { + format.TruncatedDiff = true + format.UseStringerRepresentation = true + + var ( + g Generator + buf *bytes.Buffer + operatorName = "memcached-operator" + zeroZeroOne = "0.0.1" + ) + + BeforeEach(func() { + buf = &bytes.Buffer{} + }) + + Describe("for a go project", func() { + var ( + err error + ) + + BeforeEach(func() { Expect(err).ToNot(HaveOccurred()) - Expect(csv).To(Equal(updatedCSV)) + col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*baseMultiVersionCSV} }) - }) - Context("to upgrade an existing ClusterServiceVersion", func() { - It("should return an upgraded object", func() { - col.ClusterServiceVersions = []v1alpha1.ClusterServiceVersion{*newCSVUIMeta} + AfterEach(func() { + col.ClusterServiceVersions = nil + }) + + // TODO: to verify conversion webhook specifically, this block has just the + // tests to see if csv is being written cirrectly to io.Writer. + It("testing clustersevice version being written to io.Writer", func() { g = Generator{ OperatorName: operatorName, - Version: zeroZeroTwo, + Version: zeroZeroOne, Collector: col, } - csv, err := g.generate() - Expect(err).ToNot(HaveOccurred()) - Expect(csv).To(Equal(upgradeCSV(newCSVUIMeta, g.OperatorName, g.Version))) + opts := []Option{ + WithWriter(buf), + } + Expect(g.Generate(opts...)).ToNot(HaveOccurred()) + Expect(buf.String()).To(MatchYAML(multiVersionCSVStr)) }) - }) - }) -}) -var _ = Describe("Generation requires interaction", func() { - var ( - testExistingPath = filepath.Join(csvBasesDir, "memcached-operator.clusterserviceversion.yaml") - testNotExistingPath = filepath.Join(csvBasesDir, "notexist.clusterserviceversion.yaml") - ) - - Context("when base path does not exist", func() { - By("turning interaction off explicitly") - It("returns false", func() { - Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveHardOff)).To(BeFalse()) - }) - By("turning interaction off implicitly") - It("returns true", func() { - Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveSoftOff)).To(BeTrue()) }) - By("turning interaction on explicitly") - It("returns true", func() { - Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveOnAll)).To(BeTrue()) - }) - }) - Context("when base path does exist", func() { - By("turning interaction off explicitly") - It("returns false", func() { - Expect(requiresInteraction(testExistingPath, projutil.InteractiveHardOff)).To(BeFalse()) - }) - By("turning interaction off implicitly") - It("returns false", func() { - Expect(requiresInteraction(testExistingPath, projutil.InteractiveSoftOff)).To(BeFalse()) - }) - By("turning interaction on explicitly") - It("returns true", func() { - Expect(requiresInteraction(testExistingPath, projutil.InteractiveOnAll)).To(BeTrue()) - }) }) }) @@ -331,6 +400,17 @@ func initTestCSVsHelper() { ExpectWithOffset(1, err).ToNot(HaveOccurred()) } +func initTestMultiVersionCSVHelper() { + var err error + path := filepath.Join(csvBasesDir, "memcached-operator-multiVersion.yaml") + baseMultiVersionCSV, _, err = getCSVFromFile(path) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + path = filepath.Join(csvNewLayoutBundleDir, "memcached-operator-multiVersion.yaml") + _, multiVersionCSVStr, err = getCSVFromFile(path) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) +} + func readFileHelper(path string) string { b, err := ioutil.ReadFile(path) ExpectWithOffset(1, err).ToNot(HaveOccurred()) @@ -395,3 +475,12 @@ func upgradeCSV(csv *v1alpha1.ClusterServiceVersion, name, version string) *v1al return upgraded } + +func containsConversionWebhookDefinition(whdef []v1alpha1.WebhookDescription) bool { + for _, def := range whdef { + if def.Type == v1alpha1.ConversionWebhook { + return true + } + } + return false +} diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go index 233bb3484f0..7be34437be5 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" @@ -28,6 +29,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "github.com/operator-framework/operator-sdk/internal/generate/collector" "github.com/operator-framework/operator-sdk/internal/util/k8sutil" @@ -295,9 +297,148 @@ func applyWebhooks(c *collector.Manifests, csv *operatorsv1alpha1.ClusterService } webhookDescriptions = append(webhookDescriptions, mutatingToWebhookDescription(webhook, depName, svc)) } + + for _, svc := range c.Services { + crdToConfigMap := getConvWebhookCRDNamesAndConfig(c, svc.GetName()) + + if len(crdToConfigMap) != 0 { + depName := findMatchingDepNameFromService(c, &svc) + des := conversionToWebhookDescription(crdToConfigMap, depName, &svc) + webhookDescriptions = append(webhookDescriptions, des...) + } + } csv.Spec.WebhookDefinitions = webhookDescriptions } +// conversionToWebhookDescription takes in a map of {crdNames, apiextv.WebhookConversion} and groups +// all the crds with same port and path. It then creates a webhook description for each unique combination of +// port and path. +// For example: if we have the following map: {crd1:[portX+pathX], crd2: [portX+pathX], crd3: [portY:partY]}, +// we will create 2 webhook descriptions: one with [portX+pathX]:[crd1, crd2] and the other with [portY:pathY]:[crd3] +func conversionToWebhookDescription(crdToConfig map[string]apiextv1.WebhookConversion, depName string, ws *corev1.Service) []operatorsv1alpha1.WebhookDescription { + des := make([]operatorsv1alpha1.WebhookDescription, 0) + + // this is a map of serviceportAndPath configs, and the respective CRDs. + webhookDescriptions := crdGroups(crdToConfig) + + for serviceConfig, crds := range webhookDescriptions { + // we need this to get the conversionReviewVersions. + // here, we assume all crds having same servicePortAndPath config will have + // same conversion review versions. + config, ok := crdToConfig[crds[0]] + if !ok { + log.Infof("Webhook config for CRD %q not found", crds[0]) + continue + } + + description := operatorsv1alpha1.WebhookDescription{ + Type: operatorsv1alpha1.ConversionWebhook, + ConversionCRDs: crds, + AdmissionReviewVersions: config.ConversionReviewVersions, + WebhookPath: &serviceConfig.Path, + DeploymentName: depName, + GenerateName: getGenerateName(crds), + SideEffects: func() *admissionregv1.SideEffectClass { + seNone := admissionregv1.SideEffectClassNone + return &seNone + }(), + } + + if len(description.AdmissionReviewVersions) == 0 { + log.Infof("ConversionReviewVersion not found for the deployment %q", depName) + } + + var webhookServiceRefPort int32 = 443 + + if serviceConfig.Port != nil { + webhookServiceRefPort = *serviceConfig.Port + } + + if ws != nil { + for _, port := range ws.Spec.Ports { + if webhookServiceRefPort == port.Port { + description.ContainerPort = port.Port + description.TargetPort = &port.TargetPort + break + } + } + } + + if description.DeploymentName == "" { + if config.ClientConfig.Service != nil { + description.DeploymentName = strings.TrimSuffix(config.ClientConfig.Service.Name, "-service") + } + } + + description.WebhookPath = &serviceConfig.Path + des = append(des, description) + } + + return des +} + +// serviceportPath is refers to the group of webhook service and +// path names and port. +type serviceportPath struct { + Port *int32 + Path string +} + +// crdGroups groups the crds with similar service port and name. It returns a map of serviceportPath +// and the corresponding crd names. +func crdGroups(crdToConfig map[string]apiextv1.WebhookConversion) map[serviceportPath][]string { + + uniqueConfig := make(map[serviceportPath][]string) + + for crdName, config := range crdToConfig { + serviceportPath := serviceportPath{ + Port: config.ClientConfig.Service.Port, + Path: *config.ClientConfig.Service.Path, + } + + uniqueConfig[serviceportPath] = append(uniqueConfig[serviceportPath], crdName) + } + + return uniqueConfig +} + +func getConvWebhookCRDNamesAndConfig(c *collector.Manifests, serviceName string) map[string]apiextv1.WebhookConversion { + if serviceName == "" { + return nil + } + + crdToConfig := make(map[string]apiextv1.WebhookConversion) + + for _, crd := range c.V1CustomResourceDefinitions { + if crd.Spec.Conversion != nil { + whConv := crd.Spec.Conversion.Webhook + if whConv != nil && whConv.ClientConfig != nil && whConv.ClientConfig.Service != nil { + if whConv.ClientConfig.Service.Name == serviceName { + crdToConfig[crd.GetName()] = *whConv + } + } + } + } + + for _, crd := range c.V1beta1CustomResourceDefinitions { + whConv := crd.Spec.Conversion + if whConv != nil && whConv.WebhookClientConfig != nil && whConv.WebhookClientConfig.Service != nil { + if whConv.WebhookClientConfig.Service.Name == serviceName { + v1whConv := apiextv1.WebhookConversion{ + ClientConfig: &apiextv1.WebhookClientConfig{Service: &apiextv1.ServiceReference{}}, + ConversionReviewVersions: crd.Spec.Conversion.ConversionReviewVersions, + } + if path := whConv.WebhookClientConfig.Service.Path; path != nil { + v1whConv.ClientConfig.Service.Path = new(string) + *v1whConv.ClientConfig.Service.Path = *path + } + crdToConfig[crd.GetName()] = v1whConv + } + } + } + return crdToConfig +} + // The default AdmissionReviewVersions set in a CSV if not set in the source webhook. var defaultAdmissionReviewVersions = []string{"v1beta1"} @@ -391,8 +532,7 @@ func mutatingToWebhookDescription(webhook admissionregv1.MutatingWebhook, depNam } // findMatchingDeploymentAndServiceForWebhook matches a Service to a webhook's client config (if it uses a service) -// then matches that Service to a Deployment by comparing label selectors (if the Service uses label selectors). -// The names of both Service and Deployment are returned if found. +// and uses that service to find the deployment name. func findMatchingDeploymentAndServiceForWebhook(c *collector.Manifests, wcc admissionregv1.WebhookClientConfig) (depName string, ws *corev1.Service) { // Return if a service reference is not specified, since a URL will be in that case. if wcc.Service == nil { @@ -422,7 +562,15 @@ func findMatchingDeploymentAndServiceForWebhook(c *collector.Manifests, wcc admi return } - // Match service against pod labels, in which the webhook server will be running. + depName = findMatchingDepNameFromService(c, ws) + + return depName, ws +} + +// findMatchingDepNameFromService matches the provided service to a deployment by comparing label selectors (if +// Service uses label selectors). +func findMatchingDepNameFromService(c *collector.Manifests, ws *corev1.Service) (depName string) { + // Match service against pod labels, in which the webhook server will be running for _, dep := range c.Deployments { podTemplateLabels := dep.Spec.Template.GetLabels() if len(podTemplateLabels) == 0 { @@ -441,8 +589,7 @@ func findMatchingDeploymentAndServiceForWebhook(c *collector.Manifests, wcc admi break } } - - return depName, ws + return depName } // applyCustomResources updates csv's "alm-examples" annotation with the @@ -495,3 +642,16 @@ func validate(csv *operatorsv1alpha1.ClusterServiceVersion) error { return nil } + +// generateName takes in a list of crds, and returns a conversion webhook generator name. +func getGenerateName(crds []string) string { + sort.Strings(crds) + joinedResourceNames := strings.Builder{} + + for _, name := range crds { + if name != "" { + joinedResourceNames.WriteString(strings.Split(name, ".")[0]) + } + } + return fmt.Sprintf("c%s.kb.io", joinedResourceNames.String()) +} diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go index a3bf70aac81..24b0fb4e886 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go @@ -332,6 +332,49 @@ var _ = Describe("findMatchingDeploymentAndServiceForWebhook", func() { Expect(service.GetName()).To(Equal(serviceName1)) }) }) + + Context("crdGroups", func() { + path1 := "/whPath" + port1 := new(int32) + *port1 = 2311 + crdToConfigPath := map[string]apiextv1.WebhookConversion{ + "crd-test-1": apiextv1.WebhookConversion{ + ClientConfig: &apiextv1.WebhookClientConfig{ + Service: &apiextv1.ServiceReference{ + Path: &path1, + Port: port1, + }, + }, + }, + + "crd-test-2": apiextv1.WebhookConversion{ + ClientConfig: &apiextv1.WebhookClientConfig{ + Service: &apiextv1.ServiceReference{ + Path: &path1, + Port: port1, + }, + }, + }, + } + + val := crdGroups(crdToConfigPath) + + Expect(len(val)).To(BeEquivalentTo(1)) + + test := serviceportPath{ + Port: port1, + Path: path1, + } + + g := val[test] + + Expect(g).NotTo(BeNil()) + Expect(len(g)).To(BeEquivalentTo(2)) + Expect(g).To(ContainElement("crd-test-2")) + Expect(g).To(ContainElement("crd-test-1")) + + }) + }) func newDeployment(name string, labels map[string]string) (dep appsv1.Deployment) { diff --git a/internal/generate/testdata/clusterserviceversions/bases/memcached-operator-multiVersion.yaml b/internal/generate/testdata/clusterserviceversions/bases/memcached-operator-multiVersion.yaml new file mode 100644 index 00000000000..460a4b6f861 --- /dev/null +++ b/internal/generate/testdata/clusterserviceversions/bases/memcached-operator-multiVersion.yaml @@ -0,0 +1,64 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: memcached-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached1 + name: memcacheds1.cache1.example.com + version: v1alpha1 + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached1 + name: memcacheds1.cache1.example.com + version: v1 + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached + name: memcacheds.cache.example.com + version: v1 + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached + name: memcacheds.cache.example.com + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.0 + diff --git a/internal/generate/testdata/clusterserviceversions/output/memcached-operator-multiVersion.yaml b/internal/generate/testdata/clusterserviceversions/output/memcached-operator-multiVersion.yaml new file mode 100644 index 00000000000..9c25804cd44 --- /dev/null +++ b/internal/generate/testdata/clusterserviceversions/output/memcached-operator-multiVersion.yaml @@ -0,0 +1,262 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "cache.example.com/v1", + "kind": "Memcached", + "metadata": { + "name": "memcached-sample" + }, + "spec": { + "foo": "bar" + } + } + ] + capabilities: Basic Install + name: memcached-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached + name: memcacheds.cache.example.com + version: v1 + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached + kind: Memcached + name: memcacheds.cache.example.com + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - cache.example.com + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - cache.example.com + resources: + - memcacheds/finalizers + verbs: + - update + - apiGroups: + - cache.example.com + resources: + - memcacheds/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: default + deployments: + - name: memcached-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + strategy: {} + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + resources: {} + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: controller:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + securityContext: {} + securityContext: + runAsNonRoot: true + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + - coordination.k8s.io + resources: + - configmaps + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: default + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1 + - v1beta1 + containerPort: 443 + deploymentName: memcached-operator-controller-manager + failurePolicy: Fail + generateName: vmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-cache-example-com-v1alpha1-memcached + - admissionReviewVersions: + - v1 + - v1beta1 + containerPort: 443 + deploymentName: memcached-operator-controller-manager + failurePolicy: Fail + generateName: mmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-cache-example-com-v1alpha1-memcached + - admissionReviewVersions: null + containerPort: 443 + conversionCRDs: + - memcacheds.cache.example.com + deploymentName: memcached-operator-controller-manager + generateName: cmemcacheds.kb.io + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert diff --git a/internal/generate/testdata/clusterserviceversions/output/memcached-operator.clusterserviceversion.yaml b/internal/generate/testdata/clusterserviceversions/output/memcached-operator.clusterserviceversion.yaml index 70cca5233e4..238080f6a7e 100644 --- a/internal/generate/testdata/clusterserviceversions/output/memcached-operator.clusterserviceversion.yaml +++ b/internal/generate/testdata/clusterserviceversions/output/memcached-operator.clusterserviceversion.yaml @@ -197,3 +197,14 @@ spec: targetPort: 9443 type: MutatingAdmissionWebhook webhookPath: /mutate-cache-my-domain-v1alpha1-memcached + - admissionReviewVersions: + - v1beta1 + containerPort: 443 + conversionCRDs: + - memcacheds.cache.example.com + deploymentName: memcached-operator-controller-manager + generateName: cmemcacheds.kb.io + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert diff --git a/internal/generate/testdata/clusterserviceversions/output/with-ui-metadata.clusterserviceversion.yaml b/internal/generate/testdata/clusterserviceversions/output/with-ui-metadata.clusterserviceversion.yaml index f83ff0d2598..41772985ae0 100644 --- a/internal/generate/testdata/clusterserviceversions/output/with-ui-metadata.clusterserviceversion.yaml +++ b/internal/generate/testdata/clusterserviceversions/output/with-ui-metadata.clusterserviceversion.yaml @@ -253,3 +253,14 @@ spec: targetPort: 9443 type: MutatingAdmissionWebhook webhookPath: /mutate-cache-my-domain-v1alpha1-memcached + - admissionReviewVersions: + - v1beta1 + containerPort: 443 + conversionCRDs: + - memcacheds.cache.example.com + deploymentName: memcached-operator-controller-manager + generateName: cmemcacheds.kb.io + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert diff --git a/internal/generate/testdata/go/static/basic.multiversion.operator.yaml b/internal/generate/testdata/go/static/basic.multiversion.operator.yaml new file mode 100644 index 00000000000..9132f2a0386 --- /dev/null +++ b/internal/generate/testdata/go/static/basic.multiversion.operator.yaml @@ -0,0 +1,462 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: memcached-operator-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: memcached-operator-system/memcached-operator-serving-cert + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: memcacheds.cache.example.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: memcached-operator-webhook-service + namespace: memcached-operator-system + path: /convert + conversionReviewVersions: null + group: cache.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Memcached is the Schema for the memcacheds API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MemcachedSpec defines the desired state of Memcached + properties: + foo: + description: Foo is an example field of Memcached. Edit memcached_types.go + to remove/update + type: string + schedule: + description: describes a Cron schedule. + properties: + dayOfMonth: + description: specifies the day of the month during which the job + executes. + type: string + dayOfWeek: + description: specifies the day of the week during which the job + executes. + type: string + hour: + description: specifies the hour during which the job executes. + type: string + minute: + description: specifies the minute during which the job executes. + type: string + month: + description: specifies the month during which the job executes. + type: string + type: object + size: + format: int32 + type: integer + required: + - schedule + - size + type: object + status: + description: MemcachedStatus defines the observed state of Memcached + properties: + nodes: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + items: + type: string + type: array + required: + - nodes + type: object + type: object + served: true + storage: false + subresources: + status: {} + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Memcached is the Schema for the memcacheds API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MemcachedSpec defines the desired state of Memcached + properties: + foo: + description: Foo is an example field of Memcached. Edit memcached_types.go + to remove/update + type: string + schedule: + type: string + size: + description: Size defines the number of Memcached instances + format: int32 + type: integer + required: + - schedule + type: object + status: + description: MemcachedStatus defines the observed state of Memcached + properties: + nodes: + description: Nodes store the name of the pods which are running Memcached + instances + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: memcached-operator-mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: memcached-operator-webhook-service + namespace: memcached-operator-system + path: /mutate-cache-example-com-v1alpha1-memcached + failurePolicy: Fail + name: mmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None +--- +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: memcached-operator-leader-election-role + namespace: memcached-operator-system +rules: +- apiGroups: + - "" + - coordination.k8s.io + resources: + - configmaps + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: memcached-operator-manager-role +rules: +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cache.example.com + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cache.example.com + resources: + - memcacheds/finalizers + verbs: + - update +- apiGroups: + - cache.example.com + resources: + - memcacheds/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: memcached-operator-proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: memcached-operator-metrics-reader +rules: +- nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: memcached-operator-leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: memcached-operator-leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: memcached-operator-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: default + namespace: memcached-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: memcached-operator-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: memcached-operator-manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: memcached-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: memcached-operator-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: memcached-operator-proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: memcached-operator-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: memcached-operator-controller-manager + name: memcached-operator-controller-manager-metrics-service + namespace: memcached-operator-system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: v1 +kind: Service +metadata: + name: memcached-operator-webhook-service + namespace: memcached-operator-system +spec: + ports: + - port: 443 + targetPort: 9443 + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: memcached-operator-controller-manager + namespace: memcached-operator-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: controller:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + securityContext: + allowPrivateEscalation: false + securityContext: + runAsNonRoot: true + terminationGracePeriodSeconds: 10 +--- +apiVersion: cache.example.com/v1 +kind: Memcached +metadata: + name: memcached-sample +spec: + # Add fields here + foo: bar +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: memcached-operator-validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: memcached-operator-webhook-service + namespace: memcached-operator-system + path: /validate-cache-example-com-v1alpha1-memcached + failurePolicy: Fail + name: vmemcached.kb.io + rules: + - apiGroups: + - cache.example.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None \ No newline at end of file diff --git a/internal/generate/testdata/go/static/basic.operator.yaml b/internal/generate/testdata/go/static/basic.operator.yaml index 29d87cb3621..1767ae5dcb0 100644 --- a/internal/generate/testdata/go/static/basic.operator.yaml +++ b/internal/generate/testdata/go/static/basic.operator.yaml @@ -13,6 +13,16 @@ metadata: creationTimestamp: null name: memcacheds.cache.example.com spec: + conversion: + strategy: Webhook + webhookClientConfig: + caBundle: Cg== + service: + name: memcached-operator-webhook-service + namespace: memcached-operator-system + path: /convert + conversionReviewVersions: + - v1beta1 group: cache.example.com names: kind: Memcached