Skip to content

Commit

Permalink
feat: adjust generate cmd behavior according to latest discussion (#1034
Browse files Browse the repository at this point in the history
)
  • Loading branch information
adohe committed Apr 14, 2024
1 parent d967f04 commit 41181fb
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 56 deletions.
2 changes: 1 addition & 1 deletion pkg/cmd/apply/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (o *Options) Run() error {
if len(o.IntentFile) != 0 {
spec, err = generate.SpecFromFile(o.IntentFile)
} else {
spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, true)
spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, nil, true)
}
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/apply/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func mockGenerateSpecWithSpinner() {
project *apiv1.Project,
stack *apiv1.Stack,
workspace *apiv1.Workspace,
parameters map[string]string,
noStyle bool,
) (*apiv1.Spec, error) {
return &apiv1.Spec{Resources: []apiv1.Resource{sa1, sa2, sa3}}, nil
Expand Down
2 changes: 0 additions & 2 deletions pkg/cmd/build/builders/doc.go

This file was deleted.

145 changes: 97 additions & 48 deletions pkg/cmd/generate/generate.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,75 @@
// Copyright 2024 KusionStack Authors
//
// 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 generate

import (
"bytes"
"fmt"
"io"
"os"
"strings"

"github.com/pterm/pterm"
"github.com/spf13/cobra"
yamlv3 "gopkg.in/yaml.v3"
"k8s.io/cli-runtime/pkg/genericiooptions"
"kcl-lang.io/kpm/pkg/api"

v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
"kusionstack.io/kusion/pkg/backend"
"kusionstack.io/kusion/pkg/cmd/generate/generator"
"kusionstack.io/kusion/pkg/cmd/generate/run"
"kusionstack.io/kusion/pkg/cmd/meta"
cmdutil "kusionstack.io/kusion/pkg/cmd/util"
"kusionstack.io/kusion/pkg/engine/spec"
"kusionstack.io/kusion/pkg/util/i18n"
"kusionstack.io/kusion/pkg/util/pretty"
)

var (
generateLong = i18n.T(`
Generate versioned Spec of target Stack.
The command must be executed in a Stack or by specifying a Stack directory with the -w flag.`)
This command generates Spec resources with given values, then write the resulting Spec resources to specific output file or stdout.
The nearest parent folder containing a stack.yaml file is loaded from the project in the current directory.`)

generateExample = i18n.T(`
# Generate spec with working directory
kusion generate -w /path/to/stack
# Generate and write Spec resources to specific output file
kusion generate -o /tmp/spec.yaml
# Generate spec with custom workspace
kusion generate -w /path/to/stack --workspace dev
# Generate spec with custom backend
kusion generate -w /path/to/stack --backend oss`)
kusion generate -o /tmp/spec.yaml --workspace dev`)
)

// GenerateFlags directly reflect the information that CLI is gathering via flags. They will be converted to
// GenerateOptions, which reflect the runtime requirements for the command.
//
// This structure reduces the transformation to wiring and makes the logic itself easy to unit test.
type GenerateFlags struct {
WorkDir string
MetaFlags *meta.MetaFlags

Output string
Values []string

genericiooptions.IOStreams
}

// GenerateOptions defines flags and other configuration parameters for the `generate` command.
type GenerateOptions struct {
*meta.MetaOptions

WorkDir string
Output string
Values []string

SpecStorage spec.Storage
genericiooptions.IOStreams
}

