Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions commands/operator-sdk/cmd/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
)

const (
// http://tldp.org/LDP/abs/html/exitcodes.html
ExitSuccess = iota
ExitError
// ExitBadArgs is the exit error code for bad arguments.
ExitBadArgs = 128
)
Expand Down
22 changes: 19 additions & 3 deletions commands/operator-sdk/cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"fmt"

"github.com/coreos/operator-sdk/pkg/generator"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave space between non operator-sdk imports.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!


"github.com/spf13/cobra"
)

Expand All @@ -27,8 +29,9 @@ var newCmd = &cobra.Command{
}

var (
apiGroup string
kind string
apiGroup string
kind string
projectName string
)

func init() {
Expand All @@ -44,8 +47,21 @@ func newFunc(cmd *cobra.Command, args []string) {
ExitWithError(ExitBadArgs, fmt.Errorf("new command needs 1 argument."))
}
parse(args)
// TODO: add generation logic.
verifyFlags()
g := generator.NewGenerator(apiGroup, kind, projectName)
err := g.Render()
if err != nil {
ExitWithError(ExitError, fmt.Errorf("failed to create project %v: %v", projectName, err))
}
}

func parse(args []string) {
projectName = args[0]
if len(projectName) == 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("project-name must not be empty"))
}
}

func verifyFlags() {
// TODO: verify format of input flags.
}
127 changes: 97 additions & 30 deletions pkg/generator/generator.go
Original file line number Diff line number Diff line change
@@ -1,61 +1,128 @@
package generator

import (
"errors"
"os"
"path/filepath"
"strings"
)

const defaultFileMode = 0750
const (
defaultFileMode = 0750
cmdDir = "cmd"
deployDir = "deploy"
configDir = "config"
tmpDir = "tmp"
buildDir = tmpDir + "/build"
codegenDir = tmpDir + "/codegen"
pkgDir = "pkg"
apisDir = pkgDir + "/apis"
stubDir = pkgDir + "/stub"
)

type Generator struct {
apiGroup string
kind string
// projectName is name of the new operator application
// and is also the name of the base directory.
projectName string
}

// NewGenerator creates a new scaffold Generator.
func NewGenerator(apiGroup, kind, projectName string) *Generator {
return &Generator{apiGroup: apiGroup, kind: kind, projectName: projectName}
}

