Skip to content

Commit

Permalink
Function cmd (kyma-project#606)
Browse files Browse the repository at this point in the history
  • Loading branch information
pPrecel authored Oct 14, 2020
1 parent f5e07a6 commit ad16f7d
Show file tree
Hide file tree
Showing 21 changed files with 1,116 additions and 33 deletions.
19 changes: 19 additions & 0 deletions cmd/kyma/apply/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package apply

import (
"github.com/kyma-project/cli/cmd/kyma/apply/function"
"github.com/kyma-project/cli/internal/cli"
"github.com/spf13/cobra"
)

//NewCmd creates a new function command
func NewCmd(o *cli.Options) *cobra.Command {
cmd := &cobra.Command{
Use: "apply",
Short: "Applies local resources to the Kyma cluster.",
Long: "Use this command to apply the resource configuration to the Kyma cluster.",
}

cmd.AddCommand(function.NewCmd(function.NewOptions(o)))
return cmd
}
20 changes: 20 additions & 0 deletions cmd/kyma/apply/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package apply

import (
"github.com/kyma-project/cli/internal/cli"
"github.com/stretchr/testify/require"
"io/ioutil"
"testing"
)

func TestSubcommands(t *testing.T) {
c := NewCmd(&cli.Options{})
c.SetOutput(ioutil.Discard) // not interested in the command's output

// test default flag values
require.NoError(t, c.Execute(), "Command execution must not fail")

sub := c.Commands()

require.Equal(t, 2, len(sub), "Number of created subcommands not as expected")
}
238 changes: 238 additions & 0 deletions cmd/kyma/apply/function/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package function

import (
"context"
"encoding/json"
"fmt"
"github.com/kyma-incubator/hydroform/function/pkg/client"
"github.com/kyma-incubator/hydroform/function/pkg/manager"
"github.com/kyma-incubator/hydroform/function/pkg/operator"
resources "github.com/kyma-incubator/hydroform/function/pkg/resources/unstructured"
"github.com/kyma-incubator/hydroform/function/pkg/workspace"
"github.com/kyma-project/cli/internal/cli"
"github.com/kyma-project/cli/internal/kube"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"os"
)

type command struct {
opts *Options
cli.Command
}

//NewCmd creates a new apply command
func NewCmd(o *Options) *cobra.Command {
c := command{
opts: o,
Command: cli.Command{Options: o.Options},
}
cmd := &cobra.Command{
Use: "function",
Short: "Applies local resources for your Function to the Kyma cluster.",
Long: `Use this command to apply the local sources of your Function's code and dependencies to the Kyma cluster.
Use the flags to specify the desired location for the source files or run the command to validate and print the output resources.`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.Run()
},
}

cmd.Flags().StringVarP(&o.Filename, "filename", "f", "", `Full path to the config file.`)
cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, `Validated list of objects to be created from sources.`)
cmd.Flags().Var(&o.OnError, "onerror", `Flag used to define the Kyma CLI's reaction to an error when applying resources to the cluster. Use one of these options:
- nothing
- purge`)
cmd.Flags().VarP(&o.Output, "output", "o", `Flag used to define the command output format. Use one of these options:
- text
- json
- yaml
- none`)

return cmd
}

func (c *command) Run() error {
if c.opts.Filename == "" {
c.opts.Filename = defaultFilename()
}

file, err := os.Open(c.opts.Filename)
if err != nil {
return err
}

// Load project configuration
var configuration workspace.Cfg
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
return errors.Wrap(err, "Could not decode the configuration file")
}

if c.K8s, err = kube.NewFromConfig("", c.KubeconfigPath); err != nil {
return errors.Wrap(err, "Could not initialize the Kubernetes client. Make sure your kubeconfig is valid")
}
client := c.K8s.Dynamic()

function, err := resources.NewFunction(configuration)
if err != nil {
return err
}

triggers, err := resources.NewTriggers(configuration)
if err != nil {
return err
}

operators := map[operator.Operator][]operator.Operator{
operator.NewGenericOperator(client.Resource(operator.GVKFunction).Namespace(configuration.Namespace), function): {
operator.NewTriggersOperator(client.Resource(operator.GVKTriggers).Namespace(configuration.Namespace), triggers...),
},
}

if configuration.Source.Type == workspace.SourceTypeGit {
gitRepository, err := resources.NewPublicGitRepository(configuration)
if err != nil {
return errors.Wrap(err, "Unable to read the Git repository from the provided configuration")
}
gitOperator := operator.NewGenericOperator(client.Resource(operator.GVRGitRepository).Namespace(configuration.Namespace), gitRepository)
operators[gitOperator] = nil
}

mgr := manager.NewManager(operators)
options := manager.Options{
Callbacks: callbacks(c),
OnError: chooseOnError(c.opts.OnError),
DryRun: c.opts.DryRun,
SetOwnerReferences: true,
}

return mgr.Do(context.Background(), options)
}

