Skip to content

Commit

Permalink
Create yaml parser and update k8s client.
Browse files Browse the repository at this point in the history
  • Loading branch information
sawsa307 committed Aug 11, 2023
1 parent 34458a9 commit 900d93c
Show file tree
Hide file tree
Showing 3 changed files with 351 additions and 0 deletions.
105 changes: 105 additions & 0 deletions test/utils/crud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
)

type K8sCRUD struct {
c ctrlClient.Client
}

func NewK8sCRUD(c ctrlClient.Client) *K8sCRUD {
return &K8sCRUD{c: c}
}

func (crud *K8sCRUD) GetClient() ctrlClient.Client {
return crud.c
}

// CreateK8sResources creates all kubernetes resources within the given list.
func (crud *K8sCRUD) CreateK8sResources(ctx context.Context, objects ...ctrlClient.Object) (map[schema.GroupVersionKind][]ctrlClient.ObjectKey, error) {
klog.Info("Creating K8s resources.")

objectKeys := make(map[schema.GroupVersionKind][]ctrlClient.ObjectKey)
for _, obj := range objects {
gvk, err := crud.create(ctx, obj)
if err != nil {
return nil, err
}
objectKeys[gvk] = append(objectKeys[gvk], ctrlClient.ObjectKeyFromObject(obj))
}
return objectKeys, nil
}

// Create saves the object obj in the Kubernetes cluster.
func (crud *K8sCRUD) create(ctx context.Context, obj ctrlClient.Object) (schema.GroupVersionKind, error) {
gvk := obj.GetObjectKind().GroupVersionKind()
if err := crud.GetClient().Create(ctx, obj); err != nil {
return gvk, fmt.Errorf("failed to create %s %s/%s: %w", gvk, obj.GetNamespace(), obj.GetName(), err)
}
klog.Infof("Created %s %s/%s", gvk, obj.GetNamespace(), obj.GetName())
return gvk, nil
}

// DeleteK8sResources deletes all kubernetes resources within the given list.
func (crud *K8sCRUD) DeleteK8sResources(ctx context.Context, objects ...ctrlClient.Object) error {
klog.Info("Deleting K8s resources.")

for _, obj := range objects {
if err := crud.delete(ctx, obj); err != nil {
return err
}
}
return nil
}

// Delete deletes the given obj from Kubernetes cluster.
func (crud *K8sCRUD) delete(ctx context.Context, obj ctrlClient.Object) error {
gvk := obj.GetObjectKind().GroupVersionKind()
if err := crud.GetClient().Delete(ctx, obj); err != nil {
return fmt.Errorf("failed to delete %s %s/%s: %w", gvk.String(), obj.GetNamespace(), obj.GetName(), err)
}
klog.Infof("Deleted %s %s/%s", gvk, obj.GetNamespace(), obj.GetName())
return nil
}

// ReplaceNamespace makes a copy of the given resources, and return a new list of resources with namespace.
// Namespace won't be added for resources without namespace.
func (crud *K8sCRUD) ReplaceNamespace(namespace string, objects ...ctrlClient.Object) ([]ctrlClient.Object, error) {
klog.Info("Replace K8s resources to use namespace %s.", namespace)

var namespacedObject []ctrlClient.Object
for _, obj := range objects {
// Make a copy so we don't modify the original object.
obj = obj.DeepCopyObject().(ctrlClient.Object)

isNamespaced, err := crud.GetClient().IsObjectNamespaced(obj)
if err != nil {
return nil, err
}
if isNamespaced {
obj.SetNamespace(namespace)
}
namespacedObject = append(namespacedObject, obj)
}
return namespacedObject, nil
}
63 changes: 63 additions & 0 deletions test/utils/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"fmt"
"os"
"regexp"
"strings"

"k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
)

// ParseK8sYaml takes a yaml file path to create a list of runtime objects.
func ParseK8sYamlFile(filePath string) ([]ctrlClient.Object, error) {
klog.Infof("Parse K8s resources from path %s.", filePath)

yamlText, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
return ParseK8sYAML(string(yamlText))
}

// ParseK8sYaml converts a yaml text to a list of runtime objects.
func ParseK8sYAML(yamlText string) ([]ctrlClient.Object, error) {
sepYamlfiles := regexp.MustCompile("\n---\n").Split(string(yamlText), -1)
retVal := make([]ctrlClient.Object, 0, len(sepYamlfiles))
for _, f := range sepYamlfiles {
f = strings.TrimSpace(f)
if f == "\n" || f == "" {
// ignore empty cases
continue
}

decode := scheme.Codecs.UniversalDeserializer().Decode
runtimeObj, _, err := decode([]byte(f), nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to decode YAML text: %w", err)
}

clientObj, ok := runtimeObj.(ctrlClient.Object)
if !ok {
return nil, fmt.Errorf("cast failed: want Object, got %T", runtimeObj)
}
retVal = append(retVal, clientObj)
}
return retVal, nil
}
183 changes: 183 additions & 0 deletions test/utils/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"reflect"
"testing"

