Skip to content

Commit 1c1386f

Browse files
operator-sdk add api for Ansible/Helm operators. (#2703)
* *add/api creation enabled for ansible/helm
1 parent 498658f commit 1c1386f

File tree

40 files changed

+2916
-220
lines changed

40 files changed

+2916
-220
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
entries:
2+
- description: Add support for additional API creation for Anisble/Helm based operators.
3+
kind: "addition"

cmd/operator-sdk/add/api.go

+238-58
Original file line numberDiff line numberDiff line change
@@ -20,90 +20,270 @@ import (
2020
"os"
2121
"path/filepath"
2222

23+
log "github.com/sirupsen/logrus"
24+
"github.com/spf13/cobra"
25+
"gopkg.in/yaml.v2"
26+
"k8s.io/client-go/discovery"
27+
"sigs.k8s.io/controller-runtime/pkg/client/config"
28+
2329
"github.com/operator-framework/operator-sdk/cmd/operator-sdk/internal/genutil"
24-
gencrd "github.com/operator-framework/operator-sdk/internal/generate/crd"
30+
apiflags "github.com/operator-framework/operator-sdk/internal/flags/apiflags"
2531
"github.com/operator-framework/operator-sdk/internal/scaffold"
32+
"github.com/operator-framework/operator-sdk/internal/scaffold/ansible"
33+
"github.com/operator-framework/operator-sdk/internal/scaffold/helm"
2634
"github.com/operator-framework/operator-sdk/internal/scaffold/input"
2735
"github.com/operator-framework/operator-sdk/internal/util/projutil"
28-
29-
log "github.com/sirupsen/logrus"
30-
"github.com/spf13/cobra"
3136
)
3237

33-
var (
34-
apiVersion string
35-
kind string
36-
skipGeneration bool
37-
crdVersion string
38-
)
38+
var apiFlags apiflags.APIFlags
3939

4040
func newAddAPICmd() *cobra.Command {
4141
apiCmd := &cobra.Command{
4242
Use: "api",
4343
Short: "Adds a new api definition under pkg/apis",
44-
Long: `operator-sdk add api --kind=<kind> --api-version=<group/version> creates
45-
the api definition for a new custom resource under pkg/apis. This command
46-
must be run from the project root directory. If the api already exists at
47-
pkg/apis/<group>/<version> then the command will not overwrite and return
48-
an error.
49-
50-
By default, this command runs Kubernetes deepcopy and CRD generators on
51-
tagged types in all paths under pkg/apis. Go code is generated under
52-
pkg/apis/<group>/<version>/zz_generated.deepcopy.go. CRD's are generated,
53-
or updated if they exist for a particular group + version + kind, under
44+
Long: `operator-sdk add api --kind=<kind> --api-version<group/version>
45+
creates an API definition for a new custom resource.
46+
This command must be run from the project root directory.
47+
48+
For Go-based operators:
49+
50+
- Creates the api definition for a new custom resource under pkg/apis.
51+
- By default, this command runs Kubernetes deepcopy and CRD generators on
52+
tagged types in all paths under pkg/apis. Go code is generated under
53+
pkg/apis/<group>/<version>/zz_generated.deepcopy.go. Generation can be disabled with the
54+
--skip-generation flag for Go-based operators.
55+
56+
For Ansible-based operators:
57+
58+
- Creates resource folder under /roles.
59+
- watches.yaml is updated with new resource.
60+
- deploy/role.yaml will be updated with apiGroup for new API.
61+
62+
For Helm-based operators:
63+
- Creates resource folder under /helm-charts.
64+
- watches.yaml is updated with new resource.
65+
- deploy/role.yaml will be updated to reflact new rules for the incoming API.
66+
67+
CRD's are generated, or updated if they exist for a particular group + version + kind, under
5468
deploy/crds/<full group>_<resource>_crd.yaml; OpenAPI V3 validation YAML
55-
is generated as a 'validation' object. Generation can be disabled with the
56-
--skip-generation flag.
57-
58-
Example:
59-
60-
$ operator-sdk add api --api-version=app.example.com/v1alpha1 --kind=AppService
61-
$ tree pkg/apis
62-
pkg/apis/
63-
├── addtoscheme_app_appservice.go
64-
├── apis.go
65-
└── app
66-
└── v1alpha1
67-
├── doc.go
68-
├── register.go
69-
├── appservice_types.go
70-
├── zz_generated.deepcopy.go
71-
$ tree deploy/crds
72-
├── deploy/crds/app.example.com_v1alpha1_appservice_cr.yaml
73-
├── deploy/crds/app.example.com_appservices_crd.yaml
69+
is generated as a 'validation' object.`,
70+
Example: ` # Create a new API, under an existing project. This command must be run from the project root directory.
71+
# Go Example:
72+
$ operator-sdk add api --api-version=app.example.com/v1alpha1 --kind=AppService
73+
74+
# Ansible Example
75+
$ operator-sdk add api \
76+
--api-version=app.example.com/v1alpha1 \
77+
--kind=AppService
78+
79+
# Helm Example:
80+
$ operator-sdk add api \
81+
--api-version=app.example.com/v1alpha1 \
82+
--kind=AppService
83+
84+
$ operator-sdk add api \
85+
--api-version=app.example.com/v1alpha1 \
86+
--kind=AppService
87+
--helm-chart=myrepo/app
88+
89+
$ operator-sdk add api \
90+
--helm-chart=myrepo/app
91+
92+
$ operator-sdk add api \
93+
--helm-chart=myrepo/app \
94+
--helm-chart-version=1.2.3
95+
96+
$ operator-sdk add api \
97+
--helm-chart=app \
98+
--helm-chart-repo=https://charts.mycompany.com/
99+
100+
$ operator-sdk add api \
101+
--helm-chart=app \
102+
--helm-chart-repo=https://charts.mycompany.com/ \
103+
--helm-chart-version=1.2.3
104+
105+
$ operator-sdk add api \
106+
--helm-chart=/path/to/local/chart-directories/app/
107+
108+
$ operator-sdk add api \
109+
--helm-chart=/path/to/local/chart-archives/app-1.2.3.tgz
74110
`,
75111
RunE: apiRun,
76112
}
77113

78-
apiCmd.Flags().StringVar(&apiVersion, "api-version", "",
79-
"Kubernetes APIVersion that has a format of $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)")
80-
if err := apiCmd.MarkFlagRequired("api-version"); err != nil {
81-
log.Fatalf("Failed to mark `api-version` flag for `add api` subcommand as required")
82-
}
83-
apiCmd.Flags().StringVar(&kind, "kind", "", "Kubernetes resource Kind name. (e.g AppService)")
84-
if err := apiCmd.MarkFlagRequired("kind"); err != nil {
85-
log.Fatalf("Failed to mark `kind` flag for `add api` subcommand as required")
86-
}
87-
apiCmd.Flags().BoolVar(&skipGeneration, "skip-generation", false,
88-
"Skip generation of deepcopy and OpenAPI code and OpenAPI CRD specs")
89-
apiCmd.Flags().StringVar(&crdVersion, "crd-version", gencrd.DefaultCRDVersion,
90-
"CRD version to generate")
114+
// Initialize flagSet struct with command flags
115+
apiFlags.AddTo(apiCmd.Flags())
91116

92117
return apiCmd
93118
}
94119

95120
func apiRun(cmd *cobra.Command, args []string) error {
121+
96122
projutil.MustInProjectRoot()
97123

98-
// Only Go projects can add apis.
99-
if err := projutil.CheckGoProjectCmd(cmd); err != nil {
124+
operatorType := projutil.GetOperatorType()
125+
if operatorType == projutil.OperatorTypeUnknown {
126+
return projutil.ErrUnknownOperatorType{}
127+
}
128+
// Verify the incoming flags.
129+
if err := apiFlags.VerifyCommonFlags(operatorType); err != nil {
100130
return err
101131
}
102132

103-
log.Infof("Generating api version %s for kind %s.", apiVersion, kind)
133+
log.Infof("Generating api version %s for kind %s.", apiFlags.APIVersion, apiFlags.Kind)
134+
135+
switch operatorType {
136+
case projutil.OperatorTypeGo:
137+
if err := doGoAPIScaffold(); err != nil {
138+
return err
139+
}
140+
case projutil.OperatorTypeAnsible:
141+
if err := doAnsibleAPIScaffold(); err != nil {
142+
return err
143+
}
144+
case projutil.OperatorTypeHelm:
145+
if err := doHelmAPIScaffold(); err != nil {
146+
return err
147+
}
148+
}
149+
log.Info("API generation complete.")
150+
return nil
151+
}
152+
153+
// TODO
154+
// Consolidate scaffold func to be used by both "new" and "add api" commands.
155+
func doAnsibleAPIScaffold() error {
156+
// Create and validate new resource.
157+
r, err := scaffold.NewResource(apiFlags.APIVersion, apiFlags.Kind)
158+
if err != nil {
159+
return fmt.Errorf("invalid apiVersion and kind: %v", err)
160+
}
161+
absProjectPath := projutil.MustGetwd()
162+
cfg := &input.Config{
163+
AbsProjectPath: absProjectPath,
164+
}
165+
roleFiles := ansible.RolesFiles{Resource: *r}
166+
roleTemplates := ansible.RolesTemplates{Resource: *r}
167+
168+
// update watch.yaml for the given resource r.
169+
if err := ansible.UpdateAnsibleWatchForResource(r, absProjectPath); err != nil {
170+
return fmt.Errorf("failed to update the Watch manifest for the resource (%v, %v): (%v)",
171+
r.APIVersion, r.Kind, err)
172+
}
173+
174+
s := &scaffold.Scaffold{}
175+
err = s.Execute(cfg,
176+
&scaffold.CR{Resource: r},
177+
&ansible.RolesReadme{Resource: *r},
178+
&ansible.RolesMetaMain{Resource: *r},
179+
&roleFiles,
180+
&roleTemplates,
181+
&ansible.RolesVarsMain{Resource: *r},
182+
&ansible.RolesDefaultsMain{Resource: *r},
183+
&ansible.RolesTasksMain{Resource: *r},
184+
&ansible.RolesHandlersMain{Resource: *r},
185+
)
186+
if err != nil {
187+
return fmt.Errorf("new ansible api scaffold failed: %v", err)
188+
}
189+
if err = genutil.GenerateCRDNonGo("", *r, apiFlags.CrdVersion); err != nil {
190+
return err
191+
}
192+
193+
// Remove placeholders from empty directories
194+
err = os.Remove(filepath.Join(s.AbsProjectPath, roleFiles.Path))
195+
if err != nil {
196+
return fmt.Errorf("new ansible api scaffold failed: %v", err)
197+
}
198+
err = os.Remove(filepath.Join(s.AbsProjectPath, roleTemplates.Path))
199+
if err != nil {
200+
return fmt.Errorf("new ansible api scaffold failed: %v", err)
201+
}
202+
203+
// update deploy/role.yaml for the given resource r.
204+
if err := scaffold.UpdateRoleForResource(r, absProjectPath); err != nil {
205+
return fmt.Errorf("failed to update the RBAC manifest for the resource (%v, %v): (%v)",
206+
r.APIVersion, r.Kind, err)
207+
}
208+
return nil
209+
}
210+
211+
// TODO
212+
// Consolidate scaffold func to be used by both "new" and "add api" commands.
213+
func doHelmAPIScaffold() error {
214+
215+
absProjectPath := projutil.MustGetwd()
216+
projectName := filepath.Base(absProjectPath)
217+
cfg := &input.Config{
218+
AbsProjectPath: absProjectPath,
219+
ProjectName: projectName,
220+
}
221+
222+
createOpts := helm.CreateChartOptions{
223+
ResourceAPIVersion: apiFlags.APIVersion,
224+
ResourceKind: apiFlags.Kind,
225+
Chart: apiFlags.HelmChartRef,
226+
Version: apiFlags.HelmChartVersion,
227+
Repo: apiFlags.HelmChartRepo,
228+
}
229+
230+
r, chart, err := helm.CreateChart(cfg.AbsProjectPath, createOpts)
231+
if err != nil {
232+
return fmt.Errorf("failed to create helm chart: %v", err)
233+
}
234+
235+
valuesPath := filepath.Join("<project_dir>", helm.HelmChartsDir, chart.Name(), "values.yaml")
236+
237+
rawValues, err := yaml.Marshal(chart.Values)
238+
if err != nil {
239+
return fmt.Errorf("failed to get raw chart values: %v", err)
240+
}
241+
crSpec := fmt.Sprintf("# Default values copied from %s\n\n%s", valuesPath, rawValues)
242+
243+
// update watch.yaml for the given resource r.
244+
if err := helm.UpdateHelmWatchForResource(r, absProjectPath, chart.Name()); err != nil {
245+
return fmt.Errorf("failed to update the Watch manifest for the resource (%v, %v): (%v)",
246+
r.APIVersion, r.Kind, err)
247+
}
248+
249+
s := &scaffold.Scaffold{}
250+
err = s.Execute(cfg,
251+
&scaffold.CR{
252+
Resource: r,
253+
Spec: crSpec,
254+
},
255+
)
256+
if err != nil {
257+
log.Fatalf("API scaffold failed: %v", err)
258+
}
259+
if err = genutil.GenerateCRDNonGo("", *r, apiFlags.CrdVersion); err != nil {
260+
return err
261+
}
262+
263+
roleScaffold := helm.DefaultRoleScaffold
264+
265+
if k8sCfg, err := config.GetConfig(); err != nil {
266+
log.Warnf("Using default RBAC rules: failed to get Kubernetes config: %s", err)
267+
} else if dc, err := discovery.NewDiscoveryClientForConfig(k8sCfg); err != nil {
268+
log.Warnf("Using default RBAC rules: failed to create Kubernetes discovery client: %s", err)
269+
} else {
270+
roleScaffold = helm.GenerateRoleScaffold(dc, chart)
271+
}
272+
273+
if err = scaffold.MergeRoleForResource(r, absProjectPath, roleScaffold); err != nil {
274+
return fmt.Errorf("failed to merge rules in the RBAC manifest for resource (%v, %v): %v",
275+
r.APIVersion, r.Kind, err)
276+
}
277+
278+
return nil
279+
}
280+
281+
// TODO
282+
// Consolidate scaffold func to be used by both "new" and "add api" commands.
283+
func doGoAPIScaffold() error {
104284

105285
// Create and validate new resource.
106-
r, err := scaffold.NewResource(apiVersion, kind)
286+
r, err := scaffold.NewResource(apiFlags.APIVersion, apiFlags.Kind)
107287
if err != nil {
108288
return err
109289
}
@@ -140,14 +320,14 @@ func apiRun(cmd *cobra.Command, args []string) error {
140320
r.APIVersion, r.Kind, err)
141321
}
142322

143-
if !skipGeneration {
323+
if !apiFlags.SkipGeneration {
144324
// Run k8s codegen for deepcopy
145325
if err := genutil.K8sCodegen(); err != nil {
146326
log.Fatal(err)
147327
}
148328

149329
// Generate a validation spec for the new CRD.
150-
if err := genutil.CRDGen(crdVersion); err != nil {
330+
if err := genutil.CRDGen(apiFlags.CrdVersion); err != nil {
151331
log.Fatal(err)
152332
}
153333
}

cmd/operator-sdk/add/controller.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import (
2323
"github.com/spf13/cobra"
2424
)
2525

26-
var customAPIImport string
26+
var (
27+
customAPIImport string
28+
apiVersion string
29+
kind string
30+
crdVersion string
31+
)
2732

2833
//nolint:lll
2934
func newAddControllerCmd() *cobra.Command {

cmd/operator-sdk/add/crd.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func crdFunc(cmd *cobra.Command, args []string) error {
107107
CRDVersion: crdVersion,
108108
}
109109
if err := crd.Generate(); err != nil {
110-
log.Fatalf("Error generating CRD for %s: %w", resource, err)
110+
log.Fatalf("Error generating CRD for %s: %v", resource, err)
111111
}
112112

113113
// update deploy/role.yaml for the given resource r.

0 commit comments

Comments
 (0)