Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pkg/generate/ack/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ func Controller(
return nil, err
}

// Hook code can reference a template path, and we can look up the template
// in any of our base paths...
controllerFuncMap["Hook"] = func(r *ackmodel.CRD, hookID string) string {
code, err := ResourceHookCode(templateBasePaths, r, hookID)
if err != nil {
// It's a compile-time error, so just panic...
panic(err)
}
return code
}

ts := templateset.New(
templateBasePaths,
controllerIncludePaths,
Expand Down
145 changes: 145 additions & 0 deletions pkg/generate/ack/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 ack

import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
ttpl "text/template"

ackmodel "github.com/aws-controllers-k8s/code-generator/pkg/model"
ackutil "github.com/aws-controllers-k8s/code-generator/pkg/util"
)

/*
The following hook points are supported in the ACK controller resource manager
code paths:

* sdk_read_one_pre_build_request
* sdk_read_many_pre_build_request
* sdk_get_attributes_pre_build_request
* sdk_create_pre_build_request
* sdk_update_pre_build_request
* sdk_delete_pre_build_request
* sdk_read_one_post_request
* sdk_read_many_post_request
* sdk_get_attributes_post_request
* sdk_create_post_request
* sdk_update_post_request
* sdk_delete_post_request
* sdk_read_one_pre_set_output
* sdk_read_many_pre_set_output
* sdk_get_attributes_pre_set_output
* sdk_create_pre_set_output
* sdk_update_pre_set_output

The "pre_build_request" hooks are called BEFORE the call to construct
the Input shape that is used in the API operation and therefore BEFORE
any call to validate that Input shape.

The "post_request" hooks are called IMMEDIATELY AFTER the API operation
aws-sdk-go client call. These hooks will have access to a Go variable
named `resp` that refers to the aws-sdk-go client response and a Go
variable named `respErr` that refers to any error returned from the
aws-sdk-go client call.

The "pre_set_output" hooks are called BEFORE the code that processes the
Outputshape (the pkg/generate/code.SetOutput function). These hooks will
have access to a Go variable named `ko` that represents the concrete
Kubernetes CR object that will be returned from the main method
(sdkFind, sdkCreate, etc). This `ko` variable will have been defined
immediately before the "pre_set_output" hooks as a copy of the resource
that is supplied to the main method, like so:

```go
// Merge in the information we read from the API call above to the copy of
// the original Kubernetes object we passed to the function
ko := r.ko.DeepCopy()
```
*/

// ResourceHookCode returns a string with custom callback code for a resource
// and hook identifier
func ResourceHookCode(
templateBasePaths []string,
r *ackmodel.CRD,
hookID string,
) (string, error) {
resourceName := r.Names.Original
if resourceName == "" || hookID == "" {
return "", nil
}
c := r.Config()
if c == nil {
return "", nil
}
rConfig, ok := c.Resources[resourceName]
if !ok {
return "", nil
}
hook, ok := rConfig.Hooks[hookID]
if !ok {
return "", nil
}
if hook.Code != nil {
return *hook.Code, nil
}
if hook.TemplatePath == nil {
err := fmt.Errorf(
"resource %s hook config for %s is invalid. Need either code or template_path",
resourceName, hookID,
)
return "", err
}
for _, basePath := range templateBasePaths {
tplPath := filepath.Join(basePath, *hook.TemplatePath)
if !ackutil.FileExists(tplPath) {
continue
}
tplContents, err := ioutil.ReadFile(tplPath)
if err != nil {
err := fmt.Errorf(
"resource %s hook config for %s is invalid: error reading %s: %s",
resourceName, hookID, tplPath, err,
)
return "", err
}
t := ttpl.New(tplPath)
if t, err = t.Parse(string(tplContents)); err != nil {
err := fmt.Errorf(
"resource %s hook config for %s is invalid: error parsing %s: %s",
resourceName, hookID, tplPath, err,
)
return "", err
}
var b bytes.Buffer
// TODO(jaypipes): Instead of nil for template vars here, maybe pass in
// a struct of variables?
Comment on lines +129 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea, but I couldn't come up with any variables that could be useful at compile-time? Possibly version information?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I left it as a TODO because I couldn't think of anything off top of my head. We can always add later if we stumble on something that should be passed to the template...

if err := t.Execute(&b, nil); err != nil {
err := fmt.Errorf(
"resource %s hook config for %s is invalid: error executing %s: %s",
resourceName, hookID, tplPath, err,
)
return "", err
}
return b.String(), nil
}
err := fmt.Errorf(
"resource %s hook config for %s is invalid: template_path %s not found",
resourceName, hookID, *hook.TemplatePath,
)
return "", err
}
65 changes: 65 additions & 0 deletions pkg/generate/ack/hook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 ack_test

