Skip to content

Commit 046556c

Browse files
committed
add support for generic callback hooks
This patch adds support for a generic callback system into the generator config that allows controller implementors to specify some code that should be injected at specific named points in a template. I expect that eventually this generic hook system will be more useful, flexible and extensible than the hodge-podge of custom callback methods and overrides currently in the generator config. The `pkg/generate/config.ResourceConfig` struct now has a `Hooks` field of type `map[string]*HookConfig`, with the map keys being named hook points, e.g. "sdk_update_pre_build_request". There are two ways to inject code at hook points: inline and via a template path. The inline method uses the `HookConfig.Code` field which should contain the Go code that gets injected at a named hook point. The `HookConfig.TemplatePath` field is used to refer to a template file at a specific path. The template file is searched for in any of the TemplateSet's base template paths. Here's an example of a generator config snippet that uses the inline code injection method (`HookConfig.Code`) to add a piece of custom code to be executed in the sdk_update.go.tpl right before the code in the resource manager's `sdkUpdate` method calls the `newUpdateRequestPayload()` function: ```yaml resources: Broker: hooks: sdk_update_pre_build_request: code: if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err } ``` Here is the snippet from the templates/pkg/resource/sdk_update.go.tpl file that shows how we can add these generic named hooks into our templates at various places: ``` {{- if $hookCode := .CRD.HookCode "sdk_update_pre_build_request" }} {{ $hookCode }} {{- end }} ``` The controller implementor need only implement the little `requeueIfNotRunning` function, with the function signature described in the generator config code block. We no longer need to have function signatures match for custom callback code since the function signature for callback code is up to the dev writing the generator.yaml config file. Here is an example of the `HookConfig.TemplatePath` being used to refer to a template file containing some code that is injected at a named hook point: ```yaml resources: Broker: hooks: sdk_update_pre_build_request: template_path: sdk_update_pre_build_request.go.tpl ``` A controller implementor would simply need to populate a `sdk_update_pre_build_request.go.tpl` file with the code to be included at the hook point. This patch introduces the following hook points 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() ```
1 parent 0eb1f2d commit 046556c

File tree

15 files changed

+3735
-10
lines changed

15 files changed

+3735
-10
lines changed

pkg/generate/ack/controller.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,17 @@ func Controller(
122122
return nil, err
123123
}
124124