const (
operatingFormat = "%s - %s operating... %s"
createdFormat = "%s - %s created %s"
updatedFormat = "%s - %s updated %s"
skippedFormat = "%s - %s unchanged %s"
deletedFormat = "%s - %s deleted %s"
applyFailedFormat = "%s - %s can't be applied %s"
deleteFailedFormat = "%s - %s can't be removed %s"
unknownStatusFormat = "%s - %s can't resolve status %s"
dryRunSuffix = "(dry run)"
yamlFormat = "---\n%s\n"
jsonFormat = "%s\n"
)

func chooseOnError(onErr value) manager.OnError {
if onErr.value == NothingOnError {
return manager.NothingOnError

}
return manager.PurgeOnError
}

func (l *logger) chooseFormat(status client.StatusType) string {
switch status {
case client.StatusTypeCreated:
return createdFormat
case client.StatusTypeUpdated:
return updatedFormat
case client.StatusTypeSkipped:
return skippedFormat
case client.StatusTypeDeleted:
return deletedFormat
case client.StatusTypeApplyFailed:
return applyFailedFormat
case client.StatusTypeDeleteFailed:
return deleteFailedFormat

}
return unknownStatusFormat
}

func (l *logger) formatSuffix() string {
if l.opts.DryRun {
return dryRunSuffix
}
return ""
}

type logger struct {
*command
}

func callbacks(c *command) operator.Callbacks {
logger := logger{c}
return operator.Callbacks{
Pre: []operator.Callback{
logger.pre,
},
Post: []operator.Callback{
logger.post,
},
}
}

func (l *logger) pre(v interface{}, err error) error {
if err != nil {
return err
}
entry, ok := v.(*unstructured.Unstructured)
if !ok {
return errors.New("can't parse interface{} to the Unstructured")
}

switch l.opts.Output.String() {
case TextOutput:
info := fmt.Sprintf(operatingFormat, entry.GetKind(), entry.GetName(), l.formatSuffix())
l.NewStep(info)
return nil
case JSONOutput:
if err != nil {
return err
}
unstructured.RemoveNestedField(entry.Object, "metadata", "ownerReferences")
unstructured.RemoveNestedField(entry.Object, "metadata", "labels", "ownerID")
bytes, marshalError := json.MarshalIndent(entry.Object, "", " ")
if marshalError != nil {
return marshalError
}
fmt.Printf(jsonFormat, string(bytes))
return nil
case YAMLOutput:
if err != nil {
return err
}
unstructured.RemoveNestedField(entry.Object, "metadata", "ownerReferences")
unstructured.RemoveNestedField(entry.Object, "metadata", "labels", "ownerID")
bytes, marshalError := yaml.Marshal(entry.Object)
if marshalError != nil {
return marshalError
}
fmt.Printf(yamlFormat, string(bytes))
return nil
case NoneOutput:
return err
}
return err
}

func (l *logger) post(v interface{}, err error) error {
entry, ok := v.(client.PostStatusEntry)
if !ok {
return errors.New("can't parse interface{} to StatusEntry interface")
}

if l.opts.Output.String() != TextOutput {
return err
}
format := l.chooseFormat(entry.StatusType)
info := fmt.Sprintf(format, entry.GetKind(), entry.GetName(), l.formatSuffix())
step := l.CurrentStep
if err != nil {
step.Failuref(info)
}
step.Successf(info)
return err
}
40 changes: 40 additions & 0 deletions cmd/kyma/apply/function/function_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package function

import (
"github.com/kyma-project/cli/internal/cli"
"github.com/stretchr/testify/require"
"testing"
)

// TestUpgradeFlags ensures that the provided command flags are stored in the options.
func TestUpgradeFlags(t *testing.T) {
o := NewOptions(&cli.Options{})
c := NewCmd(o)

// test default flag values
require.Equal(t, false, o.DryRun, "Default value for the --dry-run flag not as expected.")
require.Equal(t, "", o.Filename, "Default value for the --filename flag not as expected.")
require.Equal(t, "nothing", o.OnError.String(), "The parsed value for the --onerror flag not as expected.")
require.Equal(t, "text", o.Output.String(), "The parsed value for the --output flag not as expected.")

// test passing flags
err := c.ParseFlags([]string{
"--filename", "/fakepath/config.yaml",
"--dry-run", "true",
"--onerror", "purge",
"--output", "json",
})
require.NoError(t, err, "Parsing flags should not return an error")
require.Equal(t, "/fakepath/config.yaml", o.Filename, "The parsed value for the --filename flag not as expected.")
require.Equal(t, true, o.DryRun, "The parsed value for the --dry-run flag not as expected.")
require.Equal(t, "purge", o.OnError.String(), "The parsed value for the --onerror flag not as expected.")
require.Equal(t, "json", o.Output.String(), "The parsed value for the --output flag not as expected.")

err = c.ParseFlags([]string{
"-f", "/config.yaml",
"-o", "yaml",
})
require.NoError(t, err, "Parsing flags should not return an error")
require.Equal(t, "/config.yaml", o.Filename, "The parsed value for the -f flag not as expected.")
require.Equal(t, "yaml", o.Output.String(), "The parsed value for the -o flag not as expected.")
}
Loading

0 comments on commit ad16f7d

Please sign in to comment.