Skip to content

Commit

Permalink
feat(templates): process Feature templates in memory (opendatahub-io#797
Browse files Browse the repository at this point in the history
)

Co-authored-by: Bartosz Majsak <bartosz.majsak@gmail.com>
Co-authored-by: Wen Zhou <wenzhou@redhat.com>
  • Loading branch information
3 people committed Jan 23, 2024
1 parent 515328c commit 0e9c4ed
Show file tree
Hide file tree
Showing 12 changed files with 579 additions and 114 deletions.
10 changes: 2 additions & 8 deletions components/kserve/serverless_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 3 additions & 9 deletions controllers/dscinitialization/servicemesh_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dscinitialization

import (
"path"
"path/filepath"

operatorv1 "github.com/openshift/api/operator/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -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)

Expand All @@ -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,
Expand All @@ -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 {
Expand Down
17 changes: 15 additions & 2 deletions pkg/feature/builder.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
22 changes: 12 additions & 10 deletions pkg/feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ type Feature struct {
DynamicClient dynamic.Interface
Client client.Client

manifests []manifest
manifests []manifest

cleanups []Action
resources []Action
preconditions []Action
Expand Down Expand Up @@ -81,16 +82,16 @@ 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)
}
}

// 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)
}
}
Expand All @@ -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))
}
Expand Down Expand Up @@ -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
Expand Down
88 changes: 59 additions & 29 deletions pkg/feature/manifest.go
Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 6 additions & 15 deletions pkg/feature/raw_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package feature
import (
"context"
"fmt"
"os"
"regexp"
"strings"

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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")

Expand Down
Loading

0 comments on commit 0e9c4ed

Please sign in to comment.