Skip to content

Commit

Permalink
Merge branch 'master' into ks20drptcrtype
Browse files Browse the repository at this point in the history
  • Loading branch information
mfrancisc authored Mar 7, 2024
2 parents f637615 + fb92e44 commit f1d3446
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 0 deletions.
84 changes: 84 additions & 0 deletions pkg/template/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package template

import (
"bytes"
"embed"
"io"
"io/fs"
"text/template"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
)

// Variables contains all the available variables that are supported by the templates
type Variables struct {
Namespace string
}

// LoadObjectsFromEmbedFS loads all the kubernetes objects from an embedded filesystem and returns a list of Unstructured objects that can be applied in the cluster.
// The function will return all the objects it finds starting from the root of the embedded filesystem.
func LoadObjectsFromEmbedFS(efs *embed.FS, variables *Variables) ([]*unstructured.Unstructured, error) {
var objects []*unstructured.Unstructured
entries, err := getAllTemplateNames(efs)
if err != nil {
return objects, err
}
for _, templatePath := range entries {
templateContent, err := efs.ReadFile(templatePath)
if err != nil {
return objects, err
}
buf, err := replaceTemplateVariables(templatePath, templateContent, variables)
if err != nil {
return objects, err
}
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(buf.Bytes()), 100)
for {
var rawExt runtime.RawExtension
if err := decoder.Decode(&rawExt); err != nil {
if errors.Is(err, io.EOF) {
break
}
return objects, err
}
rawExt.Raw = bytes.TrimSpace(rawExt.Raw)
if len(rawExt.Raw) == 0 || bytes.Equal(rawExt.Raw, []byte("null")) {
continue
}
unstructuredObj := &unstructured.Unstructured{}
_, _, err = scheme.Codecs.UniversalDeserializer().Decode(rawExt.Raw, nil, unstructuredObj)
if err != nil {
return objects, err
}
objects = append(objects, unstructuredObj)
}
}
return objects, nil
}

// replaceTemplateVariables replaces all the variables in the given template and returns a buffer with the evaluated content
func replaceTemplateVariables(templateName string, templateContent []byte, variables *Variables) (bytes.Buffer, error) {
var buf bytes.Buffer
tmpl, err := template.New(templateName).Parse(string(templateContent))
if err != nil {
return buf, err
}
err = tmpl.Execute(&buf, variables)
return buf, err
}

// getAllTemplateNames reads the embedded filesystem and returns a list with all the filenames
func getAllTemplateNames(efs *embed.FS) (files []string, err error) {
err = fs.WalkDir(efs, ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}
files = append(files, path)
return nil
})
return files, err
}
100 changes: 100 additions & 0 deletions pkg/template/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package template_test

import (
"embed"
"testing"

"github.com/codeready-toolchain/toolchain-common/pkg/template"
"github.com/codeready-toolchain/toolchain-common/pkg/test"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)

//go:embed testdata/*
var EFS embed.FS

//go:embed testdata/host/*
var hostFS embed.FS

//go:embed testdata/member/*
var memberFS embed.FS

func TestLoadObjectsFromEmbedFS(t *testing.T) {
t.Run("loads objects recursively from all subdirectories", func(t *testing.T) {
// when
allObjects, err := template.LoadObjectsFromEmbedFS(&EFS, &template.Variables{Namespace: test.HostOperatorNs})
require.NoError(t, err)
hostFolderObjects, err := template.LoadObjectsFromEmbedFS(&hostFS, &template.Variables{Namespace: test.HostOperatorNs})
require.NoError(t, err)
memberFolderObjects, err := template.LoadObjectsFromEmbedFS(&memberFS, nil)
require.NoError(t, err)
// then
require.NotNil(t, allObjects)
require.NotNil(t, hostFolderObjects)
require.NotNil(t, memberFolderObjects)
require.Equal(t, 4, len(allObjects), "invalid number of expected total objects")
require.Equal(t, 3, len(hostFolderObjects), "invalid number of expected objects from host folder")
require.Equal(t, 1, len(memberFolderObjects), "invalid number of expected objects from member folder")
// check match for the expected objects
checkExpectedObjects(t, allObjects)
})

t.Run("error - when variables are not provided", func(t *testing.T) {
// when
// we do not pass required variables for the templates that requires variables
objects, err := template.LoadObjectsFromEmbedFS(&hostFS, nil)
// then
// we should get back an error
require.Error(t, err)
require.Nil(t, objects)
})
}

func checkExpectedObjects(t *testing.T, objects []*unstructured.Unstructured) {
sa := &v1.ServiceAccount{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(objects[0].Object, sa)
require.NoError(t, err)
require.Equal(t, "toolchaincluster-host", sa.GetName())
require.Equal(t, "toolchain-host-operator", sa.GetNamespace())
role := &rbac.Role{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(objects[1].Object, role)
require.NoError(t, err)
require.Equal(t, "toolchaincluster-host", role.GetName())
require.Equal(t, "toolchain-host-operator", role.GetNamespace())
require.Equal(t, []rbac.PolicyRule{
{
APIGroups: []string{"toolchain.dev.openshift.com"},
Resources: []string{"*"},
Verbs: []string{"*"},
},
}, role.Rules)
roleBinding := &rbac.RoleBinding{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(objects[2].Object, roleBinding)
require.NoError(t, err)
require.Equal(t, "toolchaincluster-host", roleBinding.GetName())
require.Equal(t, "toolchain-host-operator", roleBinding.GetNamespace())
require.Equal(t, rbac.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: "toolchaincluster-host",
}, roleBinding.RoleRef)
require.Equal(t, 1, len(roleBinding.Subjects))
require.Equal(t, rbac.Subject{
Kind: "ServiceAccount",
Name: "toolchaincluster-host",
}, roleBinding.Subjects[0])
clusterRole := &rbac.ClusterRole{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(objects[3].Object, clusterRole)
require.NoError(t, err)
require.Equal(t, "member-toolchaincluster-cr", clusterRole.GetName())
require.Equal(t, []rbac.PolicyRule{
{
APIGroups: []string{"authentication.k8s.io"},
Resources: []string{"tokenreviews"},
Verbs: []string{"create"},
},
}, clusterRole.Rules)
}
32 changes: 32 additions & 0 deletions pkg/template/testdata/host/service-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: toolchaincluster-host
namespace: {{.Namespace}}
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: toolchaincluster-host
namespace: {{.Namespace}}
rules:
- apiGroups:
- toolchain.dev.openshift.com
resources:
- "*"
verbs:
- "*"
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: toolchaincluster-host
namespace: {{.Namespace}}
subjects:
- kind: ServiceAccount
name: toolchaincluster-host
roleRef:
kind: Role
name: toolchaincluster-host
apiGroup: rbac.authorization.k8s.io
11 changes: 11 additions & 0 deletions pkg/template/testdata/member/cluster-role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: member-toolchaincluster-cr
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create

0 comments on commit f1d3446

Please sign in to comment.