Skip to content

Commit

Permalink
CLI: scaffold API
Browse files Browse the repository at this point in the history
  • Loading branch information
yuwenma committed Jul 11, 2024
1 parent 1ce0259 commit 3591783
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/codegen"
"github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/options"
"github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/protoapi"
"github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/scaffold"
"google.golang.org/protobuf/reflect/protoreflect"
"k8s.io/apimachinery/pkg/runtime/schema"

Expand All @@ -32,10 +33,13 @@ type GenerateCRDOptions struct {
*options.GenerateOptions

OutputAPIDirectory string
KindNames []string
}

func (o *GenerateCRDOptions) BindFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.OutputAPIDirectory, "output-api", o.OutputAPIDirectory, "base directory for writing APIs")
// TODO: 1. only add API and mapper needed by this Kind 2. validate the kind should be in camel case
cmd.Flags().StringSliceVarP(&o.KindNames, "kinds", "k", nil, "the GCP resource names under the GCP service. This will be used as the ConfigConnecter resource Kind names and should be in camel case")
}

func BuildCommand(baseOptions *options.GenerateOptions) *cobra.Command {
Expand All @@ -62,10 +66,22 @@ func BuildCommand(baseOptions *options.GenerateOptions) *cobra.Command {

func RunGenerateCRD(ctx context.Context, o *GenerateCRDOptions) error {
if o.ServiceName == "" {
return fmt.Errorf("ServiceName is required")
return fmt.Errorf("`service` is required")
}
if o.GenerateOptions.ProtoSourcePath == "" {
return fmt.Errorf("ProtoSourcePath is required")
return fmt.Errorf("`proto-source-path` is required")
}

if o.KindNames != nil {
errMsg := []string{}
for _, k := range o.KindNames {
if strings.ToLower(k) == k {
errMsg = append(errMsg, "%q in `kinds` should be CamelCase.")
}
}
if len(errMsg) > 0 {
return fmt.Errorf(strings.Join(errMsg, "\n"))
}
}

gv, err := schema.ParseGroupVersion(o.APIVersion)
Expand All @@ -78,6 +94,8 @@ func RunGenerateCRD(ctx context.Context, o *GenerateCRDOptions) error {
return fmt.Errorf("loading proto: %w", err)
}

goPackage := ""
protoPackagePath := ""
pathForMessage := func(msg protoreflect.MessageDescriptor) (string, bool) {
fullName := string(msg.FullName())
if strings.HasSuffix(fullName, "Request") {
Expand All @@ -91,12 +109,13 @@ func RunGenerateCRD(ctx context.Context, o *GenerateCRDOptions) error {
return "", false
}

protoPackagePath := string(msg.ParentFile().Package())
protoPackagePath = string(msg.ParentFile().Package())
protoPackagePath = strings.TrimPrefix(protoPackagePath, "google.")
protoPackagePath = strings.TrimPrefix(protoPackagePath, "cloud.")
protoPackagePath = strings.TrimSuffix(protoPackagePath, ".v1")
protoPackagePath = strings.TrimSuffix(protoPackagePath, ".v1beta1")
goPackage := "apis/" + strings.Join(strings.Split(protoPackagePath, "."), "/") + "/" + gv.Version
protoPackagePath = strings.Join(strings.Split(protoPackagePath, "."), "/")
goPackage = "apis/" + protoPackagePath + "/" + gv.Version

return goPackage, true
}
Expand All @@ -109,6 +128,33 @@ func RunGenerateCRD(ctx context.Context, o *GenerateCRDOptions) error {
return err
}

if o.KindNames != nil {
scaffolder := &scaffold.APIScaffolder{
BaseDir: o.OutputAPIDirectory,
GoPackage: goPackage,
Service: protoPackagePath,
Version: gv.Version,
PackageProtoTag: o.ServiceName,
}
if scaffolder.DocFileNotExist() {
if err := scaffolder.AddDocFile(); err != nil {
return fmt.Errorf("add doc.go file: %w", err)
}
}
if scaffolder.GroupVersionFileNotExist() {
if err := scaffolder.AddGroupVersionFile(); err != nil {
return fmt.Errorf("add groupversion_info.go file: %w", err)
}
}
for _, kind := range o.KindNames {
if !scaffolder.TypeFileNotExist(kind) {
fmt.Printf("file %s already exist, skip", scaffolder.GetTypeFile(kind))
continue
}
if err := scaffolder.AddTypeFile(kind); err != nil {
return fmt.Errorf("add type file %s: %w", scaffolder.GetTypeFile(kind), err)
}
}
}
return nil

}
162 changes: 162 additions & 0 deletions dev/tools/controllerbuilder/scaffold/apis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 scaffold

import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"unicode"
"unicode/utf8"

"github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/template/apis"
"github.com/fatih/color"
)

type APIScaffolder struct {
BaseDir string
GoPackage string
Service string
Version string
PackageProtoTag string
}

func (a *APIScaffolder) TypeFileNotExist(kind string) bool {
typeFilePath := a.GetTypeFile(kind)
_, err := os.Stat(typeFilePath)
if err == nil {
return false
}
return errors.Is(err, os.ErrNotExist)
}

func (a *APIScaffolder) GetTypeFile(kind string) string {
fileName := strings.ToLower(kind) + "_types.go"
return filepath.Join(a.BaseDir, a.GoPackage, fileName)
}

func (a *APIScaffolder) AddTypeFile(kind string) error {
gcpResource := kind
if !strings.HasPrefix(strings.ToLower(kind), a.Service) {
s, size := utf8.DecodeRuneInString(a.Service)
uc := unicode.ToUpper(s)
kind = string(uc) + a.Service[size:] + kind
}
typeFilePath := a.GetTypeFile(kind)
cArgs := &apis.APIArgs{
Service: a.Service,
Version: a.Version,
Kind: kind,
PackageProtoTag: a.PackageProtoTag,
KindProtoTag: a.PackageProtoTag + "." + gcpResource,
}
return scaffoldTypeFile(typeFilePath, cArgs)
}

func scaffoldTypeFile(path string, cArgs *apis.APIArgs) error {
tmpl, err := template.New(cArgs.Kind).Parse(apis.TypesTemplate)
if err != nil {
return fmt.Errorf("parse %s_types.go template: %w", strings.ToLower(cArgs.Kind), err)
}
// Apply the APIArgs args to the template
out := &bytes.Buffer{}
if err := tmpl.Execute(out, cArgs); err != nil {
return err
}
// Write the generated <kind>_types.go
if err := WriteToFile(path, out.Bytes()); err != nil {
return err
}
// Format and adjust the go imports in the generated files.
if err := FormatImports(path, out.Bytes()); err != nil {
return err
}
color.HiGreen("New API file added %s\nPlease EDIT it!\n", path)
return nil
}

func (a *APIScaffolder) GroupVersionFileNotExist() bool {
docFilePath := filepath.Join(a.BaseDir, a.GoPackage, "groupversion_info.go")
_, err := os.Stat(docFilePath)
if err == nil {
return false
}
return errors.Is(err, os.ErrNotExist)
}

func (a *APIScaffolder) AddGroupVersionFile() error {
docFilePath := filepath.Join(a.BaseDir, a.GoPackage, "groupversion_info.go")
cArgs := &apis.APIArgs{
Service: a.Service,
Version: a.Version,
PackageProtoTag: a.PackageProtoTag,
}
return scaffoldGropuVersionFile(docFilePath, cArgs)
}

func (a *APIScaffolder) DocFileNotExist() bool {
docFilePath := filepath.Join(a.BaseDir, a.GoPackage, "doc.go")
_, err := os.Stat(docFilePath)
if err == nil {
return false
}
return errors.Is(err, os.ErrNotExist)
}

func (a *APIScaffolder) AddDocFile() error {
docFilePath := filepath.Join(a.BaseDir, a.GoPackage, "doc.go")
cArgs := &apis.APIArgs{
Service: a.Service,
Version: a.Version,
PackageProtoTag: a.PackageProtoTag,
}
return scaffoldDocFile(docFilePath, cArgs)
}

func scaffoldDocFile(path string, cArgs *apis.APIArgs) error {
tmpl, err := template.New(cArgs.Service).Parse(apis.DocTemplate)
if err != nil {
return fmt.Errorf("parse doc.go template: %w", err)
}
out := &bytes.Buffer{}
if err := tmpl.Execute(out, cArgs); err != nil {
return err
}
if err := WriteToFile(path, out.Bytes()); err != nil {
return err
}
color.HiGreen("New file added %s!\n", path)
return nil
}

func scaffoldGropuVersionFile(path string, cArgs *apis.APIArgs) error {
tmpl, err := template.New(cArgs.Service).Parse(apis.GroupVersionInfoTemplate)
if err != nil {
return fmt.Errorf("parse groupversion_info.go template: %w", err)
}
out := &bytes.Buffer{}
if err := tmpl.Execute(out, cArgs); err != nil {
return err
}
if err := WriteToFile(path, out.Bytes()); err != nil {
return err
}
color.HiGreen("New file added %s!\n", path)
return nil
}
13 changes: 8 additions & 5 deletions dev/tools/controllerbuilder/scaffold/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,17 @@ func FormatImports(path string, out []byte) error {
}

func WriteToFile(path string, out []byte) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("create controller file %s: %w", path, err)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("failed to create directory %q: %w", filepath.Dir(path), err)
}
err = os.WriteFile(path, out, 0644)
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("write controller file %s: %w", path, err)
return err
}
defer f.Close()
_, err = f.Write(out)
if err != nil {
return fmt.Errorf("write file %s: %w", path, err)
}
return nil
}
22 changes: 22 additions & 0 deletions dev/tools/controllerbuilder/template/apis/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package apis

const DocTemplate = `
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.
{{- if .PackageProtoTag }}
// +kcc:proto={{ .PackageProtoTag }}
{{- end }}
package {{ .Version }}
`
37 changes: 37 additions & 0 deletions dev/tools/controllerbuilder/template/apis/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package apis

const GroupVersionInfoTemplate = `
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.
// +kubebuilder:object:generate=true
// +groupName={{.Service}}.cnrm.cloud.google.com
package {{ .Version }}
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "{{.Service}}.cnrm.cloud.google.com", Version: "{{.Version}}"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
`
Loading

0 comments on commit 3591783

Please sign in to comment.