Skip to content

Commit ff4dfb7

Browse files
author
Eric Stroczynski
authored
cmd/operator-sdk/add/controller.go: support built-in k8s API controllers (#1344)
* cmd/operator-sdk/add/controller.go: --custom-api-import takes a path and optionally an import identifier for an expernal k8s API * internal/pkg/scaffold/controller_kind*.go: parse and set K8sImport in template * update CHANGELOG and sdk cli doc
1 parent 1ac84ab commit ff4dfb7

File tree

5 files changed

+148
-22
lines changed

5 files changed

+148
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Manager is now configured with a new `DynamicRESTMapper`, which accounts for the fact that the default `RESTMapper`, which only checks resource types at startup, can't handle the case of first creating a CRD and then an instance of that CRD. ([#1329](https://github.com/operator-framework/operator-sdk/pull/1329))
77
- Unify CLI debug logging under a global `--verbose` flag ([#1361](https://github.com/operator-framework/operator-sdk/pull/1361))
88
- [Go module](https://github.com/golang/go/wiki/Modules) support by default for new Go operators and during Ansible and Helm operator migration. The dependency manager used for a new operator can be explicitly specified for new operators through the `--dep-manager` flag, available in [`operator-sdk new`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#new) and [`operator-sdk migrate`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#migrate). `dep` is still available through `--dep-manager=dep`. ([#1001](https://github.com/operator-framework/operator-sdk/pull/1001))
9+
- New optional flag `--custom-api-import` for [`operator-sdk add controller`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#controller) to specify that the new controller reconciles a built-in or external Kubernetes API, and what import path and identifier it should have. ([#1344](https://github.com/operator-framework/operator-sdk/pull/1344))
910

1011
### Changed
1112

cmd/operator-sdk/add/controller.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"github.com/spf13/cobra"
2626
)
2727

28+
var customAPIImport string
29+
2830
func newAddControllerCmd() *cobra.Command {
2931
controllerCmd := &cobra.Command{
3032
Use: "controller",
@@ -57,6 +59,7 @@ Example:
5759
if err := controllerCmd.MarkFlagRequired("kind"); err != nil {
5860
log.Fatalf("Failed to mark `kind` flag for `add controller` subcommand as required")
5961
}
62+
controllerCmd.Flags().StringVar(&customAPIImport, "custom-api-import", "", `External Kubernetes resource import path of the form "host.com/repo/path[=import_identifier]". import_identifier is optional`)
6063

6164
return controllerCmd
6265
}
@@ -81,10 +84,10 @@ func controllerRun(cmd *cobra.Command, args []string) error {
8184
Repo: projutil.CheckAndGetProjectGoPkg(),
8285
AbsProjectPath: projutil.MustGetwd(),
8386
}
84-
8587
s := &scaffold.Scaffold{}
88+
8689
err = s.Execute(cfg,
87-
&scaffold.ControllerKind{Resource: r},
90+
&scaffold.ControllerKind{Resource: r, CustomImport: customAPIImport},
8891
&scaffold.AddController{Resource: r},
8992
)
9093
if err != nil {

doc/sdk-cli-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ Adds a new controller under `pkg/controller/<kind>/...` that, by default, reconc
358358

359359
* `--api-version` string - CRD APIVersion in the format `$GROUP_NAME/$VERSION` (e.g app.example.com/v1alpha1)
360360
* `--kind` string - CRD Kind. (e.g AppService)
361+
* `--custom-api-import` string - External Kubernetes resource import path of the form "host.com/repo/path[=import_identifier]". import_identifier is optional
361362

362363
#### Example
363364

internal/pkg/scaffold/controller_kind.go

Lines changed: 105 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
package scaffold
1616

1717
import (
18+
"fmt"
19+
"path"
1820
"path/filepath"
21+
"strings"
22+
"unicode"
1923

2024
"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input"
2125
)
@@ -26,6 +30,18 @@ type ControllerKind struct {
2630

2731
// Resource defines the inputs for the controller's primary resource
2832
Resource *Resource
33+
// CustomImport holds the import path for a built-in or custom Kubernetes
34+
// API that this controller reconciles, if specified by the scaffold invoker.
35+
CustomImport string
36+
37+
// The following fields will be overwritten by GetInput().
38+
//
39+
// ImportMap maps all imports destined for the scaffold to their import
40+
// identifier, if any.
41+
ImportMap map[string]string
42+
// GoImportIdent is the import identifier for the API reconciled by this
43+
// controller.
44+
GoImportIdent string
2945
}
3046

3147
func (s *ControllerKind) GetInput() (input.Input, error) {
@@ -36,29 +52,99 @@ func (s *ControllerKind) GetInput() (input.Input, error) {
3652
// Error if this file exists.
3753
s.IfExistsAction = input.Error
3854
s.TemplateBody = controllerKindTemplate
55+
56+
// Set imports.
57+
if err := s.setImports(); err != nil {
58+
return input.Input{}, err
59+
}
3960
return s.Input, nil
4061
}
4162

63+
func (s *ControllerKind) setImports() (err error) {
64+
s.ImportMap = controllerKindImports
65+
importPath := ""
66+
if s.CustomImport != "" {
67+
importPath, s.GoImportIdent, err = getCustomAPIImportPathAndIdent(s.CustomImport)
68+
if err != nil {
69+
return err
70+
}
71+
} else {
72+
importPath = path.Join(s.Repo, "pkg", "apis", s.Resource.GoImportGroup, s.Resource.Version)
73+
s.GoImportIdent = s.Resource.GoImportGroup + s.Resource.Version
74+
}
75+
// Import identifiers must be unique within a file.
76+
for p, id := range s.ImportMap {
77+
if s.GoImportIdent == id && importPath != p {
78+
// Append "api" to the conflicting import identifier.
79+
s.GoImportIdent = s.GoImportIdent + "api"
80+
break
81+
}
82+
}
83+
s.ImportMap[importPath] = s.GoImportIdent
84+
return nil
85+
}
86+
87+
func getCustomAPIImportPathAndIdent(m string) (p string, id string, err error) {
88+
sm := strings.Split(m, "=")
89+
for i, e := range sm {
90+
if i == 0 {
91+
p = strings.TrimSpace(e)
92+
} else if i == 1 {
93+
id = strings.TrimSpace(e)
94+
}
95+
}
96+
if p == "" {
97+
return "", "", fmt.Errorf(`custom import "%s" path is empty`, m)
98+
}
99+
if id == "" {
100+
if len(sm) == 2 {
101+
return "", "", fmt.Errorf(`custom import "%s" identifier is empty, remove "=" from passed string`, m)
102+
}
103+
sp := strings.Split(p, "/")
104+
if len(sp) > 1 {
105+
id = sp[len(sp)-2] + sp[len(sp)-1]
106+
} else {
107+
id = sp[0]
108+
}
109+
id = strings.ToLower(id)
110+
}
111+
idb := &strings.Builder{}
112+
// By definition, all package identifiers must be comprised of "_", unicode
113+
// digits, and/or letters.
114+
for _, r := range id {
115+
if unicode.IsDigit(r) || unicode.IsLetter(r) || r == '_' {
116+
if _, err := idb.WriteRune(r); err != nil {
117+
return "", "", err
118+
}
119+
}
120+
}
121+
return p, idb.String(), nil
122+
}
123+
124+
var controllerKindImports = map[string]string{
125+
"k8s.io/api/core/v1": "corev1",
126+
"k8s.io/apimachinery/pkg/api/errors": "",
127+
"k8s.io/apimachinery/pkg/apis/meta/v1": "metav1",
128+
"k8s.io/apimachinery/pkg/runtime": "",
129+
"k8s.io/apimachinery/pkg/types": "",
130+
"sigs.k8s.io/controller-runtime/pkg/client": "",
131+
"sigs.k8s.io/controller-runtime/pkg/controller": "",
132+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil": "",
133+
"sigs.k8s.io/controller-runtime/pkg/handler": "",
134+
"sigs.k8s.io/controller-runtime/pkg/manager": "",
135+
"sigs.k8s.io/controller-runtime/pkg/reconcile": "",
136+
"sigs.k8s.io/controller-runtime/pkg/runtime/log": "logf",
137+
"sigs.k8s.io/controller-runtime/pkg/source": "",
138+
}
139+
42140
const controllerKindTemplate = `package {{ .Resource.LowerKind }}
43141
44142
import (
45143
"context"
46144
47-
{{ .Resource.GoImportGroup}}{{ .Resource.Version }} "{{ .Repo }}/pkg/apis/{{ .Resource.GoImportGroup}}/{{ .Resource.Version }}"
48-
49-
corev1 "k8s.io/api/core/v1"
50-
"k8s.io/apimachinery/pkg/api/errors"
51-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
52-
"k8s.io/apimachinery/pkg/runtime"
53-
"k8s.io/apimachinery/pkg/types"
54-
"sigs.k8s.io/controller-runtime/pkg/client"
55-
"sigs.k8s.io/controller-runtime/pkg/controller"
56-
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
57-
"sigs.k8s.io/controller-runtime/pkg/handler"
58-
"sigs.k8s.io/controller-runtime/pkg/manager"
59-
"sigs.k8s.io/controller-runtime/pkg/reconcile"
60-
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
61-
"sigs.k8s.io/controller-runtime/pkg/source"
145+
{{range $p, $i := .ImportMap -}}
146+
{{$i}} "{{$p}}"
147+
{{end}}
62148
)
63149
64150
var log = logf.Log.WithName("controller_{{ .Resource.LowerKind }}")
@@ -88,7 +174,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
88174
}
89175
90176
// Watch for changes to primary resource {{ .Resource.Kind }}
91-
err = c.Watch(&source.Kind{Type: &{{ .Resource.GoImportGroup}}{{ .Resource.Version }}.{{ .Resource.Kind }}{}}, &handler.EnqueueRequestForObject{})
177+
err = c.Watch(&source.Kind{Type: &{{ .GoImportIdent }}.{{ .Resource.Kind }}{}}, &handler.EnqueueRequestForObject{})
92178
if err != nil {
93179
return err
94180
}
@@ -97,7 +183,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
97183
// Watch for changes to secondary resource Pods and requeue the owner {{ .Resource.Kind }}
98184
err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
99185
IsController: true,
100-
OwnerType: &{{ .Resource.GoImportGroup}}{{ .Resource.Version }}.{{ .Resource.Kind }}{},
186+
OwnerType: &{{ .GoImportIdent }}.{{ .Resource.Kind }}{},
101187
})
102188
if err != nil {
103189
return err
@@ -129,7 +215,7 @@ func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (re
129215
reqLogger.Info("Reconciling {{ .Resource.Kind }}")
130216
131217
// Fetch the {{ .Resource.Kind }} instance
132-
instance := &{{ .Resource.GoImportGroup}}{{ .Resource.Version }}.{{ .Resource.Kind }}{}
218+
instance := &{{ .GoImportIdent }}.{{ .Resource.Kind }}{}
133219
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
134220
if err != nil {
135221
if errors.IsNotFound(err) {
@@ -172,7 +258,7 @@ func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (re
172258
}
173259
174260
// newPodForCR returns a busybox pod with the same name/namespace as the cr
175-
func newPodForCR(cr *{{ .Resource.GoImportGroup}}{{ .Resource.Version }}.{{ .Resource.Kind }}) *corev1.Pod {
261+
func newPodForCR(cr *{{ .GoImportIdent }}.{{ .Resource.Kind }}) *corev1.Pod {
176262
labels := map[string]string{
177263
"app": cr.Name,
178264
}

internal/pkg/scaffold/controller_kind_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import (
4343
"context"
4444
4545
appv1alpha1 "github.com/example-inc/app-operator/pkg/apis/app/v1alpha1"
46-
4746
corev1 "k8s.io/api/core/v1"
4847
"k8s.io/apimachinery/pkg/api/errors"
4948
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -192,3 +191,39 @@ func newPodForCR(cr *appv1alpha1.AppService) *corev1.Pod {
192191
}
193192
}
194193
`
194+
195+
func TestGetCustomAPIImportPathAndIdentifier(t *testing.T) {
196+
cases := []struct {
197+
inputImport, wantImportPath, wantImportIdent string
198+
wantErr bool
199+
}{
200+
{"", "", "", true},
201+
{"=rbacv1", "", "", true},
202+
{"k8s.io/api/rbac-2/v1", "k8s.io/api/rbac-2/v1", "rbac2v1", false},
203+
{"k8s.io/api/rbac/v1=rbacv1", "k8s.io/api/rbac/v1", "rbacv1", false},
204+
{"k8s.io/api/rbac/v1=rbac-v1", "k8s.io/api/rbac/v1", "rbacv1", false},
205+
{"k8s.io/api/rb_ac/v1", "k8s.io/api/rb_ac/v1", "rb_acv1", false},
206+
{"k8s.io/api/rbac/v1=rbaC_v1", "k8s.io/api/rbac/v1", "rbaC_v1", false},
207+
{"k8s.io/api/rbac/v1=", "", "", true},
208+
{"k8s.io/api/rbac/v1=rbacv1=", "k8s.io/api/rbac/v1", "rbacv1", false},
209+
{"k8s.io/api/rbac/v1=Rbacv1", "k8s.io/api/rbac/v1", "Rbacv1", false},
210+
}
211+
212+
for _, c := range cases {
213+
gotPath, gotIdent, err := getCustomAPIImportPathAndIdent(c.inputImport)
214+
if err != nil && !c.wantErr {
215+
t.Errorf(`wanted import path "%s" and identifier "%s" from "%s", got error %v`, c.wantImportPath, c.wantImportIdent, c.inputImport, err)
216+
continue
217+
}
218+
if err == nil && c.wantErr {
219+
t.Errorf(`wanted error from "%s", got import path "%s" and identifier "%s"`, c.inputImport, c.wantImportPath, c.wantImportIdent)
220+
continue
221+
}
222+
if gotPath != c.wantImportPath {
223+
t.Errorf(`wanted import path "%s" from "%s", got "%s"`, c.wantImportPath, c.inputImport, gotPath)
224+
}
225+
if gotIdent != c.wantImportIdent {
226+
t.Errorf(`wanted import identifier "%s" from "%s", got "%s"`, c.wantImportIdent, c.inputImport, gotIdent)
227+
}
228+
}
229+
}

0 commit comments

Comments
 (0)