import (
"os"
"path/filepath"
"testing"

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

"github.com/aws-controllers-k8s/code-generator/pkg/generate/ack"
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
)

func TestResourceHookCodeInline(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
basePaths := []string{}
hookID := "sdk_update_pre_build_request"

g := testutil.NewGeneratorForService(t, "mq")

crd := testutil.GetCRDByName(t, g, "Broker")
require.NotNil(crd)

// The Broker's update operation has a special hook callback configured
expected := `if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }`
got, err := ack.ResourceHookCode(basePaths, crd, hookID)
assert.Nil(err)
assert.Equal(expected, got)
}

func TestResourceHookCodeTemplatePath(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
wd, _ := os.Getwd()
basePaths := []string{
filepath.Join(wd, "testdata", "templates"),
}
hookID := "sdk_delete_pre_build_request"

g := testutil.NewGeneratorForService(t, "mq")

crd := testutil.GetCRDByName(t, g, "Broker")
require.NotNil(crd)

// The Broker's delete operation has a special hook configured to point to a template.
expected := "// this is my template.\n"
got, err := ack.ResourceHookCode(basePaths, crd, hookID)
assert.Nil(err)
assert.Equal(expected, got)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// this is my template.
36 changes: 35 additions & 1 deletion pkg/generate/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ type ResourceConfig struct {
// Found and other common error types for primary resources, and thus we
// need these instructions.
Exceptions *ExceptionsConfig `json:"exceptions,omitempty"`

// Hooks is a map, keyed by the hook identifier, of instructions for the
// the code generator about a custom callback hooks that should be injected
// into the resource's manager or SDK binding code.
Hooks map[string]*HooksConfig `json:"hooks"`
// Renames identifies fields in Operations that should be renamed.
Renames *RenamesConfig `json:"renames,omitempty"`
// ListOperation contains instructions for the code generator to generate
Expand Down Expand Up @@ -71,6 +74,37 @@ type ResourceConfig struct {
ShortNames []string `json:"shortNames,omitempty"`
}

// HooksConfig instructs the code generator how to inject custom callback hooks
// at various places in the resource manager and SDK linkage code.
//
// Example usage from the AmazonMQ generator config:
//
// resources:
// Broker:
// hooks:
// sdk_update_pre_build_request:
// code: if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }
//
// Note that the implementor of the AmazonMQ service controller for ACK should
// ensure that there is a `requeueIfNotRunning()` method implementation in
// `pkg/resource/broker`
//
// Instead of placing Go code directly into the generator.yaml file using the
// `code` field, you can reference a template file containing Go code with the
// `template_path` field:
//
// resources:
// Broker:
// hooks:
// sdk_update_pre_build_update_request:
// template_path: templates/sdk_update_pre_build_request.go.tpl
type HooksConfig struct {
// Code is the Go code to be injected at the hook point
Code *string `json:"code,omitempty"`
// TemplatePath is a path to the template containing the hook code
TemplatePath *string `json:"template_path,omitempty"`
}

// CompareConfig informs instruct the code generator on how to compare two different
// two objects of the same type
type CompareConfig struct {
Expand Down
14 changes: 5 additions & 9 deletions pkg/generate/templateset/templateset.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
ttpl "text/template"

"github.com/pkg/errors"

ackutil "github.com/aws-controllers-k8s/code-generator/pkg/util"
)

var (
Expand Down Expand Up @@ -83,7 +85,7 @@ func (ts *TemplateSet) Add(
var foundPath string
for _, basePath := range ts.baseSearchPaths {
path := filepath.Join(basePath, templatePath)
if fileExists(path) {
if ackutil.FileExists(path) {
foundPath = path
break
}
Expand Down Expand Up @@ -116,7 +118,7 @@ func (ts *TemplateSet) joinIncludes(t *ttpl.Template) error {
for _, basePath := range ts.baseSearchPaths {
for _, includePath := range ts.includePaths {
tplPath := filepath.Join(basePath, includePath)
if !fileExists(tplPath) {
if !ackutil.FileExists(tplPath) {
continue
}
if t, err = includeTemplate(t, tplPath); err != nil {
Expand All @@ -142,7 +144,7 @@ func (ts *TemplateSet) Execute() error {
for _, basePath := range ts.baseSearchPaths {
for _, path := range ts.copyPaths {
copyPath := filepath.Join(basePath, path)
if !fileExists(copyPath) {
if !ackutil.FileExists(copyPath) {
continue
}
b, err := byteBufferFromFile(copyPath)
Expand Down Expand Up @@ -194,9 +196,3 @@ func includeTemplate(t *ttpl.Template, tplPath string) (*ttpl.Template, error) {
}
return t, nil
}

// fileExists returns tTrue if the supplied file path exists, false otherwise
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
Loading