// NewGenerateFlags returns a default GenerateFlags
Expand All @@ -73,8 +85,8 @@ func NewCmdGenerate(ioStreams genericiooptions.IOStreams) *cobra.Command {
flags := NewGenerateFlags(ioStreams)

cmd := &cobra.Command{
Use: "generate (-w DIRECTORY)",
Short: "Generate versioned Spec of target Stack",
Use: "generate",
Short: "Generate and print the resulting Spec resources of target Stack",
Long: generateLong,
Example: generateExample,
RunE: func(cmd *cobra.Command, args []string) (err error) {
Expand All @@ -97,38 +109,24 @@ func (flags *GenerateFlags) AddFlags(cmd *cobra.Command) {
// bind flag structs
flags.MetaFlags.AddFlags(cmd)

cmd.Flags().StringVarP(&flags.WorkDir, "workdir", "w", flags.WorkDir, i18n.T("The working directory for generate (default is current dir where executed)."))
cmd.Flags().StringVarP(&flags.Output, "output", "o", flags.Output, i18n.T("File to write generated Spec resources to"))
cmd.Flags().StringArrayVar(&flags.Values, "set", []string{}, i18n.T("Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)"))
}

// ToOptions converts from CLI inputs to runtime inputs.
func (flags *GenerateFlags) ToOptions() (*GenerateOptions, error) {
// If working directory not specified, use current dir where executed
workDir := flags.WorkDir
if len(workDir) == 0 {
workDir, _ = os.Getwd()
}

// Convert meta options
metaOptions, err := flags.MetaFlags.ToOptions()
if err != nil {
return nil, err
}

// Get target spec storage
specStorage, err := backend.NewSpecStorage(
*flags.MetaFlags.Backend,
metaOptions.RefProject.Name,
metaOptions.RefStack.Name,
metaOptions.RefWorkspace.Name,
)
if err != nil {
return nil, err
}

o := &GenerateOptions{
WorkDir: workDir,
MetaOptions: metaOptions,
SpecStorage: specStorage,
Output: flags.Output,
Values: flags.Values,

IOStreams: flags.IOStreams,
}

return o, nil
Expand All @@ -140,20 +138,51 @@ func (o *GenerateOptions) Validate(cmd *cobra.Command, args []string) error {
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
}

for _, value := range o.Values {
if parts := strings.SplitN(value, "=", 2); len(parts) != 2 {
return cmdutil.UsageErrorf(cmd, "value %s is invalid format", value)
}
}

return nil
}

// Run executes the `generate` command.
func (o *GenerateOptions) Run() error {
versionedSpec, err := GenerateSpecWithSpinner(o.RefProject, o.RefStack, o.RefWorkspace, true)
// build parameters
parameters := o.buildParameters()

// call default generator to generate Spec
spec, err := GenerateSpecWithSpinner(o.RefProject, o.RefStack, o.RefWorkspace, parameters, true)
if err != nil {
return err
}
return o.SpecStorage.Apply(versionedSpec)

// write Spec to output file or a writer
return write(spec, o.Output, o.Out)
}

// GenerateSpecWithSpinner calls generator to generate versioned Spec. Add a method wrapper for testing purposes.
func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1.Workspace, noStyle bool) (*v1.Spec, error) {
// buildParameters builds parameters with given values.
func (o *GenerateOptions) buildParameters() map[string]string {
parameters := make(map[string]string)

for _, value := range o.Values {
parts := strings.SplitN(value, "=", 2)
parameters[parts[0]] = parts[1]
}

return parameters
}

// GenerateSpecWithSpinner calls generator to generate versioned Spec.
// Add a method wrapper for testing purposes.
func GenerateSpecWithSpinner(
project *v1.Project,
stack *v1.Stack,
workspace *v1.Workspace,
parameters map[string]string,
noStyle bool,
) (*v1.Spec, error) {
// Construct generator instance
defaultGenerator := &generator.DefaultGenerator{
Project: project,
Expand All @@ -162,12 +191,6 @@ func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1
Runner: &run.KPMRunner{},
}

kclPkg, err := api.GetKclPackage(stack.Path)
if err != nil {
return nil, err
}
defaultGenerator.KclPkg = kclPkg

var sp *pterm.SpinnerPrinter
if noStyle {
fmt.Printf("Generating Spec in the Stack %s...\n", stack.Name)
Expand All @@ -179,7 +202,7 @@ func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1
// style means color and prompt here. Currently, sp will be nil only when o.NoStyle is true
style := !noStyle && sp != nil

versionedSpec, err := defaultGenerator.Generate(stack.Path, nil)
versionedSpec, err := defaultGenerator.Generate(stack.Path, parameters)
if err != nil {
if style {
sp.Fail()
Expand Down Expand Up @@ -216,3 +239,29 @@ func SpecFromFile(filePath string) (*v1.Spec, error) {
}
return i, nil
}

// write writes Spec resources to a file or a writer.
func write(spec *v1.Spec, output string, out io.Writer) error {
specStr, err := yamlv3.Marshal(spec)
if err != nil {
return err
}

switch {
case output == "":
_, err := fmt.Fprintln(out, specStr)
return err
default:
return dumpToFile(string(specStr), output)
}
}

func dumpToFile(specStr string, filepath string) error {
f, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("opening file for writing Spec: %w", err)
}
defer f.Close()
_, err = f.WriteString(specStr + "\n")
return err
}
27 changes: 23 additions & 4 deletions pkg/cmd/generate/generator/generator.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2024 KusionStack Authors
//
// 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 generator

import (
Expand All @@ -21,7 +35,7 @@ import (
)

// Generator is an interface for things that can generate versioned Spec from
// configuration code under current working directory and given input parameters.
// configuration code under current working directory with given input parameters.
type Generator interface {
// Generate creates versioned Intent given working directory and set of parameters
Generate(workDir string, params map[string]string) (*v1.Spec, error)
Expand All @@ -32,8 +46,8 @@ type DefaultGenerator struct {
Project *v1.Project
Stack *v1.Stack
Workspace *v1.Workspace
Runner run.CodeRunner
KclPkg *api.KclPackage

Runner run.CodeRunner
}

// Generate versioned Spec with target code runner.
Expand Down Expand Up @@ -61,11 +75,16 @@ func (g *DefaultGenerator) Generate(workDir string, params map[string]string) (*
return nil, err
}

kclPkg, err := api.GetKclPackage(g.Stack.Path)
if err != nil {
return nil, err
}

builder := &builders.AppsConfigBuilder{
Workspace: g.Workspace,
Apps: apps,
}
return builder.Build(g.KclPkg, g.Project, g.Stack)
return builder.Build(kclPkg, g.Project, g.Stack)
}

// copyDependentModules copies dependent Kusion modules' generators to destination.
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/preview/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (o *Options) Run() error {
if len(o.IntentFile) != 0 {
spec, err = generate.SpecFromFile(o.IntentFile)
} else {
spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, true)
spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, nil, true)
}
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/preview/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ func mockGenerateIntentWithSpinner() {
project *apiv1.Project,
stack *apiv1.Stack,
workspace *apiv1.Workspace,
parameters map[string]string,
noStyle bool,
) (*apiv1.Spec, error) {
return &apiv1.Spec{Resources: []apiv1.Resource{sa1, sa2, sa3}}, nil
Expand Down

0 comments on commit 41181fb

Please sign in to comment.