"github.com/kr/pretty"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"

ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
)

func TestParseK8sYAML(t *testing.T) {
headerText := "# Copyright 2023 Google LLC\n" +
"#\n" +
"# Licensed under the Apache License, Version 2.0 (the \"License\");" +
"# you may not use this file except in compliance with the License." +
"# You may obtain a copy of the License at" +
"#\n" +
"# https://www.apache.org/licenses/LICENSE-2.0" +
"#\n" +
"# Unless required by applicable law or agreed to in writing, software" +
"# distributed under the License is distributed on an \"AS IS\" BASIS," +
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
"# See the License for the specific language governing permissions and" +
"# limitations under the License." +
"\n"

ingText := "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: foo-internal\n " +
"annotations:\n kubernetes.io/ingress.class: 'gce-internal'\n" +
"spec:\n rules:\n - http:\n paths:\n - path: /foo\n pathType: Prefix\n " +
"backend:\n service:\n name: foo\n port:\n number: 80"
prefix := networkingv1.PathTypePrefix
ing := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-internal",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "gce-internal",
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Ingress",
APIVersion: "networking.k8s.io/v1",
},
Spec: networkingv1.IngressSpec{
Rules: []networkingv1.IngressRule{
{
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/foo",
PathType: &prefix,
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "foo",
Port: networkingv1.ServiceBackendPort{
Number: int32(80),
},
},
},
},
},
},
},
},
},
},
}

svcText := "apiVersion: v1\nkind: Service\nmetadata:\n name: bar\n " +
"annotations:\n cloud.google.com/neg: '{\"ingress\": true}'\n" +
"spec:\n ports:\n - port: 80\n targetPort: 8080\n name: http\n selector:\n app: bar\n type: ClusterIP"
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Annotations: map[string]string{
"cloud.google.com/neg": "{\"ingress\": true}",
},
},
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Name: "http",
Port: 80,
TargetPort: intstr.FromInt(8080),
},
},
Selector: map[string]string{"app": "bar"},
Type: v1.ServiceTypeClusterIP,
},
}

testCases := []struct {
desc string
yamlText string
expectObjects []ctrlClient.Object
expectNil bool
}{
{
desc: "YAML TEXT contains headers and one properly formed object. Comments should be ignored.",
yamlText: headerText + svcText,
expectObjects: []ctrlClient.Object{svc},
expectNil: true,
},
{
desc: "Properly formed yaml, with one k8s object",
yamlText: svcText,
expectObjects: []ctrlClient.Object{svc},
expectNil: true,
},
{
desc: "Properly formed yaml, with multiple k8s objects",
yamlText: svcText + "\n---\n" + ingText,
expectObjects: []ctrlClient.Object{svc, ing},
expectNil: true,
},
{
desc: "Properly formed yaml, contains invalid k8s object",
yamlText: svcText + "\n---\n" + "apiVersion: networking.k8s.io/v1\n", // Object missing kind
expectObjects: nil,
expectNil: false,
},
{
desc: "Empty text",
yamlText: "",
expectObjects: nil,
expectNil: true,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
gotObjects, err := ParseK8sYAML(tc.yamlText)

if tc.expectNil && (err != nil) {
t.Fatalf("Expect error to be nil, got %v", err)
}
if !tc.expectNil && (err == nil) {
t.Fatal("Expect error to be NOT nil, got nil")
}

// Compare if we have the same set of objects.
if len(tc.expectObjects) != len(gotObjects) {
t.Fatalf("Expect %d objects, got %d", len(tc.expectObjects), len(gotObjects))
}
var match int
for _, gotObj := range gotObjects {
for _, expectObj := range tc.expectObjects {
if reflect.DeepEqual(gotObj, expectObj) {
match += 1
}
}
}
if len(tc.expectObjects) != match {
t.Fatalf("Expect %d matching objects, got %d. Expect objects: %v, gotObjects: %v.", len(tc.expectObjects), match, pretty.Sprint(tc.expectObjects), pretty.Sprint(gotObjects))
}
})

}
}

0 comments on commit 900d93c

Please sign in to comment.