// Render generates the default project structure:
//
// ├── <projectName>
// │ ├── cmd
// │ │ └── <projectName>
// │ ├── config
// │ ├── deploy
// │ ├── pkg
// │ │ ├── apis
// │ │ │ └── <api-dir-name> // computed from apiDirName(apiGroup).
// │ │ │ └── <api-version> // computed from apiVersion(apiGroup).
// │ │ └── stub
// │ └── tmp
// │ ├── build
// │ └── codegen
func (g *Generator) Render() error {
gopath, ok := os.LookupEnv("GOPATH")
if !ok {
return errors.New("GOPATH must be set")
if err := g.renderCmd(); err != nil {
return err
}
projDir, ok := os.LookupEnv("PROJECT") // github.com/coreos/play
if !ok {
return errors.New("PROJECT must be set")
if err := g.renderConfig(); err != nil {
return err
}
if err := g.renderDeploy(); err != nil {
return err
}
apiGroup, ok := os.LookupEnv("APIGROUP") // play.coreos.com/v1alpha1
if !ok {
return errors.New("PROJECT must be set")
if err := g.renderPkg(); err != nil {
return err
}
return g.renderTmp()
}

projDir = filepath.Join(gopath, "src", projDir)
err := os.MkdirAll(projDir, defaultFileMode)
if err != nil {
func (g *Generator) renderCmd() error {
if err := os.MkdirAll(filepath.Join(g.projectName, cmdDir, g.projectName), defaultFileMode); err != nil {
return err
}
// TODO render files.
return nil
}

programName := func() string {
splits := strings.Split(projDir, "/")
return splits[len(splits)-1]
}()
err = os.MkdirAll(filepath.Join(projDir, "cmd", programName), defaultFileMode)
if err != nil {
func (g *Generator) renderConfig() error {
if err := os.MkdirAll(filepath.Join(g.projectName, configDir), defaultFileMode); err != nil {
return err
}
// TODO render files.
return nil
}

// pkg/apis/
groupName, apiVersion := func() (string, string) {
splits := strings.Split(apiGroup, "/")
return strings.Split(splits[0], ".")[0], splits[1]
}()
err = os.MkdirAll(filepath.Join(projDir, "pkg/apis", groupName, apiVersion), defaultFileMode)
if err != nil {
func (g *Generator) renderDeploy() error {
if err := os.MkdirAll(filepath.Join(g.projectName, deployDir), defaultFileMode); err != nil {
return err
}
// TODO render files.
return nil
}

// pkg/controller/
err = os.MkdirAll(filepath.Join(projDir, "pkg/controller"), defaultFileMode)
if err != nil {
func (g *Generator) renderTmp() error {
if err := os.MkdirAll(filepath.Join(g.projectName, buildDir), defaultFileMode); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(g.projectName, codegenDir), defaultFileMode); err != nil {
return err
}
// TODO render files.
return nil
}

func (g *Generator) renderPkg() error {
if err := os.MkdirAll(filepath.Join(g.projectName, apisDir, apiDirName(g.apiGroup), apiVersion(g.apiGroup)), defaultFileMode); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(g.projectName, stubDir), defaultFileMode); err != nil {
return err
}
// TODO render files.
return nil
}

// apiVersion extracts api version from the given apiGroup.
func apiVersion(apiGroup string) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the APIVersion string is made up of group/version, e.g the APIVersion string app.example.com/v1alpha1 has group app.example.com and version v1alpha1.

So probably modify this func signature to

// version extracts version from the given apiVersion
func version(apiVersion string) string {

Similarly use the name apiVersion instead of apiGroup as the argument for the functions below as well.
The term apiGroup is not correct in this context since we're actually passing in the apiVersion string of the format "group/version".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also just FYI this format is only for CRDs and apis under the rest path /apis.
The core API group doesn't use this format. e.g for pods apiVersion=v1
https://kubernetes.io/docs/reference/api-overview/#api-groups

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "v1" is actually legacy now. All new APIs will follow the group/version format including CRD and UAS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look of the api-group definition at here https://kubernetes.io/docs/reference/api-overview/#api-groups.

The API group is specified in a REST path and in the apiVersion field of a serialized object.

It seems like api-group is equivalent apiVersion. In our case, the operator-sdk has a flag called --api-group. do we want to use that or --api-version instead?

the distinction between apiVersion and apiGroup is not very clear.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with what @hasbro17 said. Let's change based on his suggestion.

return strings.Split(apiGroup, "/")[1]
}

// groupName extracts the group name from the givem apiGroup.
func groupName(apiGroup string) string {
return strings.Split(apiGroup, "/")[0]
}

// apiDirName extracts the name of api directory under ../apis/ from the apiGroup.
// it uses the first word separated with "." of the groupName as the api directory name.
// for example,
// apiDirName("app.example.com/v1alpha1") => "app".
func apiDirName(apiGroup string) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do some error checking here on the format of the string, like if the user enters app/v1alpha.
Same for groupName() and apiVersion() functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hasbro17 I have a todo in https://github.com/coreos/operator-sdk/blob/master/commands/operator-sdk/cmd/new.go#L65 for that.

I think it is better to check input on top lvl so that the lower lvl code doesn't have to worry about it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fanminshi Can you follow this comment in a new PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hongchaodeng it is a TODO inside cmd/new.go.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this as part of the Action Items.

return strings.Split(groupName(apiGroup), ".")[0]
}