Skip to content

Commit 4aed99b

Browse files
authored
Merge pull request #26 from jaypipes/generic-hooks
add support for generic callback hooks
2 parents 0eb1f2d + 046556c commit 4aed99b

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)