From 0e9c4ed46cd6b399dc58ce6ef4fa82f9826d0309 Mon Sep 17 00:00:00 2001 From: Cameron Garrison Date: Tue, 23 Jan 2024 06:05:10 -0500 Subject: [PATCH] feat(templates): process Feature templates in memory (#797) Co-authored-by: Bartosz Majsak Co-authored-by: Wen Zhou --- components/kserve/serverless_setup.go | 10 +- .../dscinitialization/servicemesh_setup.go | 12 +- pkg/feature/builder.go | 17 +- pkg/feature/feature.go | 22 +- pkg/feature/manifest.go | 88 +++-- pkg/feature/raw_resources.go | 21 +- pkg/feature/template_loader.go | 39 --- ...-gateway.tmpl => istio-local-gateway.yaml} | 0 .../features/crd/istio-gateway.crd.yaml | 313 ++++++++++++++++++ .../integration/features/features_int_test.go | 148 ++++++++- .../features/servicemesh_feature_test.go | 19 ++ .../features/templates/namespace.yaml | 4 + 12 files changed, 579 insertions(+), 114 deletions(-) delete mode 100644 pkg/feature/template_loader.go rename pkg/feature/templates/serverless/serving-istio-gateways/{istio-local-gateway.tmpl => istio-local-gateway.yaml} (100%) create mode 100644 tests/integration/features/crd/istio-gateway.crd.yaml create mode 100644 tests/integration/features/templates/namespace.yaml diff --git a/components/kserve/serverless_setup.go b/components/kserve/serverless_setup.go index a86f88ad373..83d2b6cf877 100644 --- a/components/kserve/serverless_setup.go +++ b/components/kserve/serverless_setup.go @@ -2,7 +2,6 @@ package kserve import ( "path" - "path/filepath" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/feature" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/feature/serverless" @@ -16,15 +15,10 @@ const ( ) func (k *Kserve) configureServerlessFeatures(s *feature.FeaturesInitializer) error { - var rootDir = filepath.Join(feature.BaseOutputDir, s.DSCInitializationSpec.ApplicationsNamespace) - if err := feature.CopyEmbeddedFiles(templatesDir, rootDir); err != nil { - return err - } - servingDeployment, err := feature.CreateFeature("serverless-serving-deployment"). For(s.DSCInitializationSpec). Manifests( - path.Join(rootDir, templatesDir, "serving-install"), + path.Join(templatesDir, "serving-install"), ). WithData(PopulateComponentSettings(k)). PreConditions( @@ -55,7 +49,7 @@ func (k *Kserve) configureServerlessFeatures(s *feature.FeaturesInitializer) err ). WithResources(serverless.ServingCertificateResource). Manifests( - path.Join(rootDir, templatesDir, "serving-istio-gateways"), + path.Join(templatesDir, "serving-istio-gateways"), ). Load() if err != nil { diff --git a/controllers/dscinitialization/servicemesh_setup.go b/controllers/dscinitialization/servicemesh_setup.go index e36d3c367f4..e9250d57922 100644 --- a/controllers/dscinitialization/servicemesh_setup.go +++ b/controllers/dscinitialization/servicemesh_setup.go @@ -2,7 +2,6 @@ package dscinitialization import ( "path" - "path/filepath" operatorv1 "github.com/openshift/api/operator/v1" corev1 "k8s.io/api/core/v1" @@ -42,7 +41,7 @@ func (r *DSCInitializationReconciler) configureServiceMesh(instance *dsciv1.DSCI } func (r *DSCInitializationReconciler) removeServiceMesh(instance *dsciv1.DSCInitialization) error { - // on condition of Managed, do not handle Removed when set to Removed it tigger DSCI reconcile to cleanup + // on condition of Managed, do not handle Removed when set to Removed it trigger DSCI reconcile to cleanup if instance.Spec.ServiceMesh.ManagementState == operatorv1.Managed { serviceMeshInitializer := feature.NewFeaturesInitializer(&instance.Spec, configureServiceMeshFeatures) @@ -65,17 +64,12 @@ func (r *DSCInitializationReconciler) removeServiceMesh(instance *dsciv1.DSCInit } func configureServiceMeshFeatures(s *feature.FeaturesInitializer) error { - var rootDir = filepath.Join(feature.BaseOutputDir, s.DSCInitializationSpec.ApplicationsNamespace) - if err := feature.CopyEmbeddedFiles(templatesDir, rootDir); err != nil { - return err - } - serviceMeshSpec := s.ServiceMesh smcpCreation, errSmcp := feature.CreateFeature("mesh-control-plane-creation"). For(s.DSCInitializationSpec). Manifests( - path.Join(rootDir, templatesDir, "base", "create-smcp.tmpl"), + path.Join(templatesDir, "base", "create-smcp.tmpl"), ). PreConditions( servicemesh.EnsureServiceMeshOperatorInstalled, @@ -97,7 +91,7 @@ func configureServiceMeshFeatures(s *feature.FeaturesInitializer) error { servicemesh.EnsureServiceMeshInstalled, ). Manifests( - path.Join(rootDir, templatesDir, "metrics-collection"), + path.Join(templatesDir, "metrics-collection"), ). Load() if errMetrics != nil { diff --git a/pkg/feature/builder.go b/pkg/feature/builder.go index 6ec4aff6803..22dcd381671 100644 --- a/pkg/feature/builder.go +++ b/pkg/feature/builder.go @@ -1,6 +1,8 @@ package feature import ( + "io/fs" + "github.com/pkg/errors" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/client-go/dynamic" @@ -20,10 +22,14 @@ type featureBuilder struct { name string config *rest.Config builders []partialBuilder + fsys fs.FS } func CreateFeature(name string) *featureBuilder { //nolint:golint,revive //No need to export featureBuilder. - return &featureBuilder{name: name} + return &featureBuilder{ + name: name, + fsys: embeddedFiles, + } } func (fb *featureBuilder) For(spec *v1.DSCInitializationSpec) *featureBuilder { @@ -80,7 +86,7 @@ func (fb *featureBuilder) Manifests(paths ...string) *featureBuilder { var manifests []manifest for _, path := range paths { - manifests, err = loadManifestsFrom(path) + manifests, err = loadManifestsFrom(fb.fsys, path) if err != nil { return errors.WithStack(err) } @@ -188,3 +194,10 @@ func (fb *featureBuilder) withDefaultClient() error { fb.config = restCfg return nil } + +// ManifestSource sets the root file system (fsys) from which manifest paths are loaded +// If ManifestSource is not called in the builder chain, the default source will be the embeddedFiles. +func (fb *featureBuilder) ManifestSource(fsys fs.FS) *featureBuilder { + fb.fsys = fsys + return fb +} diff --git a/pkg/feature/feature.go b/pkg/feature/feature.go index 4911a4ce1fd..114349b2de0 100644 --- a/pkg/feature/feature.go +++ b/pkg/feature/feature.go @@ -30,7 +30,8 @@ type Feature struct { DynamicClient dynamic.Interface Client client.Client - manifests []manifest + manifests []manifest + cleanups []Action resources []Action preconditions []Action @@ -81,7 +82,7 @@ func (f *Feature) Apply() error { return dataLoadErr } - // create or update resources + // Create or update resources for _, resource := range f.resources { if err := resource(f); err != nil { return errors.WithStack(err) @@ -89,8 +90,8 @@ func (f *Feature) Apply() error { } // Process and apply manifests - for _, m := range f.manifests { - if err := m.processTemplate(f.Spec); err != nil { + for i := range f.manifests { + if err := f.manifests[i].process(f.Spec); err != nil { return errors.WithStack(err) } } @@ -99,6 +100,7 @@ func (f *Feature) Apply() error { return errors.WithStack(err) } + // Check all postconditions and collect errors for _, postcondition := range f.postconditions { multiErr = multierror.Append(multiErr, postcondition(f)) } @@ -170,27 +172,27 @@ func (f *Feature) addCleanup(cleanupFuncs ...Action) { f.cleanups = append(f.cleanups, cleanupFuncs...) } -type apply func(filename string) error +type apply func(data string) error func (f *Feature) apply(m manifest) error { var applier apply targetPath := m.targetPath() if m.patch { - applier = func(filename string) error { + applier = func(data string) error { f.Log.Info("patching using manifest", "feature", f.Name, "name", m.name, "path", targetPath) - return f.patchResourceFromFile(filename) + return f.patchResources(data) } } else { - applier = func(filename string) error { + applier = func(data string) error { f.Log.Info("applying manifest", "feature", f.Name, "name", m.name, "path", targetPath) - return f.createResourceFromFile(filename) + return f.createResources(data) } } - if err := applier(targetPath); err != nil { + if err := applier(m.processedContent); err != nil { f.Log.Error(err, "failed to create resource", "feature", f.Name, "name", m.name, "path", targetPath) return err diff --git a/pkg/feature/manifest.go b/pkg/feature/manifest.go index b0c7fce2247..2726e2d0e23 100644 --- a/pkg/feature/manifest.go +++ b/pkg/feature/manifest.go @@ -1,74 +1,104 @@ package feature import ( + "bytes" + "embed" "fmt" "html/template" - "os" + "io" + "io/fs" "path/filepath" "strings" - - "github.com/pkg/errors" ) -const BaseOutputDir = "/tmp/odh-operator" +//go:embed templates +var embeddedFiles embed.FS type manifest struct { name, - path string + path, + processedContent string template, - patch, - processed bool + patch bool + fsys fs.FS } -func loadManifestsFrom(path string) ([]manifest, error) { +func loadManifestsFrom(fsys fs.FS, path string) ([]manifest, error) { var manifests []manifest - if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + + err := fs.WalkDir(fsys, path, func(path string, dirEntry fs.DirEntry, walkErr error) error { + _, err := dirEntry.Info() if err != nil { return err } - if info.IsDir() { + + if dirEntry.IsDir() { return nil } - basePath := filepath.Base(path) - manifests = append(manifests, manifest{ - name: basePath, - path: path, - patch: strings.Contains(basePath, ".patch"), - template: filepath.Ext(path) == ".tmpl", - }) + m := createManifestFrom(fsys, path) + manifests = append(manifests, m) return nil - }); err != nil { - return nil, errors.WithStack(err) + }) + + if err != nil { + return nil, err } return manifests, nil } +func createManifestFrom(fsys fs.FS, path string) manifest { + basePath := filepath.Base(path) + m := manifest{ + name: basePath, + path: path, + patch: strings.Contains(basePath, ".patch"), + template: filepath.Ext(path) == ".tmpl", + fsys: fsys, + } + + return m +} + func (m *manifest) targetPath() string { return fmt.Sprintf("%s%s", m.path[:len(m.path)-len(filepath.Ext(m.path))], ".yaml") } -func (m *manifest) processTemplate(data interface{}) error { - if !m.template { - return nil +func (m *manifest) process(data interface{}) error { + manifestFile, err := m.open() + if err != nil { + return err } - path := m.targetPath() + defer manifestFile.Close() - f, err := os.Create(path) + content, err := io.ReadAll(manifestFile) if err != nil { return fmt.Errorf("failed to create file: %w", err) } - tmpl := template.New(m.name).Funcs(template.FuncMap{"ReplaceChar": ReplaceChar}) + if !m.template { + // If, by convention, the file is not suffixed with `.tmpl` we do not need to trigger template processing. + // It's safe to return at this point. + m.processedContent = string(content) + return nil + } - tmpl, err = tmpl.ParseFiles(m.path) + tmpl, err := template.New(m.name).Funcs(template.FuncMap{"ReplaceChar": ReplaceChar}).Parse(string(content)) if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + + var buffer bytes.Buffer + if err := tmpl.Execute(&buffer, data); err != nil { return err } - err = tmpl.Execute(f, data) - m.processed = err == nil + m.processedContent = buffer.String() + + return nil +} - return err +func (m *manifest) open() (fs.File, error) { + return m.fsys.Open(m.path) } diff --git a/pkg/feature/raw_resources.go b/pkg/feature/raw_resources.go index 46b1fb099e3..73468c78082 100644 --- a/pkg/feature/raw_resources.go +++ b/pkg/feature/raw_resources.go @@ -16,7 +16,6 @@ package feature import ( "context" "fmt" - "os" "regexp" "strings" @@ -33,13 +32,9 @@ const ( YamlSeparator = "(?m)^---[ \t]*$" ) -func (f *Feature) createResourceFromFile(filename string) error { - data, err := os.ReadFile(filename) - if err != nil { - return errors.WithStack(err) - } +func (f *Feature) createResources(resources string) error { splitter := regexp.MustCompile(YamlSeparator) - objectStrings := splitter.Split(string(data), -1) + objectStrings := splitter.Split(resources, -1) for _, str := range objectStrings { if strings.TrimSpace(str) == "" { continue @@ -79,13 +74,9 @@ func (f *Feature) createResourceFromFile(filename string) error { return nil } -func (f *Feature) patchResourceFromFile(filename string) error { - data, err := os.ReadFile(filename) - if err != nil { - return errors.WithStack(err) - } +func (f *Feature) patchResources(resources string) error { splitter := regexp.MustCompile(YamlSeparator) - objectStrings := splitter.Split(string(data), -1) + objectStrings := splitter.Split(resources, -1) for _, str := range objectStrings { if strings.TrimSpace(str) == "" { continue @@ -105,8 +96,8 @@ func (f *Feature) patchResourceFromFile(filename string) error { Resource: strings.ToLower(u.GroupVersionKind().Kind) + "s", } - // Convert the patch from YAML to JSON - patchAsJSON, err := yaml.YAMLToJSON(data) + // Convert the individual resource patch from YAML to JSON + patchAsJSON, err := yaml.YAMLToJSON([]byte(str)) if err != nil { f.Log.Error(err, "error converting yaml to json") diff --git a/pkg/feature/template_loader.go b/pkg/feature/template_loader.go deleted file mode 100644 index 38e28d5b505..00000000000 --- a/pkg/feature/template_loader.go +++ /dev/null @@ -1,39 +0,0 @@ -package feature - -import ( - "embed" - "io/fs" - "os" - "path/filepath" -) - -//go:embed templates -var embeddedFiles embed.FS - -// CopyEmbeddedFiles ensures that files embedded using go:embed are populated -// to dest directory. In order to process the templates, we need to create a tmp directory -// to store the files. This is because embedded files are read only. -func CopyEmbeddedFiles(src, dest string) error { - return fs.WalkDir(embeddedFiles, src, func(path string, dir fs.DirEntry, err error) error { - if err != nil { - return err - } - - destPath := filepath.Join(dest, path) - if dir.IsDir() { - if err := os.MkdirAll(destPath, 0755); err != nil { - return err - } - } else { - data, err := fs.ReadFile(embeddedFiles, path) - if err != nil { - return err - } - if err := os.WriteFile(destPath, data, 0600); err != nil { - return err - } - } - - return nil - }) -} diff --git a/pkg/feature/templates/serverless/serving-istio-gateways/istio-local-gateway.tmpl b/pkg/feature/templates/serverless/serving-istio-gateways/istio-local-gateway.yaml similarity index 100% rename from pkg/feature/templates/serverless/serving-istio-gateways/istio-local-gateway.tmpl rename to pkg/feature/templates/serverless/serving-istio-gateways/istio-local-gateway.yaml diff --git a/tests/integration/features/crd/istio-gateway.crd.yaml b/tests/integration/features/crd/istio-gateway.crd.yaml new file mode 100644 index 00000000000..81d932cd834 --- /dev/null +++ b/tests/integration/features/crd/istio-gateway.crd.yaml @@ -0,0 +1,313 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + "helm.sh/resource-policy": keep + labels: + app: istio-pilot + chart: istio + heritage: Tiller + release: istio + name: gateways.networking.istio.io +spec: + group: networking.istio.io + names: + categories: + - istio-io + - networking-istio-io + kind: Gateway + listKind: GatewayList + plural: gateways + shortNames: + - gw + singular: gateway + scope: Namespaced + versions: + - name: v1alpha3 + schema: + openAPIV3Schema: + properties: + spec: + description: 'Configuration affecting edge load balancer. See more details + at: https://istio.io/docs/reference/config/networking/gateway.html' + properties: + selector: + additionalProperties: + type: string + description: One or more labels that indicate a specific set of pods/VMs + on which this gateway configuration should be applied. + type: object + servers: + description: A list of server specifications. + items: + properties: + bind: + description: The ip or the Unix domain socket to which the listener + should be bound to. + type: string + defaultEndpoint: + type: string + hosts: + description: One or more hosts exposed by this gateway. + items: + type: string + type: array + name: + description: An optional name of the server, when set must be + unique across all servers. + type: string + port: + description: The Port on which the proxy should listen for incoming + connections. + properties: + name: + description: Label assigned to the port. + type: string + number: + description: A valid non-negative integer port number. + type: integer + protocol: + description: The protocol exposed on the port. + type: string + targetPort: + type: integer + required: + - number + - protocol + - name + type: object + tls: + description: Set of TLS related options that govern the server's + behavior. + properties: + caCertificates: + description: REQUIRED if mode is `MUTUAL` or `OPTIONAL_MUTUAL`. + type: string + cipherSuites: + description: 'Optional: If specified, only support the specified + cipher list.' + items: + type: string + type: array + credentialName: + description: For gateways running on Kubernetes, the name + of the secret that holds the TLS certs including the CA + certificates. + type: string + httpsRedirect: + description: If set to true, the load balancer will send + a 301 redirect for all http connections, asking the clients + to use HTTPS. + type: boolean + maxProtocolVersion: + description: 'Optional: Maximum TLS protocol version.' + enum: + - TLS_AUTO + - TLSV1_0 + - TLSV1_1 + - TLSV1_2 + - TLSV1_3 + type: string + minProtocolVersion: + description: 'Optional: Minimum TLS protocol version.' + enum: + - TLS_AUTO + - TLSV1_0 + - TLSV1_1 + - TLSV1_2 + - TLSV1_3 + type: string + mode: + description: 'Optional: Indicates whether connections to + this port should be secured using TLS.' + enum: + - PASSTHROUGH + - SIMPLE + - MUTUAL + - AUTO_PASSTHROUGH + - ISTIO_MUTUAL + - OPTIONAL_MUTUAL + type: string + privateKey: + description: REQUIRED if mode is `SIMPLE` or `MUTUAL`. + type: string + serverCertificate: + description: REQUIRED if mode is `SIMPLE` or `MUTUAL`. + type: string + subjectAltNames: + description: A list of alternate names to verify the subject + identity in the certificate presented by the client. + items: + type: string + type: array + verifyCertificateHash: + description: An optional list of hex-encoded SHA-256 hashes + of the authorized client certificates. + items: + type: string + type: array + verifyCertificateSpki: + description: An optional list of base64-encoded SHA-256 + hashes of the SPKIs of authorized client certificates. + items: + type: string + type: array + type: object + required: + - port + - hosts + type: object + type: array + type: object + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: true + subresources: + status: {} + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + spec: + description: 'Configuration affecting edge load balancer. See more details + at: https://istio.io/docs/reference/config/networking/gateway.html' + properties: + selector: + additionalProperties: + type: string + description: One or more labels that indicate a specific set of pods/VMs + on which this gateway configuration should be applied. + type: object + servers: + description: A list of server specifications. + items: + properties: + bind: + description: The ip or the Unix domain socket to which the listener + should be bound to. + type: string + defaultEndpoint: + type: string + hosts: + description: One or more hosts exposed by this gateway. + items: + type: string + type: array + name: + description: An optional name of the server, when set must be + unique across all servers. + type: string + port: + description: The Port on which the proxy should listen for incoming + connections. + properties: + name: + description: Label assigned to the port. + type: string + number: + description: A valid non-negative integer port number. + type: integer + protocol: + description: The protocol exposed on the port. + type: string + targetPort: + type: integer + required: + - number + - protocol + - name + type: object + tls: + description: Set of TLS related options that govern the server's + behavior. + properties: + caCertificates: + description: REQUIRED if mode is `MUTUAL` or `OPTIONAL_MUTUAL`. + type: string + cipherSuites: + description: 'Optional: If specified, only support the specified + cipher list.' + items: + type: string + type: array + credentialName: + description: For gateways running on Kubernetes, the name + of the secret that holds the TLS certs including the CA + certificates. + type: string + httpsRedirect: + description: If set to true, the load balancer will send + a 301 redirect for all http connections, asking the clients + to use HTTPS. + type: boolean + maxProtocolVersion: + description: 'Optional: Maximum TLS protocol version.' + enum: + - TLS_AUTO + - TLSV1_0 + - TLSV1_1 + - TLSV1_2 + - TLSV1_3 + type: string + minProtocolVersion: + description: 'Optional: Minimum TLS protocol version.' + enum: + - TLS_AUTO + - TLSV1_0 + - TLSV1_1 + - TLSV1_2 + - TLSV1_3 + type: string + mode: + description: 'Optional: Indicates whether connections to + this port should be secured using TLS.' + enum: + - PASSTHROUGH + - SIMPLE + - MUTUAL + - AUTO_PASSTHROUGH + - ISTIO_MUTUAL + - OPTIONAL_MUTUAL + type: string + privateKey: + description: REQUIRED if mode is `SIMPLE` or `MUTUAL`. + type: string + serverCertificate: + description: REQUIRED if mode is `SIMPLE` or `MUTUAL`. + type: string + subjectAltNames: + description: A list of alternate names to verify the subject + identity in the certificate presented by the client. + items: + type: string + type: array + verifyCertificateHash: + description: An optional list of hex-encoded SHA-256 hashes + of the authorized client certificates. + items: + type: string + type: array + verifyCertificateSpki: + description: An optional list of base64-encoded SHA-256 + hashes of the SPKIs of authorized client certificates. + items: + type: string + type: array + type: object + required: + - port + - hosts + type: object + type: array + type: object + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} \ No newline at end of file diff --git a/tests/integration/features/features_int_test.go b/tests/integration/features/features_int_test.go index cd8a84e3fcc..be2edd42040 100644 --- a/tests/integration/features/features_int_test.go +++ b/tests/integration/features/features_int_test.go @@ -2,6 +2,10 @@ package features_test import ( "context" + "embed" + "os" + "path" + "path/filepath" "time" v1 "k8s.io/api/core/v1" @@ -17,9 +21,13 @@ import ( . "github.com/onsi/gomega" ) +//go:embed templates +var testEmbeddedFiles embed.FS + const ( - timeout = 5 * time.Second - interval = 250 * time.Millisecond + timeout = 5 * time.Second + interval = 250 * time.Millisecond + templatesDir = "templates" ) var _ = Describe("preconditions", func() { @@ -127,6 +135,119 @@ var _ = Describe("preconditions", func() { }) +var _ = Describe("Manifest sources", func() { + Context("using various manifest sources", func() { + + var ( + objectCleaner *envtestutil.Cleaner + dsciSpec *dscv1.DSCInitializationSpec + namespace = "default" + ) + + BeforeEach(func() { + objectCleaner = envtestutil.CreateCleaner(envTestClient, envTest.Config, timeout, interval) + dsciSpec = newDSCInitializationSpec(namespace) + }) + + It("should be able to process an embedded template from the default location", func() { + // given + ns := createNamespace("service-ns") + Expect(envTestClient.Create(context.Background(), ns)).To(Succeed()) + defer objectCleaner.DeleteAll(ns) + + serviceMeshSpec := &dsciSpec.ServiceMesh + serviceMeshSpec.ControlPlane.Namespace = "service-ns" + + createService, err := feature.CreateFeature("create-control-plane"). + For(dsciSpec). + Manifests(path.Join(templatesDir, "serverless", "serving-istio-gateways", "local-gateway-svc.tmpl")). + UsingConfig(envTest.Config). + Load() + + Expect(err).ToNot(HaveOccurred()) + + // when + Expect(createService.Apply()).To(Succeed()) + + // then + service, err := getService("knative-local-gateway", "service-ns") + Expect(err).ToNot(HaveOccurred()) + Expect(service.Name).To(Equal("knative-local-gateway")) + }) + + It("should be able to process an embedded YAML file from the default location", func() { + // given + ns := createNamespace("knative-serving") + Expect(envTestClient.Create(context.Background(), ns)).To(Succeed()) + defer objectCleaner.DeleteAll(ns) + + createGateway, err := feature.CreateFeature("create-gateway"). + For(dsciSpec). + Manifests(path.Join(templatesDir, "serverless", "serving-istio-gateways", "istio-local-gateway.yaml")). + UsingConfig(envTest.Config). + Load() + + Expect(err).ToNot(HaveOccurred()) + + // when + Expect(createGateway.Apply()).To(Succeed()) + + // then + gateway, err := getGateway(envTest.Config, "knative-serving", "knative-local-gateway") + Expect(err).ToNot(HaveOccurred()) + Expect(gateway).ToNot(BeNil()) + }) + + It("should be able to process an embedded file from a non default location", func() { + createNs, err := feature.CreateFeature("create-ns"). + For(dsciSpec). + ManifestSource(testEmbeddedFiles). + Manifests(path.Join(templatesDir, "namespace.yaml")). + UsingConfig(envTest.Config). + Load() + + Expect(err).ToNot(HaveOccurred()) + + // when + Expect(createNs.Apply()).To(Succeed()) + + // then + namespace, err := getNamespace("embedded-test-ns") + Expect(err).ToNot(HaveOccurred()) + Expect(namespace.Name).To(Equal("embedded-test-ns")) + }) + + It("should source manifests from a specified temporary directory within the file system", func() { + // given + tempDir := GinkgoT().TempDir() + yamlData := `apiVersion: v1 +kind: Namespace +metadata: + name: real-file-test-ns` + + err := createFile(tempDir, "namespace.yaml", yamlData) + Expect(err).ToNot(HaveOccurred()) + + createNs, err := feature.CreateFeature("create-ns"). + For(dsciSpec). + ManifestSource(os.DirFS(tempDir)). + Manifests(path.Join("namespace.yaml")). // must be relative to root DirFS defined above + UsingConfig(envTest.Config). + Load() + + Expect(err).ToNot(HaveOccurred()) + + // when + Expect(createNs.Apply()).To(Succeed()) + + // then + namespace, err := getNamespace("real-file-test-ns") + Expect(err).ToNot(HaveOccurred()) + Expect(namespace.Name).To(Equal("real-file-test-ns")) + }) + }) +}) + func createNamespace(name string) *v1.Namespace { return &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -148,3 +269,26 @@ func getNamespace(namespace string) (*v1.Namespace, error) { return ns, err } + +func getService(name, namespace string) (*v1.Service, error) { + svc := &v1.Service{} + err := envTestClient.Get(context.Background(), types.NamespacedName{ + Name: name, Namespace: namespace, + }, svc) + + return svc, err +} + +func createFile(dir, filename, data string) error { + filePath := filepath.Join(dir, filename) + file, err := os.Create(filePath) + if err != nil { + return err + } + + _, err = file.WriteString(data) + if err != nil { + return err + } + return file.Sync() +} diff --git a/tests/integration/features/servicemesh_feature_test.go b/tests/integration/features/servicemesh_feature_test.go index 3f8c79ba3a7..a9615388207 100644 --- a/tests/integration/features/servicemesh_feature_test.go +++ b/tests/integration/features/servicemesh_feature_test.go @@ -6,6 +6,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" @@ -98,6 +99,24 @@ var _ = Describe("Service Mesh feature", func() { }) +func getGateway(cfg *rest.Config, namespace, name string) (*unstructured.Unstructured, error) { + dynamicClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, err + } + gwGvr := schema.GroupVersionResource{ + Group: "networking.istio.io", + Version: "v1beta1", + Resource: "gateways", + } + + gateway, err := dynamicClient.Resource(gwGvr).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return gateway, nil +} + func installServiceMeshControlPlaneCRD() *apiextensionsv1.CustomResourceDefinition { // Create SMCP the CRD smcpCrdObj := &apiextensionsv1.CustomResourceDefinition{} diff --git a/tests/integration/features/templates/namespace.yaml b/tests/integration/features/templates/namespace.yaml new file mode 100644 index 00000000000..6e6e5ab44cb --- /dev/null +++ b/tests/integration/features/templates/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: embedded-test-ns