Skip to content

Commit

Permalink
feat: support k8s crd in kusion apply and kusion destroy
Browse files Browse the repository at this point in the history
  • Loading branch information
howieyuen committed Jul 25, 2022
1 parent 3299afd commit 9cf1646
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 0 deletions.
56 changes: 56 additions & 0 deletions pkg/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"

Expand All @@ -19,8 +20,10 @@ import (
"kusionstack.io/kusion/pkg/engine/models"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/projectstack"
"kusionstack.io/kusion/pkg/resources/crd"
jsonUtil "kusionstack.io/kusion/pkg/util/json"
"kusionstack.io/kusion/pkg/util/pretty"
"kusionstack.io/kusion/pkg/util/yaml"
)

var enableRest bool
Expand Down Expand Up @@ -51,6 +54,14 @@ func CompileWithSpinner(workDir string, filenames, settings, arguments, override
if err != nil {
return nil, sp, err
}

// append crd description to compiled result
// workDir may omit empty if run in stack dir
err = appendCRDs(stack.Path, r)
if err != nil {
return nil, sp, err
}

// Construct resource from compile result to build request
resources, err := engine.ConvertKCLResult2Resources(r.Documents)
if err != nil {
Expand All @@ -60,6 +71,51 @@ func CompileWithSpinner(workDir string, filenames, settings, arguments, override
return resources, sp, nil
}

func appendCRDs(workDir string, r *CompileResult) error {
if r == nil {
return nil
}

crdObjs, err := readCRDs(workDir)
if err != nil {
return err
}
if crdObjs != nil && len(crdObjs) != 0 {
// append to Documents
for _, obj := range crdObjs {
doc, flag := obj.(map[string]interface{})
if !flag {
continue
}
r.Documents = append(r.Documents, doc)
}

// update RawYAMLResult
var items = make([]interface{}, len(r.Documents))
for i, doc := range r.Documents {
items[i] = doc
}
r.RawYAMLResult = yaml.MergeToOneYAML(items...)
}

return nil
}

func readCRDs(workDir string) ([]interface{}, error) {
projectPath := path.Dir(workDir)
crdPath := path.Join(projectPath, crd.Directory)
_, err := os.Stat(crdPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}

visitor := crd.NewVisitor(crdPath)
return visitor.Visit()
}

// Compile General KCL compilation method
func Compile(workDir string, filenames, settings, arguments, overrides []string, disableNone bool, overrideAST bool) (*CompileResult, error) {
optList, err := buildOptions(workDir, settings, arguments, overrides, disableNone, overrideAST)
Expand Down
31 changes: 31 additions & 0 deletions pkg/compile/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,34 @@ func mockRunFiles(mockErr error) {
// return &CompileResult{}, nil
// })
// }

func Test_appendCRDs(t *testing.T) {
t.Run("append one CRD", func(t *testing.T) {
cs := &CompileResult{}
err := appendCRDs("./testdata/crd", cs)
assert.Nil(t, err)
assert.NotNil(t, cs.Documents)
assert.NotEmpty(t, cs.RawYAMLResult)
})

t.Run("no CRD to append", func(t *testing.T) {
cs := &CompileResult{}
err := appendCRDs("./testdata", cs)
assert.Nil(t, err)
assert.Nil(t, cs.Documents)
assert.Empty(t, cs.RawYAMLResult)
})
}

func Test_readCRDsIfExists(t *testing.T) {
t.Run("read CRDs", func(t *testing.T) {
crds, err := readCRDs("./testdata/crd")
assert.Nil(t, err)
assert.NotNil(t, crds)
})
t.Run("no CRDs", func(t *testing.T) {
crds, err := readCRDs("./testdata")
assert.Nil(t, err)
assert.Nil(t, crds)
})
}
37 changes: 37 additions & 0 deletions pkg/compile/testdata/crd/foo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: foos.samplecontroller.k8s.io
# for more information on the below annotation, please see
# https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/2337-k8s.io-group-protection/README.md
annotations:
"api-approved.kubernetes.io": "unapproved, experimental-only; please get an approval from Kubernetes API reviewers if you're trying to develop a CRD in the *.k8s.io or *.kubernetes.io groups"
spec:
group: samplecontroller.k8s.io
versions:
- name: v1alpha1
served: true
storage: true
schema:
# schema used for validation
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
deploymentName:
type: string
replicas:
type: integer
minimum: 1
maximum: 10
status:
type: object
properties:
availableReplicas:
type: integer
names:
kind: Foo
plural: foos
scope: Namespaced
76 changes: 76 additions & 0 deletions pkg/resources/crd/crd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package crd

import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

"k8s.io/apimachinery/pkg/util/yaml"

"kusionstack.io/kusion/pkg/resources"
)

const (
Directory = "crd"
)

var FileExtensions = []string{".yaml", ".yml", ".json"}

type crdVisitor struct {
Path string
}

func NewVisitor(path string) resources.Visitor {
return &crdVisitor{Path: path}
}

// Visit read all YAML files under target path
func (v *crdVisitor) Visit() (objs []interface{}, err error) {
err = filepath.WalkDir(v.Path, func(filePath string, d os.DirEntry, err error) error {
if err != nil {
return err
}

// check file extension
if ignoreFile(filePath, FileExtensions) {
return nil
}

f, err := os.Open(filePath)
if err != nil {
return err
}
decoder := yaml.NewYAMLOrJSONDecoder(f, 4096)
for {
data := make(map[string]interface{})
if err := decoder.Decode(&data); err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("error parsing %s: %v", filePath, err)
}
if len(data) == 0 {
continue
}

objs = append(objs, data)
}
})
return objs, err
}

// ignoreFile indicates a filename is ended with specified extension or not
func ignoreFile(path string, extensions []string) bool {
if len(extensions) == 0 {
return false
}
ext := filepath.Ext(path)
for _, s := range extensions {
if strings.EqualFold(s, ext) {
return false
}
}
return true
}
49 changes: 49 additions & 0 deletions pkg/resources/crd/crd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package crd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCrdVisitor(t *testing.T) {
t.Run("read single file", func(t *testing.T) {
visitor := crdVisitor{
Path: "./testdata/one.yaml",
}
objs, err := visitor.Visit()
assert.Nil(t, err)
assert.Equal(t, 1, len(objs))
})

t.Run("read multi files", func(t *testing.T) {
visitor := crdVisitor{
Path: "./testdata",
}
objs, err := visitor.Visit()
assert.Nil(t, err)
assert.Equal(t, 3, len(objs))
})
}

func Test_ignoreFile(t *testing.T) {
t.Run("not ignore .YAML file", func(t *testing.T) {
flag := ignoreFile("foo.YAML", FileExtensions)
assert.False(t, flag)
})

t.Run("not ignore .yaml file", func(t *testing.T) {
flag := ignoreFile("foo.yaml", FileExtensions)
assert.False(t, flag)
})

t.Run("not ignore .yml file", func(t *testing.T) {
flag := ignoreFile("foo.yml", FileExtensions)
assert.False(t, flag)
})

t.Run("ignore .go file", func(t *testing.T) {
flag := ignoreFile("bar.go", FileExtensions)
assert.True(t, flag)
})
}
66 changes: 66 additions & 0 deletions pkg/resources/crd/testdata/multi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: foos.samplecontroller.k8s.io
# for more information on the below annotation, please see
# https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/2337-k8s.io-group-protection/README.md
annotations:
"api-approved.kubernetes.io": "unapproved, experimental-only; please get an approval from Kubernetes API reviewers if you're trying to develop a CRD in the *.k8s.io or *.kubernetes.io groups"
spec:
group: samplecontroller.k8s.io
versions:
- name: v1alpha1
served: true
storage: true
schema:
# schema used for validation
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
deploymentName:
type: string
replicas:
type: integer
minimum: 1
maximum: 10
status:
type: object
properties:
availableReplicas:
type: integer
names:
kind: Foo
plural: foos
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: guestbooks.apps.ibm.com
spec:
group: apps.ibm.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
guestbookTitle:
type: string
guestbookSubtitle:
type: string
scope: Namespaced
names:
plural: guestbooks
singular: guestbook
kind: Guestbook
shortNames:
- gb
37 changes: 37 additions & 0 deletions pkg/resources/crd/testdata/one.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: foos.samplecontroller.k8s.io
# for more information on the below annotation, please see
# https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/2337-k8s.io-group-protection/README.md
annotations:
"api-approved.kubernetes.io": "unapproved, experimental-only; please get an approval from Kubernetes API reviewers if you're trying to develop a CRD in the *.k8s.io or *.kubernetes.io groups"
spec:
group: samplecontroller.k8s.io
versions:
- name: v1alpha1
served: true
storage: true
schema:
# schema used for validation
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
deploymentName:
type: string
replicas:
type: integer
minimum: 1
maximum: 10
status:
type: object
properties:
availableReplicas:
type: integer
names:
kind: Foo
plural: foos
scope: Namespaced
6 changes: 6 additions & 0 deletions pkg/resources/visitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package resources

// Visitor walks a list of resources under target path.
type Visitor interface {
Visit() ([]interface{}, error)
}

0 comments on commit 9cf1646

Please sign in to comment.