diff --git a/cmd/ci/config.go b/cmd/ci/config.go index 568ee80cab..86b0a908ea 100644 --- a/cmd/ci/config.go +++ b/cmd/ci/config.go @@ -9,40 +9,71 @@ import ( const ( ConfigCIFeatureFlag = "FUNC_ENABLE_CI_CONFIG" - DefaultGithubWorkflowDir = ".github/workflows" - DefaultGithubWorkflowFilename = "func-deploy.yaml" + PathFlag = "path" - PathOption = "path" + DefaultGitHubWorkflowDir = ".github/workflows" + DefaultGitHubWorkflowFilename = "func-deploy.yaml" - WorkflowNameOption = "workflow-name" + WorkflowNameFlag = "workflow-name" DefaultWorkflowName = "Func Deploy" + + BranchFlag = "branch" + DefaultBranch = "main" + + KubeconfigSecretNameFlag = "kubeconfig-secret-name" + DefaultKubeconfigSecretName = "KUBECONFIG" + + RegistryUrlVariableNameFlag = "registry-url-variable-name" + DefaultRegistryUrlVariableName = "REGISTRY_URL" ) -// CIConfig readonly CI configuration +// CIConfig readonly configuration type CIConfig struct { githubWorkflowDir, githubWorkflowFilename, path, - workflowName string + workflowName, + branch, + kubeconfigSecret, + registryUrlVar string } -func NewCiGithubConfig() CIConfig { +func NewCIGitHubConfig() CIConfig { return CIConfig{ - githubWorkflowDir: DefaultGithubWorkflowDir, - githubWorkflowFilename: DefaultGithubWorkflowFilename, - path: viper.GetString(PathOption), - workflowName: viper.GetString(WorkflowNameOption), + githubWorkflowDir: DefaultGitHubWorkflowDir, + githubWorkflowFilename: DefaultGitHubWorkflowFilename, + path: viper.GetString(PathFlag), + workflowName: viper.GetString(WorkflowNameFlag), + branch: viper.GetString(BranchFlag), + kubeconfigSecret: viper.GetString(KubeconfigSecretNameFlag), + registryUrlVar: viper.GetString(RegistryUrlVariableNameFlag), } } -func (cc *CIConfig) FnGithubWorkflowDir(fnRoot string) string { +func (cc *CIConfig) FnGitHubWorkflowDir(fnRoot string) string { return filepath.Join(fnRoot, cc.githubWorkflowDir) } -func (cc *CIConfig) FnGithubWorkflowFilepath(fnRoot string) string { - return filepath.Join(cc.FnGithubWorkflowDir(fnRoot), cc.githubWorkflowFilename) +func (cc *CIConfig) FnGitHubWorkflowFilepath(fnRoot string) string { + return filepath.Join(cc.FnGitHubWorkflowDir(fnRoot), cc.githubWorkflowFilename) } func (cc *CIConfig) Path() string { return cc.path } + +func (cc *CIConfig) WorkflowName() string { + return cc.workflowName +} + +func (cc *CIConfig) Branch() string { + return cc.branch +} + +func (cc *CIConfig) KubeconfigSecret() string { + return cc.kubeconfigSecret +} + +func (cc *CIConfig) RegistryUrlVar() string { + return cc.registryUrlVar +} diff --git a/cmd/ci/workflow.go b/cmd/ci/workflow.go new file mode 100644 index 0000000000..30ea3a3934 --- /dev/null +++ b/cmd/ci/workflow.go @@ -0,0 +1,134 @@ +package ci + +import ( + "bytes" + "fmt" + + "gopkg.in/yaml.v3" +) + +type githubWorkflow struct { + Name string `yaml:"name"` + On workflowTriggers `yaml:"on"` + Jobs map[string]job `yaml:"jobs"` +} + +type workflowTriggers struct { + Push *pushTrigger `yaml:"push,omitempty"` +} + +type pushTrigger struct { + Branches []string `yaml:"branches,omitempty"` +} + +type job struct { + RunsOn string `yaml:"runs-on"` + Steps []step `yaml:"steps"` +} + +type step struct { + Name string `yaml:"name,omitempty"` + Uses string `yaml:"uses,omitempty"` + Run string `yaml:"run,omitempty"` + With map[string]string `yaml:"with,omitempty"` +} + +func NewGitHubWorkflow(conf CIConfig) *githubWorkflow { + runsOn := "ubuntu-latest" + + pushTrigger := newPushTrigger(conf.Branch()) + + var steps []step + checkoutCode := newStep("Checkout code"). + withUses("actions/checkout@v4") + steps = append(steps, *checkoutCode) + + setupK8Context := newStep("Setup Kubernetes context"). + withUses("azure/k8s-set-context@v4"). + withActionConfig("method", "kubeconfig"). + withActionConfig("kubeconfig", newSecret(conf.KubeconfigSecret())) + steps = append(steps, *setupK8Context) + + installFuncCli := newStep("Install func cli"). + withUses("gauron99/knative-func-action@main"). + withActionConfig("version", "knative-v1.19.1"). + withActionConfig("name", "func") + steps = append(steps, *installFuncCli) + + deployFunc := newStep("Deploy function"). + withRun("func deploy --registry=" + newVariable(conf.RegistryUrlVar()) + " -v") + steps = append(steps, *deployFunc) + + return &githubWorkflow{ + Name: conf.WorkflowName(), + On: pushTrigger, + Jobs: map[string]job{ + "deploy": { + RunsOn: runsOn, + Steps: steps, + }, + }, + } +} + +func newPushTrigger(branch string) workflowTriggers { + result := workflowTriggers{ + Push: &pushTrigger{Branches: []string{branch}}, + } + + return result +} + +func newStep(name string) *step { + return &step{Name: name} +} + +func (s *step) withUses(u string) *step { + s.Uses = u + return s +} + +func (s *step) withRun(r string) *step { + s.Run = r + return s +} + +func (s *step) withActionConfig(key, value string) *step { + if s.With == nil { + s.With = make(map[string]string) + } + + s.With[key] = value + + return s +} + +func newSecret(key string) string { + return fmt.Sprintf("${{ secrets.%s }}", key) +} + +func newVariable(key string) string { + return fmt.Sprintf("${{ vars.%s }}", key) +} + +func (gw *githubWorkflow) Export(path string, w WorkflowWriter) error { + raw, err := gw.toYaml() + if err != nil { + return err + } + + return w.Write(path, raw) +} + +func (gw *githubWorkflow) toYaml() ([]byte, error) { + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + + if err := encoder.Encode(gw); err != nil { + return nil, err + } + encoder.Close() + + return buf.Bytes(), nil +} diff --git a/cmd/ci/workflow_test.go b/cmd/ci/workflow_test.go new file mode 100644 index 0000000000..84f6ecb07b --- /dev/null +++ b/cmd/ci/workflow_test.go @@ -0,0 +1,22 @@ +package ci_test + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" + "knative.dev/func/cmd/ci" +) + +func TestGitHubWorkflow_Export(t *testing.T) { + // GIVEN + gw := ci.NewGitHubWorkflow(ci.NewCIGitHubConfig()) + bufferWriter := ci.NewBufferWriter() + + // WHEN + exportErr := gw.Export("path", bufferWriter) + + // THEN + assert.NilError(t, exportErr, "unexpected error when exporting GitHub Workflow") + assert.Assert(t, strings.Contains(bufferWriter.Buffer.String(), gw.Name)) +} diff --git a/cmd/ci/writer.go b/cmd/ci/writer.go new file mode 100644 index 0000000000..3067f18f79 --- /dev/null +++ b/cmd/ci/writer.go @@ -0,0 +1,45 @@ +package ci + +import ( + "bytes" + "os" + "path/filepath" +) + +const ( + dirPerm = 0755 // o: rwx, g|u: r-x + filePerm = 0644 // o: rw, g|u: r +) + +var DefaultWorkflowWriter = &fileWriter{} + +type WorkflowWriter interface { + Write(path string, p []byte) error +} + +type fileWriter struct{} + +func (fw *fileWriter) Write(path string, p []byte) error { + if err := os.MkdirAll(filepath.Dir(path), dirPerm); err != nil { + return err + } + + if err := os.WriteFile(path, p, filePerm); err != nil { + return err + } + + return nil +} + +type bufferWriter struct { + Buffer *bytes.Buffer +} + +func NewBufferWriter() *bufferWriter { + return &bufferWriter{Buffer: &bytes.Buffer{}} +} + +func (bw *bufferWriter) Write(_ string, p []byte) error { + _, err := bw.Buffer.Write(p) + return err +} diff --git a/cmd/common/common.go b/cmd/common/common.go index 3d7f621500..e64fc9b1d8 100644 --- a/cmd/common/common.go +++ b/cmd/common/common.go @@ -45,3 +45,33 @@ func (s standardLoaderSaver) Load(path string) (fn.Function, error) { func (s standardLoaderSaver) Save(f fn.Function) error { return f.Write() } + +// NewMockLoaderSaver creates a MockLoaderSaver with default no-op +// implementations. +func NewMockLoaderSaver() *MockLoaderSaver { + return &MockLoaderSaver{ + LoadFn: func(path string) (fn.Function, error) { + return fn.Function{}, nil + }, + SaveFn: func(f fn.Function) error { + return nil + }, + } +} + +// MockLoaderSaver provides configurable function loading and saving for testing +// purposes. +type MockLoaderSaver struct { + LoadFn func(path string) (fn.Function, error) + SaveFn func(f fn.Function) error +} + +// Load invokes the configured LoadFn to load a function from the given path. +func (m MockLoaderSaver) Load(path string) (fn.Function, error) { + return m.LoadFn(path) +} + +// Save invokes the configured SaveFn to persist the given function. +func (m MockLoaderSaver) Save(f fn.Function) error { + return m.SaveFn(f) +} diff --git a/cmd/config.go b/cmd/config.go index 15a406c444..02bbecd277 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,17 +2,19 @@ package cmd import ( "fmt" + "os" "github.com/AlecAivazis/survey/v2" "github.com/ory/viper" "github.com/spf13/cobra" + "knative.dev/func/cmd/ci" "knative.dev/func/cmd/common" "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" ) -func NewConfigCmd(loadSaver common.FunctionLoaderSaver, newClient ClientFactory) *cobra.Command { +func NewConfigCmd(loaderSaver common.FunctionLoaderSaver, writer ci.WorkflowWriter, newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Configure a function", @@ -35,10 +37,13 @@ or from the directory specified with --path. addVerboseFlag(cmd, cfg.Verbose) cmd.AddCommand(NewConfigGitCmd(newClient)) - cmd.AddCommand(NewConfigLabelsCmd(loadSaver)) - cmd.AddCommand(NewConfigEnvsCmd(loadSaver)) + cmd.AddCommand(NewConfigLabelsCmd(loaderSaver)) + cmd.AddCommand(NewConfigEnvsCmd(loaderSaver)) cmd.AddCommand(NewConfigVolumesCmd()) - cmd.AddCommand(NewConfigCICmd(loadSaver)) + + if os.Getenv(ci.ConfigCIFeatureFlag) == "true" { + cmd.AddCommand(NewConfigCICmd(loaderSaver, writer)) + } return cmd } diff --git a/cmd/config_ci.go b/cmd/config_ci.go index 2f7135bdb0..0824309458 100644 --- a/cmd/config_ci.go +++ b/cmd/config_ci.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "github.com/spf13/cobra" @@ -10,47 +9,68 @@ import ( "knative.dev/func/cmd/common" ) -func NewConfigCICmd(loaderSaver common.FunctionLoaderSaver) *cobra.Command { +func NewConfigCICmd(loaderSaver common.FunctionLoaderSaver, writer ci.WorkflowWriter) *cobra.Command { cmd := &cobra.Command{ Use: "ci", - Short: "Generate a Github Workflow for function deployment", + Short: "Generate a GitHub Workflow for function deployment", PreRunE: bindEnv( - ci.PathOption, - ci.WorkflowNameOption, + ci.PathFlag, + ci.WorkflowNameFlag, + ci.BranchFlag, + ci.KubeconfigSecretNameFlag, + ci.RegistryUrlVariableNameFlag, ), RunE: func(cmd *cobra.Command, args []string) (err error) { - return runConfigCIGithub(cmd, loaderSaver) + return runConfigCIGitHub(cmd, loaderSaver, writer) }, } addPathFlag(cmd) + cmd.Flags().String( - ci.WorkflowNameOption, + ci.WorkflowNameFlag, ci.DefaultWorkflowName, "Use a custom workflow name", ) + cmd.Flags().String( + ci.BranchFlag, + ci.DefaultBranch, + "Use a custom branch name in the workflow", + ) + + cmd.Flags().String( + ci.KubeconfigSecretNameFlag, + ci.DefaultKubeconfigSecretName, + "Use a custom secret name in the workflow, e.g. secret.YOUR_CUSTOM_KUBECONFIG", + ) + + cmd.Flags().String( + ci.RegistryUrlVariableNameFlag, + ci.DefaultRegistryUrlVariableName, + "Use a custom registry url variable name in the workflow, e.g. vars.YOUR_REGISTRY_URL", + ) + return cmd } -func runConfigCIGithub( +func runConfigCIGitHub( cmd *cobra.Command, fnLoaderSaver common.FunctionLoaderSaver, + writer ci.WorkflowWriter, ) error { - if os.Getenv(ci.ConfigCIFeatureFlag) != "true" { - return fmt.Errorf("set %s to 'true' to use this feature", ci.ConfigCIFeatureFlag) - } - - cfg := ci.NewCiGithubConfig() + cfg := ci.NewCIGitHubConfig() f, err := fnLoaderSaver.Load(cfg.Path()) if err != nil { return err } - fmt.Fprintln(cmd.OutOrStdout(), "--------------------------- Function Github Workflow Generation ---------------------------") + fmt.Fprintln(cmd.OutOrStdout(), "--------------------------- Function GitHub Workflow Generation ---------------------------") fmt.Fprintf(cmd.OutOrStdout(), "Func name: %s\n", f.Name) fmt.Fprintf(cmd.OutOrStdout(), "Func runtime: %s\n", f.Runtime) - return fmt.Errorf("not implemented") + githubWorkflow := ci.NewGitHubWorkflow(cfg) + path := cfg.FnGitHubWorkflowFilepath(f.Root) + return githubWorkflow.Export(path, writer) } diff --git a/cmd/config_ci_test.go b/cmd/config_ci_test.go index 360c4c5800..75129db0ce 100644 --- a/cmd/config_ci_test.go +++ b/cmd/config_ci_test.go @@ -2,9 +2,14 @@ package cmd_test import ( "fmt" + "io" + "os" + "strings" "testing" + "github.com/ory/viper" "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" fnCmd "knative.dev/func/cmd" "knative.dev/func/cmd/ci" "knative.dev/func/cmd/common" @@ -13,39 +18,69 @@ import ( fnTest "knative.dev/func/pkg/testing" ) +// START: Broad Unit Tests +// ----------------------- +// Execution is testet starting from the entrypoint "func config ci" including +// all components working together. Infrastructure components like the +// filesystem are mocked. func TestNewConfigCICmd_RequiresFeatureFlag(t *testing.T) { - expectedErrMsg := fmt.Sprintf("set %s to 'true' to use this feature", ci.ConfigCIFeatureFlag) - result := runConfigCiCmd(t, opts{enableFeature: false}) - assert.Error(t, result.executeErr, expectedErrMsg) + assert.ErrorContains(t, result.executeErr, "unknown command \"ci\" for \"config\"") } func TestNewConfigCICmd_CISubcommandExist(t *testing.T) { // leave 'ci' to make this test explicitly use this subcommand - opts := opts{withFuncInTempDir: true, enableFeature: true, args: []string{"ci"}} + opts := opts{ + enableFeature: true, + withMockLoaderSaver: true, + withBufferWriter: true, + args: []string{"ci"}, + } + + result := runConfigCiCmd(t, opts) + + assert.NilError(t, result.executeErr) +} + +func TestNewConfigCICmd_WritesWorkflowFile(t *testing.T) { + result := runConfigCiCmd(t, unitTestOpts()) + + assert.NilError(t, result.executeErr) + assert.Assert(t, result.gwYamlString != "") +} +func TestNewConfigCICmd_WorkflowYAMLHasCorrectStructure(t *testing.T) { + opts := unitTestOpts() result := runConfigCiCmd(t, opts) - assert.Error(t, result.executeErr, "not implemented") + assert.NilError(t, result.executeErr) + assertDefaultWorkflow(t, result.gwYamlString) } +// --------------------- +// END: Broad Unit Tests + +// START: Integration Tests +// ------------------------ +// No more mocking. Using real filesystem here for LoaderSaver and WorkflowWriter. func TestNewConfigCICmd_FailsWhenNotInitialized(t *testing.T) { + opts := opts{enableFeature: true, withFuncInTempDir: false} expectedErrMsg := fn.NewErrNotInitialized(fnTest.Cwd()).Error() - result := runConfigCiCmd(t, opts{withFuncInTempDir: false, enableFeature: true}) + result := runConfigCiCmd(t, opts) assert.Error(t, result.executeErr, expectedErrMsg) } func TestNewConfigCICmd_SuccessWhenInitialized(t *testing.T) { - result := runConfigCiCmd(t, defaultOpts()) + result := runConfigCiCmd(t, integrationTestOpts()) - assert.Error(t, result.executeErr, "not implemented") + assert.NilError(t, result.executeErr) } func TestNewConfigCICmd_FailsToLoadFuncWithWrongPath(t *testing.T) { - opts := defaultOpts() + opts := integrationTestOpts() opts.args = append(opts.args, "--path=nofunc") expectedErrMsg := "failed to create new function" @@ -56,39 +91,91 @@ func TestNewConfigCICmd_FailsToLoadFuncWithWrongPath(t *testing.T) { func TestNewConfigCICmd_SuccessfulLoadWithCorrectPath(t *testing.T) { tmpDir := t.TempDir() - opt := opts{withFuncInTempDir: false, enableFeature: true, args: []string{"ci", "--path=" + tmpDir}} - _, initErr := fn.New().Init( + opts := integrationTestOpts() + opts.args = append(opts.args, "--path="+tmpDir) + _, fnInitErr := fn.New().Init( fn.Function{Name: "github-ci-func", Runtime: "go", Root: tmpDir}, ) - result := runConfigCiCmd(t, opt) + result := runConfigCiCmd(t, opts) + + assert.NilError(t, fnInitErr) + assert.NilError(t, result.executeErr) +} + +func TestNewConfigCICmd_CreatesGitHubWorkflowDirectory(t *testing.T) { + result := runConfigCiCmd(t, integrationTestOpts()) - assert.NilError(t, initErr) - assert.Error(t, result.executeErr, "not implemented") + assert.NilError(t, result.executeErr) + _, err := os.Stat(result.ciConfig.FnGitHubWorkflowDir(result.f.Root)) + assert.NilError(t, err) } +func TestNewConfigCICmd_WritesWorkflowFileToFSWithCorrectYAMLStructure(t *testing.T) { + result := runConfigCiCmd(t, integrationTestOpts()) + file, openErr := os.Open(result.ciConfig.FnGitHubWorkflowFilepath(result.f.Root)) + raw, readErr := io.ReadAll(file) + + assert.NilError(t, result.executeErr) + assert.NilError(t, openErr) + assert.NilError(t, readErr) + assertDefaultWorkflow(t, string(raw)) + + file.Close() +} + +// ---------------------- +// END: Integration Tests + // START: Testing Framework // ------------------------ type opts struct { - withFuncInTempDir bool - enableFeature bool - args []string + withMockLoaderSaver bool + withFuncInTempDir bool + enableFeature bool + withBufferWriter bool + args []string +} + +// unitTestOpts contains test options for broad unit tests +// +// - withMockLoaderSaver: true, +// - withFuncInTempDir: false, +// - enableFeature: true, +// - withBufferWriter: true, +// - args: []string{"ci"}, +func unitTestOpts() opts { + return opts{ + withMockLoaderSaver: true, + withFuncInTempDir: false, + enableFeature: true, + withBufferWriter: true, + args: []string{"ci"}, + } } -// defaultOpts provides the most used options for tests -// - withFuncInTempDir: true, -// - enableFeature: true, -// - args: []string{"ci"}, -func defaultOpts() opts { +// integrationTestOpts contains test options for integration tests +// +// - withMockLoaderSaver: false, +// - withFuncInTempDir: true, +// - enableFeature: true, +// - withBufferWriter: false, +// - args: []string{"ci"}, +func integrationTestOpts() opts { return opts{ - withFuncInTempDir: true, - enableFeature: true, - args: []string{"ci"}, + withMockLoaderSaver: false, + withFuncInTempDir: true, + enableFeature: true, + withBufferWriter: false, + args: []string{"ci"}, } } type result struct { - executeErr error + f fn.Function + ciConfig ci.CIConfig + executeErr error + gwYamlString string } func runConfigCiCmd( @@ -98,21 +185,37 @@ func runConfigCiCmd( t.Helper() // PRE-RUN PREP + // all options for "func config ci" command + loaderSaver := common.DefaultLoaderSaver + if opts.withMockLoaderSaver { + loaderSaver = common.NewMockLoaderSaver() + } + + f := fn.Function{} if opts.withFuncInTempDir { - _ = cmdTest.CreateFuncInTempDir(t, "github-ci-func") + f = cmdTest.CreateFuncInTempDir(t, "github-ci-func") } if opts.enableFeature { t.Setenv(ci.ConfigCIFeatureFlag, "true") } + var writer ci.WorkflowWriter = ci.DefaultWorkflowWriter + bufferWriter := ci.NewBufferWriter() + if opts.withBufferWriter { + writer = bufferWriter + } + args := opts.args if len(opts.args) == 0 { args = []string{"ci"} } + viper.Reset() + cmd := fnCmd.NewConfigCmd( - common.DefaultLoaderSaver, + loaderSaver, + writer, fnCmd.NewClient, ) cmd.SetArgs(args) @@ -120,7 +223,60 @@ func runConfigCiCmd( // RUN err := cmd.Execute() + // POST-RUN GATHER + ciConfig := ci.NewCIGitHubConfig() + gwYamlString := bufferWriter.Buffer.String() + return result{ + f, + ciConfig, err, + gwYamlString, } } + +// assertDefaultWorkflow asserts all the GitHub workflow value for correct values +// including the default values which can be changed: +// - runs-on: ubuntu-latest +// - kubeconfig: ${{ secrets.KUBECONFIG }} +// - run: func deploy --registry=${{ vars.REGISTRY_URL }} -v +func assertDefaultWorkflow(t *testing.T, actualGw string) { + t.Helper() + + assert.Assert(t, yamlContains(actualGw, "Func Deploy")) + assert.Assert(t, yamlContains(actualGw, "- main")) + + assert.Assert(t, yamlContains(actualGw, "ubuntu-latest")) + + assert.Assert(t, strings.Count(actualGw, "- name:") == 4) + + assert.Assert(t, yamlContains(actualGw, "Checkout code")) + assert.Assert(t, yamlContains(actualGw, "actions/checkout@v4")) + + assert.Assert(t, yamlContains(actualGw, "Setup Kubernetes context")) + assert.Assert(t, yamlContains(actualGw, "azure/k8s-set-context@v4")) + assert.Assert(t, yamlContains(actualGw, "method: kubeconfig")) + assert.Assert(t, yamlContains(actualGw, "kubeconfig: ${{ secrets.KUBECONFIG }}")) + + assert.Assert(t, yamlContains(actualGw, "Install func cli")) + assert.Assert(t, yamlContains(actualGw, "gauron99/knative-func-action@main")) + assert.Assert(t, yamlContains(actualGw, "version: knative-v1.19.1")) + assert.Assert(t, yamlContains(actualGw, "name: func")) + + assert.Assert(t, yamlContains(actualGw, "Deploy function")) + assert.Assert(t, yamlContains(actualGw, "func deploy --registry=${{ vars.REGISTRY_URL }} -v")) +} + +func yamlContains(yaml, substr string) cmp.Comparison { + return func() cmp.Result { + if strings.Contains(yaml, substr) { + return cmp.ResultSuccess + } + return cmp.ResultFailure(fmt.Sprintf( + "missing '%s' in:\n\n%s", substr, yaml, + )) + } +} + +// ---------------------- +// END: Testing Framework diff --git a/cmd/config_test.go b/cmd/config_test.go index 93183254be..dcadabd69e 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -9,24 +9,26 @@ import ( "testing" "github.com/ory/viper" + "github.com/spf13/cobra" fnCmd "knative.dev/func/cmd" + "knative.dev/func/cmd/ci" + "knative.dev/func/cmd/common" fn "knative.dev/func/pkg/functions" ) func TestListEnvs(t *testing.T) { - mock := newMockLoaderSaver() + mock := common.NewMockLoaderSaver() foo := "foo" bar := "bar" envs := []fn.Env{{Name: &foo, Value: &bar}} - mock.load = func(path string) (fn.Function, error) { + mock.LoadFn = func(path string) (fn.Function, error) { if path != "" { t.Fatalf("bad path, got %q but expected ", path) } return fn.Function{Run: fn.RunSpec{Envs: envs}}, nil } - cmd := fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "-o=json", "--path="}) + cmd := setupConfigEnvCmd(mock, "-o=json", "--path=") var buff bytes.Buffer cmd.SetOut(&buff) @@ -47,6 +49,12 @@ func TestListEnvs(t *testing.T) { } } +func setupConfigEnvCmd(mock common.FunctionLoaderSaver, args ...string) *cobra.Command { + cmd := fnCmd.NewConfigCmd(mock, ci.NewBufferWriter(), fnCmd.NewClient) + cmd.SetArgs(append([]string{"envs"}, args...)) + return cmd +} + func TestListEnvAdd(t *testing.T) { // strings as vars so we can take address of them foo := "foo" @@ -55,12 +63,12 @@ func TestListEnvAdd(t *testing.T) { fortyTwo := "42" configMapExpression := "{{ configMap:myMap }}" - mock := newMockLoaderSaver() - mock.load = func(path string) (fn.Function, error) { + mock := common.NewMockLoaderSaver() + mock.LoadFn = func(path string) (fn.Function, error) { return fn.Function{Run: fn.RunSpec{Envs: []fn.Env{{Name: &foo, Value: &bar}}}}, nil } var expectedEnvs []fn.Env - mock.save = func(f fn.Function) error { + mock.SaveFn = func(f fn.Function) error { if !envsEqual(expectedEnvs, f.Run.Envs) { return fmt.Errorf("unexpected envs: got %v but %v was expected", f.Run.Envs, expectedEnvs) } @@ -68,8 +76,7 @@ func TestListEnvAdd(t *testing.T) { } expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: &answer, Value: &fortyTwo}} - cmd := fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "add", "--name=answer", "--value=42"}) + cmd := setupConfigEnvCmd(mock, "add", "--name=answer", "--value=42") cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -80,8 +87,7 @@ func TestListEnvAdd(t *testing.T) { viper.Reset() expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: nil, Value: &configMapExpression}} - cmd = fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "add", "--value={{ configMap:myMap }}"}) + cmd = setupConfigEnvCmd(mock, "add", "--value={{ configMap:myMap }}") cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -91,8 +97,7 @@ func TestListEnvAdd(t *testing.T) { } viper.Reset() - cmd = fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "add", "--name=1", "--value=abc"}) + cmd = setupConfigEnvCmd(mock, "add", "--name=1", "--value=abc") cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -155,27 +160,3 @@ func envsEqual(a, b []fn.Env) bool { } return true } - -func newMockLoaderSaver() *mockLoaderSaver { - return &mockLoaderSaver{ - load: func(path string) (fn.Function, error) { - return fn.Function{}, nil - }, - save: func(f fn.Function) error { - return nil - }, - } -} - -type mockLoaderSaver struct { - load func(path string) (fn.Function, error) - save func(f fn.Function) error -} - -func (m mockLoaderSaver) Load(path string) (fn.Function, error) { - return m.load(path) -} - -func (m mockLoaderSaver) Save(f fn.Function) error { - return m.save(f) -} diff --git a/cmd/root.go b/cmd/root.go index dd7da95b73..25313e3ff8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "knative.dev/client/pkg/util" + "knative.dev/func/cmd/ci" "knative.dev/func/cmd/common" "knative.dev/func/cmd/templates" "knative.dev/func/pkg/config" @@ -100,7 +101,7 @@ Learn more about Knative at: https://knative.dev`, cfg.Name), { Header: "System Commands:", Commands: []*cobra.Command{ - NewConfigCmd(common.DefaultLoaderSaver, newClient), + NewConfigCmd(common.DefaultLoaderSaver, ci.DefaultWorkflowWriter, newClient), NewLanguagesCmd(newClient), NewTemplatesCmd(newClient), NewRepositoryCmd(newClient), diff --git a/docs/reference/func_config.md b/docs/reference/func_config.md index 0f318fdb2d..ef2aea0ff3 100644 --- a/docs/reference/func_config.md +++ b/docs/reference/func_config.md @@ -26,7 +26,6 @@ func config ### SEE ALSO * [func](func.md) - func manages Knative Functions -* [func config ci](func_config_ci.md) - Generate a Github Workflow for function deployment * [func config envs](func_config_envs.md) - List and manage configured environment variable for a function * [func config git](func_config_git.md) - Manage Git configuration of a function * [func config labels](func_config_labels.md) - List and manage configured labels for a function