From 82813467a09e4f3e82252066547096753f450fca Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Mon, 15 Dec 2025 18:10:46 +0100 Subject: [PATCH] feat: add registry login to CI workflows GitHub workflows generated by 'func config ci' previously lacked registry authentication. Now a registry login step was added. Add new flags for registry authentication and runner configuration: - --use-registry-login: enable/disable registry login step (default: true) - --registry-login-url-variable-name: customize registry URL variable (default: REGISTRY_LOGIN_URL) - --registry-user-variable-name: customize username variable (default: REGISTRY_USERNAME) - --registry-pass-secret-name: customize password secret (default: REGISTRY_PASSWORD) - --self-hosted-runner: use self-hosted instead of ubuntu-latest runners (default: false) Workflow generation logic is refactored into helper functions for better maintainability. More logic will be added to this functions in follow up PRs. Issue #3256 Signed-off-by: Stanislav Jakuschevskij --- cmd/ci/config.go | 55 +++++++++++++++++++++++--- cmd/ci/workflow.go | 89 ++++++++++++++++++++++++++++++++----------- cmd/config_ci.go | 35 +++++++++++++++++ cmd/config_ci_test.go | 62 ++++++++++++++++++++++++++++-- 4 files changed, 209 insertions(+), 32 deletions(-) diff --git a/cmd/ci/config.go b/cmd/ci/config.go index 86b0a908ea..8b559b784a 100644 --- a/cmd/ci/config.go +++ b/cmd/ci/config.go @@ -14,17 +14,32 @@ const ( DefaultGitHubWorkflowDir = ".github/workflows" DefaultGitHubWorkflowFilename = "func-deploy.yaml" - WorkflowNameFlag = "workflow-name" - DefaultWorkflowName = "Func Deploy" - BranchFlag = "branch" DefaultBranch = "main" + WorkflowNameFlag = "workflow-name" + DefaultWorkflowName = "Func Deploy" + KubeconfigSecretNameFlag = "kubeconfig-secret-name" DefaultKubeconfigSecretName = "KUBECONFIG" + RegistryLoginUrlVariableNameFlag = "registry-login-url-variable-name" + DefaultRegistryLoginUrlVariableName = "REGISTRY_LOGIN_URL" + + RegistryUserVariableNameFlag = "registry-user-variable-name" + DefaultRegistryUserVariableName = "REGISTRY_USERNAME" + + RegistryPassSecretNameFlag = "registry-pass-secret-name" + DefaultRegistryPassSecretName = "REGISTRY_PASSWORD" + RegistryUrlVariableNameFlag = "registry-url-variable-name" DefaultRegistryUrlVariableName = "REGISTRY_URL" + + UseRegistryLoginFlag = "use-registry-login" + DefaultUseRegistryLogin = true + + UseSelfHostedRunnerFlag = "self-hosted-runner" + DefaultUseSelfHostedRunner = false ) // CIConfig readonly configuration @@ -32,10 +47,15 @@ type CIConfig struct { githubWorkflowDir, githubWorkflowFilename, path, - workflowName, branch, + workflowName, kubeconfigSecret, + registryLoginUrlVar, + registryUserVar, + registryPassSecret, registryUrlVar string + useRegistryLogin, + useSelfHostedRunner bool } func NewCIGitHubConfig() CIConfig { @@ -43,10 +63,15 @@ func NewCIGitHubConfig() CIConfig { githubWorkflowDir: DefaultGitHubWorkflowDir, githubWorkflowFilename: DefaultGitHubWorkflowFilename, path: viper.GetString(PathFlag), - workflowName: viper.GetString(WorkflowNameFlag), branch: viper.GetString(BranchFlag), + workflowName: viper.GetString(WorkflowNameFlag), kubeconfigSecret: viper.GetString(KubeconfigSecretNameFlag), + registryLoginUrlVar: viper.GetString(RegistryLoginUrlVariableNameFlag), + registryUserVar: viper.GetString(RegistryUserVariableNameFlag), + registryPassSecret: viper.GetString(RegistryPassSecretNameFlag), registryUrlVar: viper.GetString(RegistryUrlVariableNameFlag), + useRegistryLogin: viper.GetBool(UseRegistryLoginFlag), + useSelfHostedRunner: viper.GetBool(UseSelfHostedRunnerFlag), } } @@ -70,10 +95,30 @@ func (cc *CIConfig) Branch() string { return cc.branch } +func (cc *CIConfig) UseRegistryLogin() bool { + return cc.useRegistryLogin +} + +func (cc *CIConfig) UseSelfHostedRunner() bool { + return cc.useSelfHostedRunner +} + func (cc *CIConfig) KubeconfigSecret() string { return cc.kubeconfigSecret } +func (cc *CIConfig) RegistryLoginUrlVar() string { + return cc.registryLoginUrlVar +} + +func (cc *CIConfig) RegistryUserVar() string { + return cc.registryUserVar +} + +func (cc *CIConfig) RegistryPassSecret() string { + return cc.registryPassSecret +} + func (cc *CIConfig) RegistryUrlVar() string { return cc.registryUrlVar } diff --git a/cmd/ci/workflow.go b/cmd/ci/workflow.go index 30ea3a3934..2e707f125d 100644 --- a/cmd/ci/workflow.go +++ b/cmd/ci/workflow.go @@ -34,30 +34,15 @@ type step struct { } func NewGitHubWorkflow(conf CIConfig) *githubWorkflow { - runsOn := "ubuntu-latest" - - pushTrigger := newPushTrigger(conf.Branch()) + runsOn := createRunsOn(conf.UseSelfHostedRunner()) + pushTrigger := createPushTrigger(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) + steps = createCheckoutStep(steps) + steps = createK8ContextStep(conf, steps) + steps = createRegistryLoginStep(conf, steps) + steps = createFuncCLIInstallStep(steps) + steps = createFuncDeployStep(conf, steps) return &githubWorkflow{ Name: conf.WorkflowName(), @@ -71,7 +56,15 @@ func NewGitHubWorkflow(conf CIConfig) *githubWorkflow { } } -func newPushTrigger(branch string) workflowTriggers { +func createRunsOn(useSelfHostedRunner bool) string { + runsOn := "ubuntu-latest" + if useSelfHostedRunner { + runsOn = "self-hosted" + } + return runsOn +} + +func createPushTrigger(branch string) workflowTriggers { result := workflowTriggers{ Push: &pushTrigger{Branches: []string{branch}}, } @@ -79,6 +72,56 @@ func newPushTrigger(branch string) workflowTriggers { return result } +func createCheckoutStep(steps []step) []step { + checkoutCode := newStep("Checkout code"). + withUses("actions/checkout@v4") + + return append(steps, *checkoutCode) +} + +func createK8ContextStep(conf CIConfig, steps []step) []step { + setupK8Context := newStep("Setup Kubernetes context"). + withUses("azure/k8s-set-context@v4"). + withActionConfig("method", "kubeconfig"). + withActionConfig("kubeconfig", newSecret(conf.KubeconfigSecret())) + + return append(steps, *setupK8Context) +} + +func createRegistryLoginStep(conf CIConfig, steps []step) []step { + if !conf.UseRegistryLogin() { + return steps + } + + loginToContainerRegistry := newStep("Login to container registry"). + withUses("docker/login-action@v3"). + withActionConfig("registry", newVariable(conf.RegistryLoginUrlVar())). + withActionConfig("username", newVariable(conf.RegistryUserVar())). + withActionConfig("password", newSecret(conf.RegistryPassSecret())) + + return append(steps, *loginToContainerRegistry) +} + +func createFuncDeployStep(conf CIConfig, steps []step) []step { + registryUrl := newVariable(conf.RegistryUrlVar()) + if conf.UseRegistryLogin() { + registryUrl = newVariable(conf.RegistryLoginUrlVar()) + "/" + newVariable(conf.RegistryUserVar()) + } + deployFunc := newStep("Deploy function"). + withRun("func deploy --registry=" + registryUrl + " -v") + + return append(steps, *deployFunc) +} + +func createFuncCLIInstallStep(steps []step) []step { + installFuncCli := newStep("Install func cli"). + withUses("gauron99/knative-func-action@main"). + withActionConfig("version", "knative-v1.19.1"). + withActionConfig("name", "func") + + return append(steps, *installFuncCli) +} + func newStep(name string) *step { return &step{Name: name} } diff --git a/cmd/config_ci.go b/cmd/config_ci.go index 0824309458..f37bd28997 100644 --- a/cmd/config_ci.go +++ b/cmd/config_ci.go @@ -15,9 +15,14 @@ func NewConfigCICmd(loaderSaver common.FunctionLoaderSaver, writer ci.WorkflowWr Short: "Generate a GitHub Workflow for function deployment", PreRunE: bindEnv( ci.PathFlag, + ci.UseRegistryLoginFlag, + ci.UseSelfHostedRunnerFlag, ci.WorkflowNameFlag, ci.BranchFlag, ci.KubeconfigSecretNameFlag, + ci.RegistryLoginUrlVariableNameFlag, + ci.RegistryUserVariableNameFlag, + ci.RegistryPassSecretNameFlag, ci.RegistryUrlVariableNameFlag, ), RunE: func(cmd *cobra.Command, args []string) (err error) { @@ -27,6 +32,18 @@ func NewConfigCICmd(loaderSaver common.FunctionLoaderSaver, writer ci.WorkflowWr addPathFlag(cmd) + cmd.Flags().Bool( + ci.UseRegistryLoginFlag, + ci.DefaultUseRegistryLogin, + "Add a registry login step in the github workflow", + ) + + cmd.Flags().Bool( + ci.UseSelfHostedRunnerFlag, + ci.DefaultUseSelfHostedRunner, + "Use a 'self-hosted' runner instead of the default 'ubuntu-latest' for local runner execution", + ) + cmd.Flags().String( ci.WorkflowNameFlag, ci.DefaultWorkflowName, @@ -45,6 +62,24 @@ func NewConfigCICmd(loaderSaver common.FunctionLoaderSaver, writer ci.WorkflowWr "Use a custom secret name in the workflow, e.g. secret.YOUR_CUSTOM_KUBECONFIG", ) + cmd.Flags().String( + ci.RegistryLoginUrlVariableNameFlag, + ci.DefaultRegistryLoginUrlVariableName, + "Use a custom registry login url variable name in the workflow, e.g. vars.YOUR_REGISTRY_LOGIN_URL", + ) + + cmd.Flags().String( + ci.RegistryUserVariableNameFlag, + ci.DefaultRegistryUserVariableName, + "Use a custom registry user variable name in the workflow, e.g. vars.YOUR_REGISTRY_USER", + ) + + cmd.Flags().String( + ci.RegistryPassSecretNameFlag, + ci.DefaultRegistryPassSecretName, + "Use a custom registry pass secret name in the workflow, e.g. secret.YOUR_REGISTRY_PASSWORD", + ) + cmd.Flags().String( ci.RegistryUrlVariableNameFlag, ci.DefaultRegistryUrlVariableName, diff --git a/cmd/config_ci_test.go b/cmd/config_ci_test.go index 75129db0ce..dacdc58924 100644 --- a/cmd/config_ci_test.go +++ b/cmd/config_ci_test.go @@ -51,11 +51,46 @@ func TestNewConfigCICmd_WritesWorkflowFile(t *testing.T) { } func TestNewConfigCICmd_WorkflowYAMLHasCorrectStructure(t *testing.T) { + result := runConfigCiCmd(t, unitTestOpts()) + + assert.NilError(t, result.executeErr) + assertDefaultWorkflow(t, result.gwYamlString) +} + +func TestNewConfigCICmd_WorkflowYAMLHasCustomValues(t *testing.T) { + // GIVEN opts := unitTestOpts() + opts.args = append(opts.args, + "--self-hosted-runner", + "--workflow-name=Custom Deploy", + "--kubeconfig-secret-name=DEV_CLUSTER_KUBECONFIG", + "--registry-login-url-variable-name=DEV_REGISTRY_LOGIN_URL", + "--registry-user-variable-name=DEV_REGISTRY_USER", + "--registry-pass-secret-name=DEV_REGISTRY_PASS", + "--branch=master", + ) + + // WHEN result := runConfigCiCmd(t, opts) + // THEN assert.NilError(t, result.executeErr) - assertDefaultWorkflow(t, result.gwYamlString) + assertCustomWorkflow(t, result.gwYamlString) +} + +func TestNewConfigCICmd_WorkflowHasNoRegistryLogin(t *testing.T) { + // GIVEN + opts := unitTestOpts() + opts.args = append(opts.args, "--use-registry-login=false") + + // WHEN + result := runConfigCiCmd(t, opts) + + // THEN + assert.NilError(t, result.executeErr) + assert.Assert(t, !strings.Contains(result.gwYamlString, "docker/login-action@v3")) + assert.Assert(t, !strings.Contains(result.gwYamlString, "Login to container registry")) + assert.Assert(t, yamlContains(result.gwYamlString, "--registry=${{ vars.REGISTRY_URL }}")) } // --------------------- @@ -239,7 +274,10 @@ func runConfigCiCmd( // including the default values which can be changed: // - runs-on: ubuntu-latest // - kubeconfig: ${{ secrets.KUBECONFIG }} -// - run: func deploy --registry=${{ vars.REGISTRY_URL }} -v +// - registry: ${{ vars.REGISTRY_LOGIN_URL }}") +// - username: ${{ vars.REGISTRY_USERNAME }} +// - password: ${{ secrets.REGISTRY_PASSWORD }} +// - run: func deploy --registry=${{ vars.REGISTRY_LOGIN_URL }}/${{ vars.REGISTRY_USERNAME }} -v func assertDefaultWorkflow(t *testing.T, actualGw string) { t.Helper() @@ -248,7 +286,7 @@ func assertDefaultWorkflow(t *testing.T, actualGw string) { assert.Assert(t, yamlContains(actualGw, "ubuntu-latest")) - assert.Assert(t, strings.Count(actualGw, "- name:") == 4) + assert.Assert(t, strings.Count(actualGw, "- name:") == 5) assert.Assert(t, yamlContains(actualGw, "Checkout code")) assert.Assert(t, yamlContains(actualGw, "actions/checkout@v4")) @@ -258,13 +296,19 @@ func assertDefaultWorkflow(t *testing.T, actualGw string) { assert.Assert(t, yamlContains(actualGw, "method: kubeconfig")) assert.Assert(t, yamlContains(actualGw, "kubeconfig: ${{ secrets.KUBECONFIG }}")) + assert.Assert(t, yamlContains(actualGw, "Login to container registry")) + assert.Assert(t, yamlContains(actualGw, "docker/login-action@v3")) + assert.Assert(t, yamlContains(actualGw, "registry: ${{ vars.REGISTRY_LOGIN_URL }}")) + assert.Assert(t, yamlContains(actualGw, "username: ${{ vars.REGISTRY_USERNAME }}")) + assert.Assert(t, yamlContains(actualGw, "password: ${{ secrets.REGISTRY_PASSWORD }}")) + 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")) + assert.Assert(t, yamlContains(actualGw, "func deploy --registry=${{ vars.REGISTRY_LOGIN_URL }}/${{ vars.REGISTRY_USERNAME }} -v")) } func yamlContains(yaml, substr string) cmp.Comparison { @@ -278,5 +322,15 @@ func yamlContains(yaml, substr string) cmp.Comparison { } } +func assertCustomWorkflow(t *testing.T, actualGw string) { + assert.Assert(t, yamlContains(actualGw, "Custom Deploy")) + assert.Assert(t, yamlContains(actualGw, "self-hosted")) + assert.Assert(t, yamlContains(actualGw, "DEV_CLUSTER_KUBECONFIG")) + assert.Assert(t, yamlContains(actualGw, "DEV_REGISTRY_LOGIN_URL")) + assert.Assert(t, yamlContains(actualGw, "DEV_REGISTRY_USER")) + assert.Assert(t, yamlContains(actualGw, "DEV_REGISTRY_PASS")) + assert.Assert(t, yamlContains(actualGw, "- master")) +} + // ---------------------- // END: Testing Framework