Skip to content

Commit

Permalink
Merge pull request #17 from pwittrock/test
Browse files Browse the repository at this point in the history
Tests for pkg/resource
  • Loading branch information
k8s-ci-robot authored Jun 25, 2018
2 parents cbb09c1 + 2716561 commit 24a846a
Show file tree
Hide file tree
Showing 31 changed files with 959 additions and 154 deletions.
56 changes: 42 additions & 14 deletions cmd/controller-scaffold/cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ import (
"log"
"os"
"os/exec"

"path/filepath"
"strings"

"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"sigs.k8s.io/controller-tools/pkg/scaffold"
"sigs.k8s.io/controller-tools/pkg/scaffold/controller"
"sigs.k8s.io/controller-tools/pkg/scaffold/input"
"sigs.k8s.io/controller-tools/pkg/scaffold/resource"
)

var r *resource.Resource
var resourceFlag, controllerFlag *flag.Flag
var doResource, doController, doMake bool

// APICmd represents the resource command
var APICmd = &cobra.Command{
Expand Down Expand Up @@ -65,14 +67,18 @@ After the scaffold is written, api will run make on the project.
Run: func(cmd *cobra.Command, args []string) {
DieIfNoProject()

fmt.Println("Create Resource under pkg/apis [y/n]?")
re := yesno()
fmt.Println("Create Controller under pkg/controller [y/n]?")
c := yesno()
if !resourceFlag.Changed {
fmt.Println("Create Resource under pkg/apis [y/n]?")
doResource = yesno()
}
if !controllerFlag.Changed {
fmt.Println("Create Controller under pkg/controller [y/n]?")
doController = yesno()
}

fmt.Println("Writing scaffold for you to edit...")

if re {
if doResource {
fmt.Println(filepath.Join("pkg", "apis", r.Group, r.Version,
fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))))
fmt.Println(filepath.Join("pkg", "apis", r.Group, r.Version,
Expand All @@ -95,7 +101,7 @@ After the scaffold is written, api will run make on the project.
}
}

if c {
if doController {
fmt.Println(filepath.Join("pkg", "controller", strings.ToLower(r.Kind),
fmt.Sprintf("%s_controller.go", strings.ToLower(r.Kind))))
fmt.Println(filepath.Join("pkg", "apis", strings.ToLower(r.Kind),
Expand All @@ -112,19 +118,29 @@ After the scaffold is written, api will run make on the project.
}
}

fmt.Println("Running make...")
cm := exec.Command("make") // #nosec
cm.Stderr = os.Stderr
cm.Stdout = os.Stdout
if err := cm.Run(); err != nil {
log.Fatal(err)
if doMake {
fmt.Println("Running make...")
cm := exec.Command("make") // #nosec
cm.Stderr = os.Stderr
cm.Stdout = os.Stdout
if err := cm.Run(); err != nil {
log.Fatal(err)
}
}
},
}

func init() {
rootCmd.AddCommand(APICmd)
r = resource.ForFlags(APICmd.Flags())
APICmd.Flags().BoolVar(&doMake, "make", true,
"if true, run make after generating files")
APICmd.Flags().BoolVar(&doResource, "resource", true,
"if set, generate the resource without prompting the user")
resourceFlag = APICmd.Flag("resource")
APICmd.Flags().BoolVar(&doController, "controller", true,
"if set, generate the controller without prompting the user")
controllerFlag = APICmd.Flag("controller")
r = ResourceForFlags(APICmd.Flags())
}

// DieIfNoProject checks to make sure the command is run from a directory containing a project file.
Expand All @@ -133,3 +149,15 @@ func DieIfNoProject() {
log.Fatalf("Command must be run from a diretory containing %s", "PROJECT")
}
}

// ResourceForFlags registers flags for Resource fields and returns the Resource
func ResourceForFlags(f *flag.FlagSet) *resource.Resource {
r := &resource.Resource{}
f.StringVar(&r.Kind, "kind", "", "resource Kind")
f.StringVar(&r.Group, "group", "", "resource Group")
f.StringVar(&r.Version, "version", "", "resource Version")
f.BoolVar(&r.Namespaced, "namespaced", true, "true if the resource is namespaced")
f.BoolVar(&r.CreateExampleReconcileBody, "example", true,
"true if an example reconcile body should be written")
return r
}
13 changes: 11 additions & 2 deletions cmd/controller-scaffold/cmd/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ var bp *project.Boilerplate
var gopkg *project.GopkgToml
var mrg *manager.Cmd
var dkr *manager.Dockerfile
var dep bool
var depFlag *flag.Flag

// ProjectCmd represents the project command
var ProjectCmd = &cobra.Command{
Expand Down Expand Up @@ -75,8 +77,11 @@ controller-scaffold project --domain k8s.io --license apache2 --owner "The Kuber
log.Fatal(err)
}

fmt.Println("Run `dep ensure` to fetch dependencies (Recommended) [y/n]?")
if yesno() {
if !depFlag.Changed {
fmt.Println("Run `dep ensure` to fetch dependencies (Recommended) [y/n]?")
dep = yesno()
}
if dep {
c := exec.Command("dep", "ensure") // #nosec
c.Stderr = os.Stderr
c.Stdout = os.Stdout
Expand All @@ -102,6 +107,10 @@ controller-scaffold project --domain k8s.io --license apache2 --owner "The Kuber
func init() {
rootCmd.AddCommand(ProjectCmd)

ProjectCmd.Flags().BoolVar(
&dep, "dep", true, "if specified, determines whether dep will be used.")
depFlag = ProjectCmd.Flag("dep")

prj = ProjectForFlags(ProjectCmd.Flags())
bp = BoilerplateForFlags(ProjectCmd.Flags())
gopkg = &project.GopkgToml{}
Expand Down
18 changes: 17 additions & 1 deletion pkg/scaffold/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ var controllerTemplate = `{{ .Boilerplate }}
package {{ lower .Resource.Kind }}
import (
{{ if .Resource.CreateExampleReconcileBody -}}
"context"
"log"
"reflect"
Expand All @@ -99,6 +100,19 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
{{ .Resource.Group}}{{ .Resource.Version }} "{{ .ResourcePackage }}/{{ .Resource.Group}}/{{ .Resource.Version }}"
{{ else -}}
"context"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
{{ .Resource.Group}}{{ .Resource.Version }} "{{ .ResourcePackage }}/{{ .Resource.Group}}/{{ .Resource.Version }}"
{{ end -}}
)
/**
Expand Down Expand Up @@ -168,12 +182,13 @@ func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (re
return reconcile.Result{}, err
}
{{ if .Resource.CreateExampleReconcileBody -}}
// TODO(user): Change this to be the object type created by your controller
// Define the desired Deployment object
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-deployment",
Namespace: instance.Namespace,
Namespace: {{ if .Resource.Namespaced}}instance.Namespace{{ else }}"default"{{ end }},
OwnerReferences: []metav1.OwnerReference{
// TODO(user): Important to copy this line to any object you create so it can be tied back to
// the {{ .Resource.Kind }} and cause a reconcile when it changes
Expand Down Expand Up @@ -226,6 +241,7 @@ func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (re
return reconcile.Result{}, err
}
}
{{ end -}}
return reconcile.Result{}, nil
}
Expand Down
22 changes: 13 additions & 9 deletions pkg/scaffold/controller/controllertest.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ import (
"github.com/onsi/gomega"
"golang.org/x/net/context"
{{ if .Resource.CreateExampleReconcileBody -}}
appsv1 "k8s.io/api/apps/v1"
{{ end -}}
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -63,15 +65,14 @@ import (
var c client.Client
var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: "foo", Namespace: "default"}}
var depKey = types.NamespacedName{Namespace: "default", Name: "foo-deployment"}
var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: "foo"{{ if .Resource.Namespaced }}, Namespace: "default"{{end}}}}
{{ if .Resource.CreateExampleReconcileBody }}var depKey = types.NamespacedName{Name: "foo-deployment"{{ if .Resource.Namespaced }}, Namespace: "default"{{end}}}{{ end }}
const timeout = time.Second * 5
func TestReconcile(t *testing.T) {
g := gomega.NewGomegaWithT(t)
instance := &{{ .Resource.Group }}{{ .Resource.Version }}.{{ .Resource.Kind }}{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
deploy := &appsv1.Deployment{}
instance := &{{ .Resource.Group }}{{ .Resource.Version }}.{{ .Resource.Kind }}{ObjectMeta: metav1.ObjectMeta{Name: "foo"{{ if .Resource.Namespaced }}, Namespace: "default"{{end}}}}
// Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a
// channel when it is finished.
Expand All @@ -83,20 +84,23 @@ func TestReconcile(t *testing.T) {
g.Expect(add(mrg, recFn)).NotTo(gomega.HaveOccurred())
defer close(StartTestManager(mrg, g))
// Create the {{ .Resource.Kind }} object and expect the Reconcile and Deployment to be created
g.Expect(c.Create(context.TODO(), instance)).NotTo(gomega.HaveOccurred())
// Create the {{ .Resource.Kind }} object and expect the Reconcile {{ if .Resource.CreateExampleReconcileBody }}and Deployment to be created{{ end }}
g.Expect(c.Create(context.TODO(), instance)).To(gomega.Succeed())
defer c.Delete(context.TODO(), instance)
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedRequest)))
{{ if .Resource.CreateExampleReconcileBody }}
deploy := &appsv1.Deployment{}
g.Eventually(func() error { return c.Get(context.TODO(), depKey, deploy) }, timeout).
ShouldNot(gomega.HaveOccurred())
Should(gomega.Succeed())
// Delete the Deployment and expect Reconcile to be called for Deployment deletion
g.Expect(c.Delete(context.TODO(), deploy)).NotTo(gomega.HaveOccurred())
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedRequest)))
g.Eventually(func() error { return c.Get(context.TODO(), depKey, deploy) }, timeout).
ShouldNot(gomega.HaveOccurred())
Should(gomega.Succeed())
// Manually delete Deployment since GC isn't enabled in the test control plane
g.Expect(c.Delete(context.TODO(), deploy)).NotTo(gomega.HaveOccurred())
g.Expect(c.Delete(context.TODO(), deploy)).To(gomega.Succeed())
{{ end }}
}
`
2 changes: 2 additions & 0 deletions pkg/scaffold/project/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var _ = Describe("Project", func() {

JustBeforeEach(func() {
s, result = scaffoldtest.NewTestScaffold(writeToPath, goldenPath)
s.BoilerplateOptional = true
s.ProjectOptional = true
})

Describe("scaffolding a boilerplate file", func() {
Expand Down
34 changes: 12 additions & 22 deletions pkg/scaffold/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ package resource

import (
"fmt"
"log"
"regexp"
"strings"

"github.com/markbates/inflect"
flag "github.com/spf13/pflag"
)

// Resource contains the information required to scaffold files for a resource.
Expand All @@ -45,18 +43,21 @@ type Resource struct {

// ShortNames is the list of resource shortnames.
ShortNames []string

// CreateExampleReconcileBody will create a Deployment in the Reconcile example
CreateExampleReconcileBody bool
}

// Validate checks the Resource values to make sure they are valid.
func (r *Resource) Validate() error {
if len(r.Group) == 0 {
return fmt.Errorf("Must specify --group")
return fmt.Errorf("group cannot be empty")
}
if len(r.Version) == 0 {
return fmt.Errorf("Must specify --version")
return fmt.Errorf("version cannot be empty")
}
if len(r.Kind) == 0 {
log.Fatal("Must specify --kind")
return fmt.Errorf("kind cannot be empty")
}

rs := inflect.NewDefaultRuleset()
Expand All @@ -66,29 +67,18 @@ func (r *Resource) Validate() error {

groupMatch := regexp.MustCompile("^[a-z]+$")
if !groupMatch.MatchString(r.Group) {
return fmt.Errorf("--group must match regex ^[a-z]+$ but was (%s)", r.Group)
return fmt.Errorf("group must match ^[a-z]+$ (was %s)", r.Group)
}
versionMatch := regexp.MustCompile("^v\\d+(alpha\\d+|beta\\d+)*$")

versionMatch := regexp.MustCompile("^v\\d+(alpha\\d+|beta\\d+)?$")
if !versionMatch.MatchString(r.Version) {
return fmt.Errorf(
"--version has bad format. must match ^v\\d+(alpha\\d+|beta\\d+)*$. "+
"e.g. v1alpha1,v1beta1,v1 but was (%s)", r.Version)
"version must match ^v\\d+(alpha\\d+|beta\\d+)?$ (was %s)", r.Version)
}

kindMatch := regexp.MustCompile("^[A-Z]+[A-Za-z0-9]*$")
if !kindMatch.MatchString(r.Kind) {
return fmt.Errorf("--kind must match regex ^[A-Z]+[A-Za-z0-9]*$ but was (%s)", r.Kind)
if r.Kind != inflect.Camelize(r.Kind) {
return fmt.Errorf("Kind must be camelcase (expected %s was %s)", inflect.Camelize(r.Kind), r.Kind)
}

return nil
}

// ForFlags registers flags for Resource fields and returns the Resource
func ForFlags(f *flag.FlagSet) *Resource {
r := &Resource{}
f.StringVar(&r.Kind, "kind", "", "resource Kind")
f.StringVar(&r.Group, "group", "", "resource Group")
f.StringVar(&r.Version, "version", "", "resource Version")
f.BoolVar(&r.Namespaced, "namespaced", true, "true if the resource is namespaced")
return r
}
13 changes: 13 additions & 0 deletions pkg/scaffold/resource/resource_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package resource_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestResource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Resource Suite")
}
Loading

0 comments on commit 24a846a

Please sign in to comment.