Skip to content

Commit 4ef5654

Browse files
authored
feat: add GitHub workflow generation for deployment (#3295)
This change adds GitHub Actions workflow generation for deploying functions. The workflow includes checkout, Kubernetes context setup via kubeconfig, func CLI installation, and deployment with registry configuration. The command is feature-flagged (FUNC_ENABLE_CI_CONFIG) to allow gradual rollout. Users can customize workflow parameters: - Branch trigger (default: main) - Kubeconfig secret name (default: KUBECONFIG) - Registry URL variable (default: REGISTRY_URL) - Workflow name (default: Func Deploy) Implementation includes comprehensive test coverage with both unit tests (mocked components) and integration tests (real filesystem operations). Also extracts MockLoaderSaver to cmd/common for reusability across test suites. Issue #3256 Signed-off-by: Stanislav Jakuschevskij <sjakusch@redhat.com>
1 parent e358070 commit 4ef5654

11 files changed

Lines changed: 524 additions & 100 deletions

File tree

cmd/ci/config.go

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,71 @@ import (
99
const (
1010
ConfigCIFeatureFlag = "FUNC_ENABLE_CI_CONFIG"
1111

12-
DefaultGithubWorkflowDir = ".github/workflows"
13-
DefaultGithubWorkflowFilename = "func-deploy.yaml"
12+
PathFlag = "path"
1413

15-
PathOption = "path"
14+
DefaultGitHubWorkflowDir = ".github/workflows"
15+
DefaultGitHubWorkflowFilename = "func-deploy.yaml"
1616

17-
WorkflowNameOption = "workflow-name"
17+
WorkflowNameFlag = "workflow-name"
1818
DefaultWorkflowName = "Func Deploy"
19+
20+
BranchFlag = "branch"
21+
DefaultBranch = "main"
22+
23+
KubeconfigSecretNameFlag = "kubeconfig-secret-name"
24+
DefaultKubeconfigSecretName = "KUBECONFIG"
25+
26+
RegistryUrlVariableNameFlag = "registry-url-variable-name"
27+
DefaultRegistryUrlVariableName = "REGISTRY_URL"
1928
)
2029

21-
// CIConfig readonly CI configuration
30+
// CIConfig readonly configuration
2231
type CIConfig struct {
2332
githubWorkflowDir,
2433
githubWorkflowFilename,
2534
path,
26-
workflowName string
35+
workflowName,
36+
branch,
37+
kubeconfigSecret,
38+
registryUrlVar string
2739
}
2840

29-
func NewCiGithubConfig() CIConfig {
41+
func NewCIGitHubConfig() CIConfig {
3042
return CIConfig{
31-
githubWorkflowDir: DefaultGithubWorkflowDir,
32-
githubWorkflowFilename: DefaultGithubWorkflowFilename,
33-
path: viper.GetString(PathOption),
34-
workflowName: viper.GetString(WorkflowNameOption),
43+
githubWorkflowDir: DefaultGitHubWorkflowDir,
44+
githubWorkflowFilename: DefaultGitHubWorkflowFilename,
45+
path: viper.GetString(PathFlag),
46+
workflowName: viper.GetString(WorkflowNameFlag),
47+
branch: viper.GetString(BranchFlag),
48+
kubeconfigSecret: viper.GetString(KubeconfigSecretNameFlag),
49+
registryUrlVar: viper.GetString(RegistryUrlVariableNameFlag),
3550
}
3651
}
3752

38-
func (cc *CIConfig) FnGithubWorkflowDir(fnRoot string) string {
53+
func (cc *CIConfig) FnGitHubWorkflowDir(fnRoot string) string {
3954
return filepath.Join(fnRoot, cc.githubWorkflowDir)
4055
}
4156

42-
func (cc *CIConfig) FnGithubWorkflowFilepath(fnRoot string) string {
43-
return filepath.Join(cc.FnGithubWorkflowDir(fnRoot), cc.githubWorkflowFilename)
57+
func (cc *CIConfig) FnGitHubWorkflowFilepath(fnRoot string) string {
58+
return filepath.Join(cc.FnGitHubWorkflowDir(fnRoot), cc.githubWorkflowFilename)
4459
}
4560

4661
func (cc *CIConfig) Path() string {
4762
return cc.path
4863
}
64+
65+
func (cc *CIConfig) WorkflowName() string {
66+
return cc.workflowName
67+
}
68+
69+
func (cc *CIConfig) Branch() string {
70+
return cc.branch
71+
}
72+
73+
func (cc *CIConfig) KubeconfigSecret() string {
74+
return cc.kubeconfigSecret
75+
}
76+
77+
func (cc *CIConfig) RegistryUrlVar() string {
78+
return cc.registryUrlVar
79+
}

cmd/ci/workflow.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package ci
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"gopkg.in/yaml.v3"
8+
)
9+
10+
type githubWorkflow struct {
11+
Name string `yaml:"name"`
12+
On workflowTriggers `yaml:"on"`
13+
Jobs map[string]job `yaml:"jobs"`
14+
}
15+
16+
type workflowTriggers struct {
17+
Push *pushTrigger `yaml:"push,omitempty"`
18+
}
19+
20+
type pushTrigger struct {
21+
Branches []string `yaml:"branches,omitempty"`
22+
}
23+
24+
type job struct {
25+
RunsOn string `yaml:"runs-on"`
26+
Steps []step `yaml:"steps"`
27+
}
28+
29+
type step struct {
30+
Name string `yaml:"name,omitempty"`
31+
Uses string `yaml:"uses,omitempty"`
32+
Run string `yaml:"run,omitempty"`
33+
With map[string]string `yaml:"with,omitempty"`
34+
}
35+
36+
func NewGitHubWorkflow(conf CIConfig) *githubWorkflow {
37+
runsOn := "ubuntu-latest"
38+
39+
pushTrigger := newPushTrigger(conf.Branch())
40+
41+
var steps []step
42+
checkoutCode := newStep("Checkout code").
43+
withUses("actions/checkout@v4")
44+
steps = append(steps, *checkoutCode)
45+
46+
setupK8Context := newStep("Setup Kubernetes context").
47+
withUses("azure/k8s-set-context@v4").
48+
withActionConfig("method", "kubeconfig").
49+
withActionConfig("kubeconfig", newSecret(conf.KubeconfigSecret()))
50+
steps = append(steps, *setupK8Context)
51+
52+
installFuncCli := newStep("Install func cli").
53+
withUses("gauron99/knative-func-action@main").
54+
withActionConfig("version", "knative-v1.19.1").
55+
withActionConfig("name", "func")
56+
steps = append(steps, *installFuncCli)
57+
58+
deployFunc := newStep("Deploy function").
59+
withRun("func deploy --registry=" + newVariable(conf.RegistryUrlVar()) + " -v")
60+
steps = append(steps, *deployFunc)
61+
62+
return &githubWorkflow{
63+
Name: conf.WorkflowName(),
64+
On: pushTrigger,
65+
Jobs: map[string]job{
66+
"deploy": {
67+
RunsOn: runsOn,
68+
Steps: steps,
69+
},
70+
},
71+
}
72+
}
73+
74+
func newPushTrigger(branch string) workflowTriggers {
75+
result := workflowTriggers{
76+
Push: &pushTrigger{Branches: []string{branch}},
77+
}
78+
79+
return result
80+
}
81+
82+
func newStep(name string) *step {
83+
return &step{Name: name}
84+
}
85+
86+
func (s *step) withUses(u string) *step {
87+
s.Uses = u
88+
return s
89+
}
90+
91+
func (s *step) withRun(r string) *step {
92+
s.Run = r
93+
return s
94+
}
95+
96+
func (s *step) withActionConfig(key, value string) *step {
97+
if s.With == nil {
98+
s.With = make(map[string]string)
99+
}
100+
101+
s.With[key] = value
102+
103+
return s
104+
}
105+
106+
func newSecret(key string) string {
107+
return fmt.Sprintf("${{ secrets.%s }}", key)
108+
}
109+
110+
func newVariable(key string) string {
111+
return fmt.Sprintf("${{ vars.%s }}", key)
112+
}
113+
114+
func (gw *githubWorkflow) Export(path string, w WorkflowWriter) error {
115+
raw, err := gw.toYaml()
116+
if err != nil {
117+
return err
118+
}
119+
120+
return w.Write(path, raw)
121+
}
122+
123+
func (gw *githubWorkflow) toYaml() ([]byte, error) {
124+
var buf bytes.Buffer
125+
encoder := yaml.NewEncoder(&buf)
126+
encoder.SetIndent(2)
127+
128+
if err := encoder.Encode(gw); err != nil {
129+
return nil, err
130+
}
131+
encoder.Close()
132+
133+
return buf.Bytes(), nil
134+
}

cmd/ci/workflow_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ci_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"gotest.tools/v3/assert"
8+
"knative.dev/func/cmd/ci"
9+
)
10+
11+
func TestGitHubWorkflow_Export(t *testing.T) {
12+
// GIVEN
13+
gw := ci.NewGitHubWorkflow(ci.NewCIGitHubConfig())
14+
bufferWriter := ci.NewBufferWriter()
15+
16+
// WHEN
17+
exportErr := gw.Export("path", bufferWriter)
18+
19+
// THEN
20+
assert.NilError(t, exportErr, "unexpected error when exporting GitHub Workflow")
21+
assert.Assert(t, strings.Contains(bufferWriter.Buffer.String(), gw.Name))
22+
}

cmd/ci/writer.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ci
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
const (
10+
dirPerm = 0755 // o: rwx, g|u: r-x
11+
filePerm = 0644 // o: rw, g|u: r
12+
)
13+
14+
var DefaultWorkflowWriter = &fileWriter{}
15+
16+
type WorkflowWriter interface {
17+
Write(path string, p []byte) error
18+
}
19+
20+
type fileWriter struct{}
21+
22+
func (fw *fileWriter) Write(path string, p []byte) error {
23+
if err := os.MkdirAll(filepath.Dir(path), dirPerm); err != nil {
24+
return err
25+
}
26+
27+
if err := os.WriteFile(path, p, filePerm); err != nil {
28+
return err
29+
}
30+
31+
return nil
32+
}
33+
34+
type bufferWriter struct {
35+
Buffer *bytes.Buffer
36+
}
37+
38+
func NewBufferWriter() *bufferWriter {
39+
return &bufferWriter{Buffer: &bytes.Buffer{}}
40+
}
41+
42+
func (bw *bufferWriter) Write(_ string, p []byte) error {
43+
_, err := bw.Buffer.Write(p)
44+
return err
45+
}

cmd/common/common.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,33 @@ func (s standardLoaderSaver) Load(path string) (fn.Function, error) {
4545
func (s standardLoaderSaver) Save(f fn.Function) error {
4646
return f.Write()
4747
}
48+
49+
// NewMockLoaderSaver creates a MockLoaderSaver with default no-op
50+
// implementations.
51+
func NewMockLoaderSaver() *MockLoaderSaver {
52+
return &MockLoaderSaver{
53+
LoadFn: func(path string) (fn.Function, error) {
54+
return fn.Function{}, nil
55+
},
56+
SaveFn: func(f fn.Function) error {
57+
return nil
58+
},
59+
}
60+
}
61+
62+
// MockLoaderSaver provides configurable function loading and saving for testing
63+
// purposes.
64+
type MockLoaderSaver struct {
65+
LoadFn func(path string) (fn.Function, error)
66+
SaveFn func(f fn.Function) error
67+
}
68+
69+
// Load invokes the configured LoadFn to load a function from the given path.
70+
func (m MockLoaderSaver) Load(path string) (fn.Function, error) {
71+
return m.LoadFn(path)
72+
}
73+
74+
// Save invokes the configured SaveFn to persist the given function.
75+
func (m MockLoaderSaver) Save(f fn.Function) error {
76+
return m.SaveFn(f)
77+
}

cmd/config.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/AlecAivazis/survey/v2"
78
"github.com/ory/viper"
89
"github.com/spf13/cobra"
910

11+
"knative.dev/func/cmd/ci"
1012
"knative.dev/func/cmd/common"
1113
"knative.dev/func/pkg/config"
1214
fn "knative.dev/func/pkg/functions"
1315
)
1416

15-
func NewConfigCmd(loadSaver common.FunctionLoaderSaver, newClient ClientFactory) *cobra.Command {
17+
func NewConfigCmd(loaderSaver common.FunctionLoaderSaver, writer ci.WorkflowWriter, newClient ClientFactory) *cobra.Command {
1618
cmd := &cobra.Command{
1719
Use: "config",
1820
Short: "Configure a function",
@@ -35,10 +37,13 @@ or from the directory specified with --path.
3537
addVerboseFlag(cmd, cfg.Verbose)
3638

3739
cmd.AddCommand(NewConfigGitCmd(newClient))
38-
cmd.AddCommand(NewConfigLabelsCmd(loadSaver))
39-
cmd.AddCommand(NewConfigEnvsCmd(loadSaver))
40+
cmd.AddCommand(NewConfigLabelsCmd(loaderSaver))
41+
cmd.AddCommand(NewConfigEnvsCmd(loaderSaver))
4042
cmd.AddCommand(NewConfigVolumesCmd())
41-
cmd.AddCommand(NewConfigCICmd(loadSaver))
43+
44+
if os.Getenv(ci.ConfigCIFeatureFlag) == "true" {
45+
cmd.AddCommand(NewConfigCICmd(loaderSaver, writer))
46+
}
4247

4348
return cmd
4449
}

0 commit comments

Comments
 (0)