125+
// Hook code can reference a template path, and we can look up the template
126+
// in any of our base paths...
127+
controllerFuncMap["Hook"] = func(r *ackmodel.CRD, hookID string) string {
128+
code, err := ResourceHookCode(templateBasePaths, r, hookID)
129+
if err != nil {
130+
// It's a compile-time error, so just panic...
131+
panic(err)
132+
}
133+
return code
134+
}
135+
125136
ts := templateset.New(
126137
templateBasePaths,
127138
controllerIncludePaths,

pkg/generate/ack/hook.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package ack
15+
16+
import (
17+
"bytes"
18+
"fmt"
19+
"io/ioutil"
20+
"path/filepath"
21+
ttpl "text/template"
22+
23+
ackmodel "github.com/aws-controllers-k8s/code-generator/pkg/model"
24+
ackutil "github.com/aws-controllers-k8s/code-generator/pkg/util"
25+
)
26+
27+
/*
28+
The following hook points are supported in the ACK controller resource manager
29+
code paths:
30+
31+
* sdk_read_one_pre_build_request
32+
* sdk_read_many_pre_build_request
33+
* sdk_get_attributes_pre_build_request
34+
* sdk_create_pre_build_request
35+
* sdk_update_pre_build_request
36+
* sdk_delete_pre_build_request
37+
* sdk_read_one_post_request
38+
* sdk_read_many_post_request
39+
* sdk_get_attributes_post_request
40+
* sdk_create_post_request
41+
* sdk_update_post_request
42+
* sdk_delete_post_request
43+
* sdk_read_one_pre_set_output
44+
* sdk_read_many_pre_set_output
45+
* sdk_get_attributes_pre_set_output
46+
* sdk_create_pre_set_output
47+
* sdk_update_pre_set_output
48+
49+
The "pre_build_request" hooks are called BEFORE the call to construct
50+
the Input shape that is used in the API operation and therefore BEFORE
51+
any call to validate that Input shape.
52+
53+
The "post_request" hooks are called IMMEDIATELY AFTER the API operation
54+
aws-sdk-go client call. These hooks will have access to a Go variable
55+
named `resp` that refers to the aws-sdk-go client response and a Go
56+
variable named `respErr` that refers to any error returned from the
57+
aws-sdk-go client call.
58+
59+
The "pre_set_output" hooks are called BEFORE the code that processes the
60+
Outputshape (the pkg/generate/code.SetOutput function). These hooks will
61+
have access to a Go variable named `ko` that represents the concrete
62+
Kubernetes CR object that will be returned from the main method
63+
(sdkFind, sdkCreate, etc). This `ko` variable will have been defined
64+
immediately before the "pre_set_output" hooks as a copy of the resource
65+
that is supplied to the main method, like so:
66+
67+
```go
68+
// Merge in the information we read from the API call above to the copy of
69+
// the original Kubernetes object we passed to the function
70+
ko := r.ko.DeepCopy()
71+
```
72+
*/
73+
74+
// ResourceHookCode returns a string with custom callback code for a resource
75+
// and hook identifier
76+
func ResourceHookCode(
77+
templateBasePaths []string,
78+
r *ackmodel.CRD,
79+
hookID string,
80+
) (string, error) {
81+
resourceName := r.Names.Original
82+
if resourceName == "" || hookID == "" {
83+
return "", nil
84+
}
85+
c := r.Config()
86+
if c == nil {
87+
return "", nil
88+
}
89+
rConfig, ok := c.Resources[resourceName]
90+
if !ok {
91+
return "", nil
92+
}
93+
hook, ok := rConfig.Hooks[hookID]
94+
if !ok {
95+
return "", nil
96+
}
97+
if hook.Code != nil {
98+
return *hook.Code, nil
99+
}
100+
if hook.TemplatePath == nil {
101+
err := fmt.Errorf(
102+
"resource %s hook config for %s is invalid. Need either code or template_path",
103+
resourceName, hookID,
104+
)
105+
return "", err
106+
}
107+
for _, basePath := range templateBasePaths {
108+
tplPath := filepath.Join(basePath, *hook.TemplatePath)
109+
if !ackutil.FileExists(tplPath) {
110+
continue
111+
}
112+
tplContents, err := ioutil.ReadFile(tplPath)
113+
if err != nil {
114+
err := fmt.Errorf(
115+
"resource %s hook config for %s is invalid: error reading %s: %s",
116+
resourceName, hookID, tplPath, err,
117+
)
118+
return "", err
119+
}
120+
t := ttpl.New(tplPath)
121+
if t, err = t.Parse(string(tplContents)); err != nil {
122+
err := fmt.Errorf(
123+
"resource %s hook config for %s is invalid: error parsing %s: %s",
124+
resourceName, hookID, tplPath, err,
125+
)
126+
return "", err
127+
}
128+
var b bytes.Buffer
129+
// TODO(jaypipes): Instead of nil for template vars here, maybe pass in
130+
// a struct of variables?
131+
if err := t.Execute(&b, nil); err != nil {
132+
err := fmt.Errorf(
133+
"resource %s hook config for %s is invalid: error executing %s: %s",
134+
resourceName, hookID, tplPath, err,
135+
)
136+
return "", err
137+
}
138+
return b.String(), nil
139+
}
140+
err := fmt.Errorf(
141+
"resource %s hook config for %s is invalid: template_path %s not found",
142+
resourceName, hookID, *hook.TemplatePath,
143+
)
144+
return "", err
145+
}

pkg/generate/ack/hook_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package ack_test
15+
16+
import (
17+
"os"
18+
"path/filepath"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
24+
"github.com/aws-controllers-k8s/code-generator/pkg/generate/ack"
25+
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
26+
)
27+
28+
func TestResourceHookCodeInline(t *testing.T) {
29+
assert := assert.New(t)
30+
require := require.New(t)
31+
basePaths := []string{}
32+
hookID := "sdk_update_pre_build_request"
33+
34+
g := testutil.NewGeneratorForService(t, "mq")
35+
36+
crd := testutil.GetCRDByName(t, g, "Broker")
37+
require.NotNil(crd)
38+
39+
// The Broker's update operation has a special hook callback configured
40+
expected := `if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }`
41+
got, err := ack.ResourceHookCode(basePaths, crd, hookID)
42+
assert.Nil(err)
43+
assert.Equal(expected, got)
44+
}
45+
46+
func TestResourceHookCodeTemplatePath(t *testing.T) {
47+
assert := assert.New(t)
48+
require := require.New(t)
49+
wd, _ := os.Getwd()
50+
basePaths := []string{
51+
filepath.Join(wd, "testdata", "templates"),
52+
}
53+
hookID := "sdk_delete_pre_build_request"
54+
55+
g := testutil.NewGeneratorForService(t, "mq")
56+
57+
crd := testutil.GetCRDByName(t, g, "Broker")
58+
require.NotNil(crd)
59+
60+
// The Broker's delete operation has a special hook configured to point to a template.
61+
expected := "// this is my template.\n"
62+
got, err := ack.ResourceHookCode(basePaths, crd, hookID)
63+
assert.Nil(err)
64+
assert.Equal(expected, got)
65+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// this is my template.

pkg/generate/config/resource.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ type ResourceConfig struct {
2929
// Found and other common error types for primary resources, and thus we
3030
// need these instructions.
3131
Exceptions *ExceptionsConfig `json:"exceptions,omitempty"`
32-
32+
// Hooks is a map, keyed by the hook identifier, of instructions for the
33+
// the code generator about a custom callback hooks that should be injected
34+
// into the resource's manager or SDK binding code.
35+
Hooks map[string]*HooksConfig `json:"hooks"`
3336
// Renames identifies fields in Operations that should be renamed.
3437
Renames *RenamesConfig `json:"renames,omitempty"`
3538
// ListOperation contains instructions for the code generator to generate
@@ -71,6 +74,37 @@ type ResourceConfig struct {
7174
ShortNames []string `json:"shortNames,omitempty"`
7275
}
7376

77+
// HooksConfig instructs the code generator how to inject custom callback hooks
78+
// at various places in the resource manager and SDK linkage code.
79+
//
80+
// Example usage from the AmazonMQ generator config:
81+
//
82+
// resources:
83+
// Broker:
84+
// hooks:
85+
// sdk_update_pre_build_request:
86+
// code: if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }
87+
//
88+
// Note that the implementor of the AmazonMQ service controller for ACK should
89+
// ensure that there is a `requeueIfNotRunning()` method implementation in
90+
// `pkg/resource/broker`
91+
//
92+
// Instead of placing Go code directly into the generator.yaml file using the
93+
// `code` field, you can reference a template file containing Go code with the
94+
// `template_path` field:
95+
//
96+
// resources:
97+
// Broker:
98+
// hooks:
99+
// sdk_update_pre_build_update_request:
100+
// template_path: templates/sdk_update_pre_build_request.go.tpl
101+
type HooksConfig struct {
102+
// Code is the Go code to be injected at the hook point
103+
Code *string `json:"code,omitempty"`
104+
// TemplatePath is a path to the template containing the hook code
105+
TemplatePath *string `json:"template_path,omitempty"`
106+
}
107+
74108
// CompareConfig informs instruct the code generator on how to compare two different
75109
// two objects of the same type
76110
type CompareConfig struct {

pkg/generate/templateset/templateset.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
ttpl "text/template"
2323

2424
"github.com/pkg/errors"
25+
26+
ackutil "github.com/aws-controllers-k8s/code-generator/pkg/util"
2527
)
2628

2729
var (
@@ -83,7 +85,7 @@ func (ts *TemplateSet) Add(
8385
var foundPath string
8486
for _, basePath := range ts.baseSearchPaths {
8587
path := filepath.Join(basePath, templatePath)
86-
if fileExists(path) {
88+
if ackutil.FileExists(path) {
8789
foundPath = path
8890
break
8991
}
@@ -116,7 +118,7 @@ func (ts *TemplateSet) joinIncludes(t *ttpl.Template) error {
116118
for _, basePath := range ts.baseSearchPaths {
117119
for _, includePath := range ts.includePaths {
118120
tplPath := filepath.Join(basePath, includePath)
119-
if !fileExists(tplPath) {
121+
if !ackutil.FileExists(tplPath) {
120122
continue
121123
}
122124
if t, err = includeTemplate(t, tplPath); err != nil {
@@ -142,7 +144,7 @@ func (ts *TemplateSet) Execute() error {
142144
for _, basePath := range ts.baseSearchPaths {
143145
for _, path := range ts.copyPaths {
144146
copyPath := filepath.Join(basePath, path)
145-
if !fileExists(copyPath) {
147+
if !ackutil.FileExists(copyPath) {
146148
continue
147149
}
148150
b, err := byteBufferFromFile(copyPath)
@@ -194,9 +196,3 @@ func includeTemplate(t *ttpl.Template, tplPath string) (*ttpl.Template, error) {
194196
}
195197
return t, nil
196198
}
197-
198-
// fileExists returns tTrue if the supplied file path exists, false otherwise
199-
func fileExists(path string) bool {
200-
_, err := os.Stat(path)
201-
return !os.IsNotExist(err)
202-
}

0 commit comments

Comments
 (0)