From 4f6c53f317a5650795672911e264c0c74d779b57 Mon Sep 17 00:00:00 2001 From: gauron99 Date: Sun, 13 Sep 2026 20:41:33 +0200 Subject: [PATCH 1/2] fix: PAC PipelineRun for pack fetches its Pipeline from .tekton --- pkg/pipelines/tekton/templates_pack.go | 3 +++ pkg/pipelines/tekton/templates_test.go | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pkg/pipelines/tekton/templates_pack.go b/pkg/pipelines/tekton/templates_pack.go index d4e73dc4b7..25a53f804d 100644 --- a/pkg/pipelines/tekton/templates_pack.go +++ b/pkg/pipelines/tekton/templates_pack.go @@ -156,6 +156,9 @@ metadata: # The branch or tag we are targeting (ie: main, refs/tags/*) pipelinesascode.tekton.dev/on-target-branch: "[{{.PipelinesTargetBranch}}]" + # Fetch the pipeline definition from the .tekton directory + pipelinesascode.tekton.dev/pipeline: {{.PipelineYamlURL}} + # How many runs we want to keep attached to this event pipelinesascode.tekton.dev/max-keep-runs: "5" diff --git a/pkg/pipelines/tekton/templates_test.go b/pkg/pipelines/tekton/templates_test.go index 7523ce931c..7fc519d184 100644 --- a/pkg/pipelines/tekton/templates_test.go +++ b/pkg/pipelines/tekton/templates_test.go @@ -2,7 +2,9 @@ package tekton import ( "bytes" + "os" "path/filepath" + "strings" "testing" "text/template" @@ -169,6 +171,22 @@ func Test_createPipelineRunTemplatePAC(t *testing.T) { t.Errorf("a pipeline run should be generated in %s", fp) return } + if tt.wantErr { + return + } + + // Pipelines-as-Code applies no Pipeline itself: the PipelineRun + // must tell it to fetch the one generated next to it, or its + // pipelineRef resolves only if a Pipeline of that name already + // exists on the cluster. + b, err := os.ReadFile(fp) + if err != nil { + t.Fatal(err) + } + want := "pipelinesascode.tekton.dev/pipeline: " + resourcesDirectory + "/" + pipelineFileNamePAC + if !strings.Contains(string(b), want) { + t.Errorf("expected the pipeline run to reference the generated pipeline with %q", want) + } }) } } From c2df65ccb14e627352c9b21db64112964b6b11aa Mon Sep 17 00:00:00 2001 From: gauron99 Date: Thu, 3 Sep 2026 08:02:18 +0200 Subject: [PATCH 2/2] feat: name the remote source with --source, --source-dir and --revision A remote build from a repository is named by its repository, a directory within it and a revision, and nothing about those values is specific to git: the revision is whatever git's fetch takes, a branch, a tag or a commit. deploy's --git-url, --git-dir and --git-branch are --source, --source-dir and --revision (FUNC_SOURCE, FUNC_SOURCE_DIR, FUNC_REVISION); the MCP deploy tool's fields follow. In func.yaml build.git (url, revision, contextDir) is build.source (url, revision, dir), with a migration that carries existing files over. config git set keeps --git-url, --git-branch and --git-dir: the command is git-only, webhooks come from git hosts, and its branch is the one whose pushes Pipelines-as-Code builds, a trigger filter rather than a revision, as the TODO at the flag records. It writes the same build.source keys. --- cmd/config_git_remove.go | 2 +- cmd/config_git_set.go | 21 ++-- cmd/deploy.go | 92 ++++++++-------- cmd/deploy_test.go | 102 +++++++++++------- docs/reference/func_config_git_set.md | 2 +- docs/reference/func_deploy.md | 14 +-- docs/reference/func_yaml.md | 15 +-- e2e/e2e_core_test.go | 8 +- e2e/e2e_remote_test.go | 18 ++-- .../testing/integration_test_helper.go | 6 +- pkg/functions/client_test.go | 4 +- pkg/functions/function.go | 7 +- pkg/functions/function_git.go | 34 ------ pkg/functions/function_git_unit_test.go | 74 ------------- pkg/functions/function_migrations.go | 62 ++++++++++- .../function_migrations_unit_test.go | 41 ++++++- pkg/functions/function_source.go | 41 +++++++ pkg/functions/function_source_unit_test.go | 90 ++++++++++++++++ .../testdata/migrations/v0.37.0/func.yaml | 9 ++ pkg/functions/testdata/source/.gitignore | 5 + pkg/functions/testdata/source/func.yaml | 9 ++ pkg/k8s/openshift.go | 4 +- pkg/mcp/tools_deploy.go | 12 +-- pkg/mcp/tools_deploy_test.go | 6 +- pkg/operator/syncer.go | 6 +- pkg/pipelines/tekton/gitlab_int_test.go | 2 +- .../tekton/pipelines_pac_provider.go | 6 +- .../tekton/pipelines_pac_provider_test.go | 2 +- pkg/pipelines/tekton/pipelines_provider.go | 2 +- pkg/pipelines/tekton/resources.go | 2 +- pkg/pipelines/tekton/resources_pac.go | 2 +- pkg/pipelines/tekton/templates.go | 10 +- schema/func_yaml-schema.json | 40 +++---- 33 files changed, 462 insertions(+), 288 deletions(-) delete mode 100644 pkg/functions/function_git.go delete mode 100644 pkg/functions/function_git_unit_test.go create mode 100644 pkg/functions/function_source.go create mode 100644 pkg/functions/function_source_unit_test.go create mode 100644 pkg/functions/testdata/migrations/v0.37.0/func.yaml create mode 100644 pkg/functions/testdata/source/.gitignore create mode 100644 pkg/functions/testdata/source/func.yaml diff --git a/cmd/config_git_remove.go b/cmd/config_git_remove.go index 50ddd350e7..9cdd254f71 100644 --- a/cmd/config_git_remove.go +++ b/cmd/config_git_remove.go @@ -133,7 +133,7 @@ func (c configGitRemoveConfig) Configure(f fn.Function) (fn.Function, error) { var err error if c.metadata.ConfigureLocalResources { - f.Build.Git = fn.Git{} + f.Build.Source = fn.Source{} } // Save the function which has now been updated with flags/config diff --git a/cmd/config_git_set.go b/cmd/config_git_set.go index 9b5998e595..77368853e4 100644 --- a/cmd/config_git_set.go +++ b/cmd/config_git_set.go @@ -68,8 +68,15 @@ func NewConfigGitSetCmd(newClient ClientFactory) *cobra.Command { fmt.Sprintf("The type of the Git platform provider to setup webhook. This value is usually automatically generated from input URL, use this parameter to override this setting. Currently supported providers are %s.", git.SupportedProvidersList.PrettyString())) cmd.Flags().StringP("git-url", "g", "", "Repository url containing the function to build ($FUNC_GIT_URL)") + // TODO: this appears to be a trigger filter, not a revision. Pipelines-as-Code + // matches it against the ref of each push (a branch name, or a pattern + // such as refs/tags/*), and the commit to build comes from the push + // event. It shares build.source.revision with deploy's --revision, which + // names one commit to build now, only because both persist to the same + // key. It should become a key of its own, and the PAC deploy step should + // take the revision it stamps from the event, not from func.yaml. cmd.Flags().StringP("git-branch", "t", "", - "Git revision (branch) to be used when deploying via the Git repository ($FUNC_GIT_BRANCH)") + "Git branch whose pushes Pipelines-as-Code builds and deploys ($FUNC_GIT_BRANCH)") cmd.Flags().StringP("git-dir", "d", "", "Directory in the Git repository containing the function (default is the root) ($FUNC_GIT_DIR)") @@ -157,7 +164,7 @@ func (c configGitSetConfig) Prompt(f fn.Function) (configGitSetConfig, error) { // prompt if git URL hasn't been set previously if c.GitURL == "" { - url := f.Build.Git.URL + url := f.Build.Source.URL if gitInfo.URL != "" { url = gitInfo.URL } @@ -172,7 +179,7 @@ func (c configGitSetConfig) Prompt(f fn.Function) (configGitSetConfig, error) { // prompt if git revision hasn't been set previously if c.GitRevision == "" { - revision := f.Build.Git.Revision + revision := f.Build.Source.Revision if gitInfo.Branch != "" { revision = gitInfo.Branch } @@ -188,7 +195,7 @@ func (c configGitSetConfig) Prompt(f fn.Function) (configGitSetConfig, error) { // prompt if contextDir hasn't been set previously if c.GitContextDir == "" { - contextDir := f.Build.Git.ContextDir + contextDir := f.Build.Source.Dir if err := survey.AskOne(&survey.Input{ Message: "A subpath within the repository:", Help: "A subpath within the repository where the source code of a function is located.", @@ -270,9 +277,9 @@ func (c configGitSetConfig) Configure(f fn.Function) (fn.Function, error) { f = c.buildConfig.Configure(f) // also configures .buildConfig.Global // Configure basic members - f.Build.Git.URL = c.GitURL - f.Build.Git.ContextDir = c.GitContextDir - f.Build.Git.Revision = c.GitRevision // TODO: should match; perhaps "refSpec" + f.Build.Source.URL = c.GitURL + f.Build.Source.Dir = c.GitContextDir + f.Build.Source.Revision = c.GitRevision // Save the function which has now been updated with flags/config if err = f.Write(); err != nil { // TODO: remove when client API uses 'f' diff --git a/cmd/deploy.go b/cmd/deploy.go index b8b8273efd..28451e1a51 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -34,7 +34,7 @@ NAME SYNOPSIS {{rootCmdUse}} deploy [-R|--remote] [-r|--registry] [-i|--image] [-n|--namespace] - [-e|--env] [-g|--git-url] [-t|--git-branch] [-d|--git-dir] + [-e|--env] [-s|--source] [-t|--revision] [-d|--source-dir] [-b|--build] [--builder] [--builder-image] [-p|--push] [--domain] [--platform] [--build-timestamp] [--pvc-size] [--service-account] [-c|--confirm] [-v|--verbose] @@ -79,8 +79,8 @@ DESCRIPTION invoked in the remote. Deploying with '{{rootCmdUse}} deploy --remote' will send the function's source code to be built and deployed by the cluster, eliminating the need for a local container engine. To trigger deployment - of a git repository instead of local source, combine with '--git-url': - '{{rootCmdUse}} deploy --remote --git-url=git.example.com/alice/f.git' + of a git repository instead of local source, combine with '--source': + '{{rootCmdUse}} deploy --remote --source=git.example.com/alice/f.git' Domain When deploying, a function's route is automatically generated using the @@ -120,7 +120,7 @@ EXAMPLES o Trigger a remote deploy, which instructs the cluster to build and deploy the function in the specified git repository. - $ {{rootCmdUse}} deploy --remote --git-url=https://example.com/alice/myfunc.git + $ {{rootCmdUse}} deploy --remote --source=https://example.com/alice/myfunc.git o Deploy the function, rebuilding the image even if no changes have been detected in the local filesystem (source). @@ -138,8 +138,8 @@ EXAMPLES `, SuggestFor: []string{"delpoy", "deplyo"}, PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image", - "base-image", "confirm", "domain", "env", "git-branch", "git-dir", - "git-url", "image", "image-pull-secret", "management-disabled", + "base-image", "confirm", "domain", "env", "revision", "source-dir", + "source", "image", "image-pull-secret", "management-disabled", "namespace", "path", "platform", "push", "pvc-size", "service-account", "deployer", "expose", "registry", "registry-insecure", "registry-authfile", "remote", "username", "password", "token", "verbose", "remote-storage-class"), @@ -191,12 +191,12 @@ EXAMPLES "To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).") cmd.Flags().String("domain", f.Domain, "Domain to use for the function's route. Cluster must be configured with domain matching for the given domain (ignored if unrecognized) ($FUNC_DOMAIN)") - cmd.Flags().StringP("git-url", "g", f.Build.Git.URL, - "Repository url containing the function to build ($FUNC_GIT_URL)") - cmd.Flags().StringP("git-branch", "t", f.Build.Git.Revision, - "Git revision (branch) to be used when deploying via the Git repository ($FUNC_GIT_BRANCH)") - cmd.Flags().StringP("git-dir", "d", f.Build.Git.ContextDir, - "Directory in the Git repository containing the function (default is the root) ($FUNC_GIT_DIR)") + cmd.Flags().StringP("source", "s", f.Build.Source.URL, + "Repository to build the function from on the cluster, instead of the local directory ($FUNC_SOURCE)") + cmd.Flags().StringP("revision", "t", f.Build.Source.Revision, + "Revision of --source to build: a branch, a tag or a commit (default is the repository's default branch) ($FUNC_REVISION)") + cmd.Flags().StringP("source-dir", "d", f.Build.Source.Dir, + "Directory within --source containing the function (default is the root) ($FUNC_SOURCE_DIR)") cmd.Flags().BoolP("remote", "R", f.Local.Remote, "Trigger a remote deployment. Default is to deploy and build from the local system ($FUNC_REMOTE)") cmd.Flags().StringP("remote-storage-class", "", f.Build.RemoteStorageClass, @@ -275,7 +275,7 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { // Check if function exists BEFORE prompting for config if !f.Initialized() { - if !cfg.Remote || f.Build.Git.URL == "" { + if !cfg.Remote || f.Build.Source.URL == "" { // Only error if this is not a fully remote build return NewErrNotInitializedFromPath(f.Root, "deploy") } else { @@ -564,14 +564,14 @@ type deployConfig struct { // the FWDN. Example `func delete www.example.com` Domain string - // Git branch for remote builds - GitBranch string + // Revision of Source to build for remote builds + Revision string - // Directory in the git repo where the function is located - GitDir string + // Directory within Source where the function is located + SourceDir string - // Git repo url for remote builds - GitURL string + // Source repository for remote builds + Source string // Namespace override for the deployed function. If provided, the // underlying platform will be instructed to deploy the function to the given @@ -622,9 +622,9 @@ func newDeployConfig(cmd *cobra.Command) deployConfig { Build: viper.GetString("build"), Env: viper.GetStringSlice("env"), Domain: viper.GetString("domain"), - GitBranch: viper.GetString("git-branch"), - GitDir: viper.GetString("git-dir"), - GitURL: viper.GetString("git-url"), + Revision: viper.GetString("revision"), + SourceDir: viper.GetString("source-dir"), + Source: viper.GetString("source"), Namespace: viper.GetString("namespace"), Remote: viper.GetBool("remote"), RemoteStorageClass: viper.GetString("remote-storage-class"), @@ -664,9 +664,9 @@ func (c deployConfig) Configure(f fn.Function) (fn.Function, error) { // Configure basic members f.Domain = c.Domain f.Namespace = c.Namespace - f.Build.Git.URL = c.GitURL - f.Build.Git.ContextDir = c.GitDir - f.Build.Git.Revision = c.GitBranch // TODO: should match; perhaps "refSpec" + f.Build.Source.URL = c.Source + f.Build.Source.Dir = c.SourceDir + f.Build.Source.Revision = c.Revision f.Build.RemoteStorageClass = c.RemoteStorageClass f.Deploy.ServiceAccountName = c.ServiceAccountName f.Deploy.ImagePullSecret = c.ImagePullSecret @@ -696,9 +696,9 @@ func (c deployConfig) Configure(f fn.Function) (fn.Function, error) { // TODO: the system should support specifying revision (refSpec) as a URL // fragment ([#]) throughout, which, when implemented, removes // the need for the below split into separate members: - if parts := strings.SplitN(c.GitURL, "#", 2); len(parts) == 2 { - f.Build.Git.URL = parts[0] - f.Build.Git.Revision = parts[1] + if parts := strings.SplitN(c.Source, "#", 2); len(parts) == 2 { + f.Build.Source.URL = parts[0] + f.Build.Source.Revision = parts[1] } return f, nil } @@ -753,10 +753,10 @@ func (c deployConfig) Prompt() (deployConfig, error) { if c.Remote { qs = []*survey.Question{ { - Name: "GitURL", + Name: "Source", Prompt: &survey.Input{ - Message: "URL to Git Repository for the remote to use (default is to send local source code)", - Default: c.GitURL, + Message: "Repository to build from on the cluster (default is to send the local source code)", + Default: c.Source, }, }, } @@ -765,8 +765,8 @@ func (c deployConfig) Prompt() (deployConfig, error) { } } - // TODO: prompt for optional additional git settings here: - // if c.GitURL != "" { + // TODO: prompt for optional additional source settings here: + // if c.Source != "" { // } return c, err @@ -831,14 +831,14 @@ func (c deployConfig) Validate(cmd *cobra.Command) (err error) { // Git references can only be supplied explicitly when coupled with --remote // See `printDeployMessages` which issues informative messages to the user // regarding this potentially confusing nuance. - if !c.Remote && (cmd.Flags().Changed("git-url") || cmd.Flags().Changed("git-dir") || cmd.Flags().Changed("git-branch")) { - return errors.New("git settings (--git-url --git-dir and --git-branch) are only applicable when triggering remote deployments (--remote)") + if !c.Remote && (cmd.Flags().Changed("source") || cmd.Flags().Changed("source-dir") || cmd.Flags().Changed("revision")) { + return errors.New("source settings (--source, --revision and --source-dir) are only applicable when triggering remote deployments (--remote)") } - // Git URL can contain at maximum one '#' - urlParts := strings.Split(c.GitURL, "#") + // Source can contain at maximum one '#' + urlParts := strings.Split(c.Source, "#") if len(urlParts) > 2 { - return fmt.Errorf("invalid --git-url '%v'", c.GitURL) + return fmt.Errorf("invalid --source '%v'", c.Source) } // NOTE: There is no explicit check for --registry or --image here, because @@ -922,29 +922,27 @@ func printDeployMessages(out io.Writer, f fn.Function) { // present. // // However, when building _locally_ thereafter, the deploy command should - // prefer the local source code, ignoring the values for --git-url etc. + // prefer the local source code, ignoring the values for --source etc. // Since this might be confusing, a warning is issued below that the local // function source does include a reference to a git repository, but that it // will be ignored in favor of the local source code since --remote was not // specified. - - // TODO update names of these to Source--Revision--Dir - if !f.Local.Remote && (f.Build.Git.URL != "" || f.Build.Git.Revision != "" || f.Build.Git.ContextDir != "") { - fmt.Fprintf(out, "Warning: git settings are only applicable when running with --remote. Local source code will be used.") + if !f.Local.Remote && (f.Build.Source.URL != "" || f.Build.Source.Revision != "" || f.Build.Source.Dir != "") { + fmt.Fprintf(out, "Warning: source settings are only applicable when running with --remote. Local source code will be used.") } // Git Branch Mismatch // ------------------- - // When doing a remote build with --git-branch, warn if the local branch + // When doing a remote build with --revision, warn if the local branch // doesn't match, as this can lead to confusion about which func.yaml is used. - if f.Local.Remote && f.Build.Git.URL != "" && f.Build.Git.Revision != "" { + if f.Local.Remote && f.Build.Source.URL != "" && f.Build.Source.Revision != "" { // Doing a remote build, specified a git repository to pull from, and // specified a reference within that remote. currentBranch, err := common.DefaultCurrentBranch(f.Root) if err != nil { fmt.Fprintf(out, "Warning: unable to verify local and remote references match. %v\n", err) - } else if currentBranch != f.Build.Git.Revision { - fmt.Fprintf(out, "Warning: Local git branch '%s' does not match --git-branch '%s'. The local func.yaml will be used for function metadata (name, runtime, etc). Ensure your local branch matches the remote branch to avoid deployment issues.\n", currentBranch, f.Build.Git.Revision) + } else if currentBranch != f.Build.Source.Revision { + fmt.Fprintf(out, "Warning: Local git branch '%s' does not match --revision '%s'. The local func.yaml will be used for function metadata (name, runtime, etc). Ensure your local branch matches the remote branch to avoid deployment issues.\n", currentBranch, f.Build.Source.Revision) } } } diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 906993f54e..a4c7b8c307 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -533,9 +533,9 @@ func testFunctionContext(cmdFn commandConstructor, t *testing.T) { } } -// TestDeploy_GitArgsPersist ensures that the git flags, if provided, are -// persisted to the Function for subsequent deployments. -func TestDeploy_GitArgsPersist(t *testing.T) { +// TestDeploy_RemoteSourcePersists ensures that the source flags, if provided, +// are persisted to the Function for subsequent deployments. +func TestDeploy_RemoteSourcePersists(t *testing.T) { root := FromTempDirectory(t) var ( @@ -550,12 +550,12 @@ func TestDeploy_GitArgsPersist(t *testing.T) { t.Fatal(err) } - // Deploy the Function specifying all of the git-related flags + // Deploy the Function specifying all of the source flags cmd := NewDeployCmd(NewTestClient( fn.WithPipelinesProvider(mock.NewPipelinesProvider()), fn.WithRegistry(TestRegistry), )) - cmd.SetArgs([]string{"--remote", "--git-url=" + url, "--git-branch=" + branch, "--git-dir=" + dir, "."}) + cmd.SetArgs([]string{"--remote", "--source=" + url, "--revision=" + branch, "--source-dir=" + dir, "."}) if err := cmd.Execute(); err != nil { t.Fatal(err) } @@ -565,20 +565,46 @@ func TestDeploy_GitArgsPersist(t *testing.T) { if err != nil { t.Fatal(err) } - if f.Build.Git.URL != url { - t.Errorf("expected git URL '%v' got '%v'", url, f.Build.Git.URL) + if want := (fn.Source{URL: url, Revision: branch, Dir: dir}); f.Build.Source != want { + t.Errorf("expected source %+v persisted, got %+v", want, f.Build.Source) } - if f.Build.Git.Revision != branch { - t.Errorf("expected git branch '%v' got '%v'", branch, f.Build.Git.Revision) +} + +// TestDeploy_RemoteSourceEnv ensures the source flags are also read from +// their environment variables, FUNC_SOURCE, FUNC_REVISION and +// FUNC_SOURCE_DIR. +func TestDeploy_RemoteSourceEnv(t *testing.T) { + root := FromTempDirectory(t) + + if _, err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + t.Setenv("FUNC_SOURCE", "https://example.com/user/repo") + t.Setenv("FUNC_REVISION", "v1.2.0") + t.Setenv("FUNC_SOURCE_DIR", "function") + + cmd := NewDeployCmd(NewTestClient( + fn.WithPipelinesProvider(mock.NewPipelinesProvider()), + fn.WithRegistry(TestRegistry), + )) + cmd.SetArgs([]string{"--remote"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) } - if f.Build.Git.ContextDir != dir { - t.Errorf("expected git dir '%v' got '%v'", dir, f.Build.Git.ContextDir) + want := fn.Source{URL: "https://example.com/user/repo", Revision: "v1.2.0", Dir: "function"} + if f.Build.Source != want { + t.Errorf("expected source %+v from the environment, got %+v", want, f.Build.Source) } } -// TestDeploy_GitArgsUsed ensures that any git values provided as flags are used +// TestDeploy_RemoteSourceUsed ensures that any source values provided as flags are used // when invoking a remote deployment. -func TestDeploy_GitArgsUsed(t *testing.T) { +func TestDeploy_RemoteSourceUsed(t *testing.T) { root := FromTempDirectory(t) var ( @@ -595,14 +621,14 @@ func TestDeploy_GitArgsUsed(t *testing.T) { // A Pipelines Provider which will validate the expected values were received pipeliner := mock.NewPipelinesProvider() pipeliner.RunFn = func(f fn.Function) (string, fn.Function, error) { - if f.Build.Git.URL != url { - t.Errorf("Pipeline Provider expected git URL '%v' got '%v'", url, f.Build.Git.URL) + if f.Build.Source.URL != url { + t.Errorf("Pipeline Provider expected git URL '%v' got '%v'", url, f.Build.Source.URL) } - if f.Build.Git.Revision != branch { - t.Errorf("Pipeline Provider expected git branch '%v' got '%v'", branch, f.Build.Git.Revision) + if f.Build.Source.Revision != branch { + t.Errorf("Pipeline Provider expected git branch '%v' got '%v'", branch, f.Build.Source.Revision) } - if f.Build.Git.ContextDir != dir { - t.Errorf("Pipeline Provider expected git dir '%v' got '%v'", url, f.Build.Git.ContextDir) + if f.Build.Source.Dir != dir { + t.Errorf("Pipeline Provider expected git dir '%v' got '%v'", url, f.Build.Source.Dir) } return url, f, nil } @@ -614,15 +640,15 @@ func TestDeploy_GitArgsUsed(t *testing.T) { fn.WithRegistry(TestRegistry), )) - cmd.SetArgs([]string{"--remote=true", "--git-url=" + url, "--git-branch=" + branch, "--git-dir=" + dir}) + cmd.SetArgs([]string{"--remote=true", "--source=" + url, "--revision=" + branch, "--source-dir=" + dir}) if err := cmd.Execute(); err != nil { t.Fatal(err) } } -// TestDeploy_GitURLBranch ensures that a --git-url which specifies the branch -// in the URL is equivalent to providing --git-branch -func TestDeploy_GitURLBranch(t *testing.T) { +// TestDeploy_RemoteSourceFragment ensures that a --source which specifies the branch +// in the URL is equivalent to providing --revision +func TestDeploy_RemoteSourceFragment(t *testing.T) { root := FromTempDirectory(t) f, err := fn.New().Init(fn.Function{Runtime: "go", Root: root}) @@ -641,7 +667,7 @@ func TestDeploy_GitURLBranch(t *testing.T) { fn.WithPipelinesProvider(mock.NewPipelinesProvider()), fn.WithRegistry(TestRegistry), )) - cmd.SetArgs([]string{"--remote", "--git-url=" + url}) + cmd.SetArgs([]string{"--remote", "--source=" + url}) if err := cmd.Execute(); err != nil { t.Fatal(err) @@ -651,11 +677,11 @@ func TestDeploy_GitURLBranch(t *testing.T) { if err != nil { t.Fatal(err) } - if f.Build.Git.URL != expectedUrl { - t.Errorf("expected git URL '%v' got '%v'", expectedUrl, f.Build.Git.URL) + if f.Build.Source.URL != expectedUrl { + t.Errorf("expected git URL '%v' got '%v'", expectedUrl, f.Build.Source.URL) } - if f.Build.Git.Revision != expectedBranch { - t.Errorf("expected git branch '%v' got '%v'", expectedBranch, f.Build.Git.Revision) + if f.Build.Source.Revision != expectedBranch { + t.Errorf("expected git branch '%v' got '%v'", expectedBranch, f.Build.Source.Revision) } } @@ -1587,7 +1613,7 @@ func testRegistryOrImageRequired(cmdFn commandConstructor, t *testing.T) { } } -// TestDeploy_RemoteBuildURLPermutations ensures that the remote, build and git-url flags +// TestDeploy_RemoteBuildURLPermutations ensures that the remote, build and source flags // are properly respected for all permutations, including empty. func TestDeploy_RemoteBuildURLPermutations(t *testing.T) { // Valid flag permutations (empty indicates flag should be omitted) @@ -1609,7 +1635,7 @@ func TestDeploy_RemoteBuildURLPermutations(t *testing.T) { args = append(args, fmt.Sprintf("--build=%v", build)) } if url != "" { - args = append(args, fmt.Sprintf("--git-url=%v", url)) + args = append(args, fmt.Sprintf("--source=%v", url)) } return args } @@ -1666,13 +1692,13 @@ func TestDeploy_RemoteBuildURLPermutations(t *testing.T) { } else { // LOCAL Assertions - // TODO: (enhancement) allow --git-url when running local deployment. + // TODO: (enhancement) allow --source when running local deployment. // Check that the local builder is invoked with a directive to use a // git repo rather than the local filesystem if building is enabled and - // a url is provided. For now it throws an error statign that git-url + // a url is provided. For now it throws an error statign that source // is only used when --remote if url != "" && err == nil { - t.Fatal("error expected when deploying from local but provided --git-url") + t.Fatal("error expected when deploying from local but provided --source") return } else if url != "" && err != nil { return // test successfully confirmed this is an error case @@ -1812,7 +1838,7 @@ func TestDeploy_UnsetFlag(t *testing.T) { // Deploy it, specifying a Git URL cmd := NewDeployCmd(NewTestClient()) - cmd.SetArgs([]string{"--remote", "--git-url=https://git.example.com/alice/f"}) + cmd.SetArgs([]string{"--remote", "--source=https://git.example.com/alice/f"}) if err := cmd.Execute(); err != nil { t.Fatal(err) } @@ -1822,13 +1848,13 @@ func TestDeploy_UnsetFlag(t *testing.T) { if err != nil { t.Fatal(err) } - if f.Build.Git.URL != "https://git.example.com/alice/f" { + if f.Build.Source.URL != "https://git.example.com/alice/f" { t.Fatalf("url not persisted") } // Deploy it again, unsetting the value cmd = NewDeployCmd(NewTestClient()) - cmd.SetArgs([]string{"--git-url="}) + cmd.SetArgs([]string{"--source="}) if err := cmd.Execute(); err != nil { t.Fatal(err) } @@ -1838,7 +1864,7 @@ func TestDeploy_UnsetFlag(t *testing.T) { if err != nil { t.Fatal(err) } - if f.Build.Git.URL != "" { + if f.Build.Source.URL != "" { t.Fatalf("url not cleared") } } @@ -3028,7 +3054,7 @@ func TestDeploy_RemoteExposeRecordsObservation(t *testing.T) { cmd.SetOut(&out) cmd.SetErr(&out) cmd.SetArgs([]string{"--remote", - "--git-url=https://example.com/user/repo", + "--source=https://example.com/user/repo", "--deployer=raw", "--expose=route"}) if err := cmd.Execute(); err != nil { t.Fatal(err) diff --git a/docs/reference/func_config_git_set.md b/docs/reference/func_config_git_set.md index 6e480265b8..ca9b11d144 100644 --- a/docs/reference/func_config_git_set.md +++ b/docs/reference/func_config_git_set.md @@ -24,7 +24,7 @@ func config git set --config-remote Configure remote resources (webhook on the Git provider side). --gh-access-token string GitHub Personal Access Token. For public repositories the scope is 'public_repo', for private is 'repo'. If you want to configure the webhook automatically, 'admin:repo_hook' is needed as well. Get more details: https://pipelines-as-code.pages.dev/docs/install/github_webhook/. --gh-webhook-secret string GitHub Webhook Secret used for payload validation. If not specified, it will be generated automatically. - -t, --git-branch string Git revision (branch) to be used when deploying via the Git repository ($FUNC_GIT_BRANCH) + -t, --git-branch string Git branch whose pushes Pipelines-as-Code builds and deploys ($FUNC_GIT_BRANCH) -d, --git-dir string Directory in the Git repository containing the function (default is the root) ($FUNC_GIT_DIR) --git-provider string The type of the Git platform provider to setup webhook. This value is usually automatically generated from input URL, use this parameter to override this setting. Currently supported providers are "github" and "gitlab". -g, --git-url string Repository url containing the function to build ($FUNC_GIT_URL) diff --git a/docs/reference/func_deploy.md b/docs/reference/func_deploy.md index 6fdb998af7..3e3013629b 100644 --- a/docs/reference/func_deploy.md +++ b/docs/reference/func_deploy.md @@ -10,7 +10,7 @@ NAME SYNOPSIS func deploy [-R|--remote] [-r|--registry] [-i|--image] [-n|--namespace] - [-e|--env] [-g|--git-url] [-t|--git-branch] [-d|--git-dir] + [-e|--env] [-s|--source] [-t|--revision] [-d|--source-dir] [-b|--build] [--builder] [--builder-image] [-p|--push] [--domain] [--platform] [--build-timestamp] [--pvc-size] [--service-account] [-c|--confirm] [-v|--verbose] @@ -55,8 +55,8 @@ DESCRIPTION invoked in the remote. Deploying with 'func deploy --remote' will send the function's source code to be built and deployed by the cluster, eliminating the need for a local container engine. To trigger deployment - of a git repository instead of local source, combine with '--git-url': - 'func deploy --remote --git-url=git.example.com/alice/f.git' + of a git repository instead of local source, combine with '--source': + 'func deploy --remote --source=git.example.com/alice/f.git' Domain When deploying, a function's route is automatically generated using the @@ -96,7 +96,7 @@ EXAMPLES o Trigger a remote deploy, which instructs the cluster to build and deploy the function in the specified git repository. - $ func deploy --remote --git-url=https://example.com/alice/myfunc.git + $ func deploy --remote --source=https://example.com/alice/myfunc.git o Deploy the function, rebuilding the image even if no changes have been detected in the local filesystem (source). @@ -130,9 +130,6 @@ func deploy --domain string Domain to use for the function's route. Cluster must be configured with domain matching for the given domain (ignored if unrecognized) ($FUNC_DOMAIN) -e, --env stringArray Environment variable to set in the form NAME=VALUE. You may provide this flag multiple times for setting multiple environment variables. To unset, specify the environment variable name followed by a "-" (e.g., NAME-). --expose string External exposure mode: 'route' for an OpenShift Route (OpenShift clusters only), 'none' for cluster-local. Default: no exposure. Raw and keda deployers only. ($FUNC_EXPOSE) - -t, --git-branch string Git revision (branch) to be used when deploying via the Git repository ($FUNC_GIT_BRANCH) - -d, --git-dir string Directory in the Git repository containing the function (default is the root) ($FUNC_GIT_DIR) - -g, --git-url string Repository url containing the function to build ($FUNC_GIT_URL) -h, --help help for deploy -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. ($FUNC_IMAGE) --image-pull-secret string Image pull secret to use when the function's image is in a private registry ($FUNC_IMAGE_PULL_SECRET) @@ -148,7 +145,10 @@ func deploy --registry-insecure Skip TLS certificate verification when communicating in HTTPS with the registry. The value is persisted over consecutive runs ($FUNC_REGISTRY_INSECURE) -R, --remote Trigger a remote deployment. Default is to deploy and build from the local system ($FUNC_REMOTE) --remote-storage-class string Specify a storage class to use for the volume on-cluster during remote builds + -t, --revision string Revision of --source to build: a branch, a tag or a commit (default is the repository's default branch) ($FUNC_REVISION) --service-account string Service account to be used in the deployed function ($FUNC_SERVICE_ACCOUNT) + -s, --source string Repository to build the function from on the cluster, instead of the local directory ($FUNC_SOURCE) + -d, --source-dir string Directory within --source containing the function (default is the root) ($FUNC_SOURCE_DIR) --token string Token to use when pushing to the registry. ($FUNC_TOKEN) --username string Username to use when pushing to the registry. ($FUNC_USERNAME) -v, --verbose Print verbose logs ($FUNC_VERBOSE) diff --git a/docs/reference/func_yaml.md b/docs/reference/func_yaml.md index 6c0eabba13..2b9060f110 100644 --- a/docs/reference/func_yaml.md +++ b/docs/reference/func_yaml.md @@ -35,15 +35,18 @@ build: s2i: example.com/user/my-s2i-node-builder ``` -### `git` +### `source` -If using a `git` build strategy, this field is used to specify the git URL as well -as an optional context directory. For example: +The repository a function is built from on the cluster, when it is not the +local directory: the `--source`, `--revision` and `--source-dir` of +`func deploy --remote`. The revision is a branch, a tag or a commit; the dir +is the directory within the repository holding the function. For example: ``` -git: - url: github.com/boson-project/example - contextDir: subdirectory +source: + url: https://github.com/boson-project/example + revision: main + dir: subdirectory ``` ### `buildEnvs` diff --git a/e2e/e2e_core_test.go b/e2e/e2e_core_test.go index 70811e6646..dd815378d7 100644 --- a/e2e/e2e_core_test.go +++ b/e2e/e2e_core_test.go @@ -136,9 +136,9 @@ func TestCore_Deploy_Template(t *testing.T) { // TestCore_Deploy_Source ensures that a function can be built and deployed // locally from source code housed in a remote source repository. -// func deploy --git-url={url} -// func deploy --git-url={url} --git-ref={ref} -// func deploy --git-url={url} --git-ref={ref} --git-dir={subdir} +// func deploy --source={url} +// func deploy --source={url} --revision={ref} +// func deploy --source={url} --revision={ref} --source-dir={subdir} func TestCore_Deploy_Source(t *testing.T) { t.Log("Not Implemented: running a local deploy from source code in a remote repo is not currently an implemented feature because this can be easily accomplished with `git clone ... && func deploy`") // Should this be a feature implemented in the future (mostly just a @@ -147,7 +147,7 @@ func TestCore_Deploy_Source(t *testing.T) { // name := "func-e2e-test-core-deploy-source" // _ = cdTemp(t, name) // sets Function name obliquely, see function docs // - // if err := newCmd(t, "deploy", "--git-url=https://github.com/functions-dev/func-e2e-tests").Run(); err != nil { + // if err := newCmd(t, "deploy", "--source=https://github.com/functions-dev/func-e2e-tests").Run(); err != nil { // t.Fatal(err) // } // defer func() { diff --git a/e2e/e2e_remote_test.go b/e2e/e2e_remote_test.go index 653a32a765..e122ee91d2 100644 --- a/e2e/e2e_remote_test.go +++ b/e2e/e2e_remote_test.go @@ -44,7 +44,7 @@ func TestRemote_Deploy(t *testing.T) { // TestRemote_Source ensures a remote build can be triggered which pulls // source from a remote repository. // -// func deploy --remote --git-url={url} --registry={} --builder=pack +// func deploy --remote --source={url} --registry={} --builder=pack func TestRemote_Source(t *testing.T) { name := "func-e2e-test-remote-source" _ = fromCleanEnv(t, name) @@ -58,7 +58,7 @@ func TestRemote_Source(t *testing.T) { // Trigger the deploy if err := newCmd(t, "deploy", "--remote", - "--git-url", "https://github.com/functions-dev/func-e2e-tests", + "--source", "https://github.com/functions-dev/func-e2e-tests", "--registry", Registry, "--builder", "pack", ).Run(); err != nil { @@ -101,8 +101,8 @@ func TestRemote_Ref(t *testing.T) { // Trigger the deploy if err := newCmd(t, "deploy", "--remote", - "--git-url", "https://github.com/functions-dev/func-e2e-tests", - "--git-branch", name, + "--source", "https://github.com/functions-dev/func-e2e-tests", + "--revision", name, "--registry", Registry, "--builder", "pack", "--build", @@ -122,8 +122,8 @@ func TestRemote_Ref(t *testing.T) { // TestRemote_Dir ensures that remote builds can be instructed to build and // deploy a function located in a subdirectory. // -// func deploy --remote --git-dir={subdir} -// func deploy --remote --git-dir={subdir} --git-url={url} +// func deploy --remote --source-dir={subdir} +// func deploy --remote --source-dir={subdir} --source={url} func TestRemote_Dir(t *testing.T) { name := "func-e2e-test-remote-dir" _ = fromCleanEnv(t, name) @@ -135,7 +135,7 @@ func TestRemote_Dir(t *testing.T) { t.Fatal(err) } - // IMPORTANT: When using --git-dir, we need to change to that directory locally + // IMPORTANT: When using --source-dir, we need to change to that directory locally // to ensure the local func.yaml matches the one that will be used in the remote build. // This is a current limitation where remote builds still require local source to // determine function metadata (name, runtime, etc). @@ -148,8 +148,8 @@ func TestRemote_Dir(t *testing.T) { // Trigger the deploy if err := newCmd(t, "deploy", "--remote", - "--git-url", "https://github.com/functions-dev/func-e2e-tests", - "--git-dir", name, + "--source", "https://github.com/functions-dev/func-e2e-tests", + "--source-dir", name, "--registry", Registry, "--builder", "pack", "--build", diff --git a/pkg/deployer/testing/integration_test_helper.go b/pkg/deployer/testing/integration_test_helper.go index baaad2ea77..7ca2550624 100644 --- a/pkg/deployer/testing/integration_test_helper.go +++ b/pkg/deployer/testing/integration_test_helper.go @@ -1263,9 +1263,9 @@ func TestInt_OperatorSync(t *testing.T, deployer fn.Deployer, remover fn.Remover t.Fatal(err) } - f.Build.Git.URL = repoURL - f.Build.Git.Revision = repoBranch - f.Build.Git.ContextDir = repoPath + f.Build.Source.URL = repoURL + f.Build.Source.Revision = repoBranch + f.Build.Source.Dir = repoPath err = client.Scaffold(ctx, f, "") if err != nil { diff --git a/pkg/functions/client_test.go b/pkg/functions/client_test.go index 028a27c7c3..ebe5d4e06c 100644 --- a/pkg/functions/client_test.go +++ b/pkg/functions/client_test.go @@ -1479,7 +1479,7 @@ func TestClient_Pipelines_Deploy_Image(t *testing.T) { Runtime: "node", Root: root, Build: fn.BuildSpec{ - Git: fn.Git{URL: "http://example-git.com/alice/myfunc.git"}, + Source: fn.Source{URL: "http://example-git.com/alice/myfunc.git"}, }, } @@ -1557,7 +1557,7 @@ func TestClient_Pipelines_Deploy_Namespace(t *testing.T) { Root: root, Namespace: "myns", Build: fn.BuildSpec{ - Git: fn.Git{URL: "http://example-git.com/alice/myfunc.git"}, + Source: fn.Source{URL: "http://example-git.com/alice/myfunc.git"}, }, } diff --git a/pkg/functions/function.go b/pkg/functions/function.go index e40cae3750..1e6906fbb9 100644 --- a/pkg/functions/function.go +++ b/pkg/functions/function.go @@ -143,8 +143,9 @@ type KnativeSubscription struct { // BuildSpec type BuildSpec struct { - // Git stores information about an optionally associated git repository. - Git Git `yaml:"git,omitempty"` + // Source is the repository the function is built from on the cluster, + // when it is not the local directory: url, revision and dir. + Source Source `yaml:"source,omitempty"` // BuilderImages define optional explicit builder images to use by // builder implementations in leau of the in-code defaults. They key @@ -480,7 +481,7 @@ func (f Function) Validate() error { ValidateEnvs(f.Run.Envs), validateOptions(f.Deploy.Options), ValidateLabels(f.Deploy.Labels), - validateGit(f.Build.Git), + validateSource(f.Build.Source), validateKafka(f.Run.Kafka, f.Invoke, f.Runtime), validateExpose(f.Deploy.Expose, f.Expose), } diff --git a/pkg/functions/function_git.go b/pkg/functions/function_git.go deleted file mode 100644 index b3122ac791..0000000000 --- a/pkg/functions/function_git.go +++ /dev/null @@ -1,34 +0,0 @@ -package functions - -import ( - "fmt" - "strings" - - giturls "github.com/chainguard-dev/git-urls" -) - -type Git struct { - URL string `yaml:"url,omitempty"` - Revision string `yaml:"revision,omitempty"` - ContextDir string `yaml:"contextDir,omitempty"` -} - -// validateGit validates input Git option from Function config -func validateGit(git Git) (errors []string) { - if git.URL != "" { - _, err := giturls.ParseTransport(git.URL) - if err != nil { - _, err = giturls.ParseScp(git.URL) - } - if err != nil { - errMsg := fmt.Sprintf("specified option \"git.url=%s\" is not valid", git.URL) - - originalErr := err.Error() - if !strings.HasSuffix(originalErr, "is not a valid transport") { - errMsg = fmt.Sprintf("%s, error: %s", errMsg, originalErr) - } - errors = append(errors, errMsg) - } - } - return -} diff --git a/pkg/functions/function_git_unit_test.go b/pkg/functions/function_git_unit_test.go deleted file mode 100644 index 4243bf7c69..0000000000 --- a/pkg/functions/function_git_unit_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package functions - -import ( - "testing" -) - -func Test_validateGit(t *testing.T) { - - tests := []struct { - name string - git Git - errs int - }{ - { - "correct 'Git - only URL https", - Git{ - URL: "https://myrepo/foo.git", - }, - 0, - }, - { - "correct 'Git - only URL scp", - Git{ - URL: "git@myrepo:foo.git", - }, - 0, - }, - { - "correct 'Git - URL + revision", - Git{ - URL: "https://myrepo/foo.git", - Revision: "mybranch", - }, - 0, - }, - { - "correct 'Git - URL + context-dir", - Git{ - URL: "https://myrepo/foo.git", - ContextDir: "my-folder", - }, - 0, - }, - { - "correct 'Git - URL + revision & context-dir", - Git{ - URL: "https://myrepo/foo.git", - Revision: "mybranch", - ContextDir: "my-folder", - }, - 0, - }, - { - "incorrect 'Git - bad URL", - Git{ - URL: "foo", - }, - 1, - }, - { - "correct 'Git - not mandatory", - Git{}, - 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := validateGit(tt.git); len(got) != tt.errs { - t.Errorf("validateGit() = %v\n got %d errors but want %d", got, len(got), tt.errs) - } - }) - } -} diff --git a/pkg/functions/function_migrations.go b/pkg/functions/function_migrations.go index 42995cc59c..1da8a82b25 100644 --- a/pkg/functions/function_migrations.go +++ b/pkg/functions/function_migrations.go @@ -99,6 +99,7 @@ var migrations = []migration{ {"0.34.0", migrateToSpecsStructure}, {"0.35.0", migrateFromInvokeStructure}, {"0.36.0", migratePersistentVolumeTypoFixup}, + {"0.37.0", migrateGitToSource}, // New Migrations Here. } @@ -239,13 +240,13 @@ func migrateToSpecsStructure(f1 Function, m migration) (Function, error) { } if f0.Git.URL != "" { - f1.Build.Git.URL = f0.Git.URL + f1.Build.Source.URL = f0.Git.URL } if f0.Git.Revision != "" { - f1.Build.Git.Revision = f0.Git.Revision + f1.Build.Source.Revision = f0.Git.Revision } if f0.Git.ContextDir != "" { - f1.Build.Git.ContextDir = f0.Git.ContextDir + f1.Build.Source.Dir = f0.Git.ContextDir } //Append BuilderImages from old format, without destroying previous migrations if f0.BuilderImages != nil { @@ -356,6 +357,55 @@ func migratePersistentVolumeTypoFixup(fn Function, m migration) (Function, error return fn, nil } +// migrateGitToSource +// The repository a function is built from was build.git, with url, revision +// and contextDir. It is build.source, with url, revision and dir: the flags +// are --source, --revision and --source-dir, and nothing about the values +// is specific to git. The old keys are carried over; the next write stores +// the new ones. +func migrateGitToSource(fn Function, m migration) (Function, error) { + f, err := os.Open(filepath.Join(fn.Root, FunctionFile)) + if err != nil { + return Function{}, fmt.Errorf("cannot open func.yaml: %w", err) + } + defer f.Close() + + // Before the specs structure (0.34.0), build was the build type as a + // string, so the key is read loosely. + data := struct { + Build interface{} `yaml:"build"` + }{} + if err = yaml.NewDecoder(f).Decode(&data); err != nil { + return Function{}, fmt.Errorf("cannot deserialize old sub-structure: %w", err) + } + + if fn.Build.Source.URL == "" { + fn.Build.Source.URL = nestedString(data.Build, "git", "url") + } + if fn.Build.Source.Revision == "" { + fn.Build.Source.Revision = nestedString(data.Build, "git", "revision") + } + if fn.Build.Source.Dir == "" { + fn.Build.Source.Dir = nestedString(data.Build, "git", "contextDir") + } + fn.SpecVersion = m.version + return fn, nil +} + +// nestedString returns the string at the given path of keys within a +// decoded YAML value, or "" when the path does not lead to a string. +func nestedString(v interface{}, keys ...string) string { + for _, key := range keys { + m, ok := v.(map[interface{}]interface{}) + if !ok { + return "" + } + v = m[key] + } + s, _ := v.(string) + return s +} + // The pertinent aspects of the Function's schema prior the 1.0.0 version migrations type migrateToSpecs_previousFunction struct { @@ -364,7 +414,11 @@ type migrateToSpecs_previousFunction struct { // Git stores information about remote git repository, // in case build type "git" is being used - Git Git `yaml:"git"` + Git struct { + URL string `yaml:"url"` + Revision string `yaml:"revision"` + ContextDir string `yaml:"contextDir"` + } `yaml:"git"` // BuilderImages define optional explicit builder images to use by // builder implementations in leau of the in-code defaults. They key diff --git a/pkg/functions/function_migrations_unit_test.go b/pkg/functions/function_migrations_unit_test.go index b56abffaf0..65fbbcf7be 100644 --- a/pkg/functions/function_migrations_unit_test.go +++ b/pkg/functions/function_migrations_unit_test.go @@ -2,6 +2,7 @@ package functions import ( "os" + "path/filepath" "strings" "sync" "testing" @@ -146,7 +147,7 @@ func TestMigrateToSpecVersion(t *testing.T) { func TestMigrateToSpecs(t *testing.T) { root := "testdata/migrations/v0.34.0" - expectedGit := Git{URL: "http://test-url", Revision: "test revision", ContextDir: "/test/context/dir"} + expectedGit := Source{URL: "http://test-url", Revision: "test revision", Dir: "/test/context/dir"} expectedNamespace := "test-namespace" var expectedEnvs []Env var expectedVolumes []Volume @@ -157,8 +158,8 @@ func TestMigrateToSpecs(t *testing.T) { t.Fatal(f) } - if f.Build.Git != expectedGit { - t.Fatalf("migrated Function expected Git '%v', got '%v'", expectedGit, f.Build.Git) + if f.Build.Source != expectedGit { + t.Fatalf("migrated Function expected Source '%v', got '%v'", expectedGit, f.Build.Source) } if f.Deploy.Namespace != expectedNamespace { @@ -316,3 +317,37 @@ func writeFunc(f Function, root string) error { } return os.WriteFile(root+"/func.yaml", bb, 0644) } + +// TestMigrateGitToSource ensures the former build.git keys (url, revision, +// contextDir) are carried over into build.source (url, revision, dir), and +// written back under the new keys. +func TestMigrateGitToSource(t *testing.T) { + f, err := NewFunction("testdata/migrations/v0.37.0") + if err != nil { + t.Fatal(err) + } + want := Source{URL: "https://example.com/alice/testfunc.git", Revision: "feature", Dir: "functions/testfunc"} + if f.Build.Source != want { + t.Fatalf("migrated Function expected source %+v, got %+v", want, f.Build.Source) + } + if f.SpecVersion != LastSpecVersion() { + t.Errorf("expected specVersion %q, got %q", LastSpecVersion(), f.SpecVersion) + } + + f.Root = t.TempDir() + if err := f.Write(); err != nil { + t.Fatal(err) + } + bb, err := os.ReadFile(filepath.Join(f.Root, FunctionFile)) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"source:", "url: https://example.com/alice/testfunc.git", "revision: feature", "dir: functions/testfunc"} { + if !strings.Contains(string(bb), want) { + t.Errorf("expected %q in the written func.yaml, got:\n%s", want, bb) + } + } + if strings.Contains(string(bb), "git:") || strings.Contains(string(bb), "contextDir:") { + t.Errorf("expected no build.git keys in the written func.yaml, got:\n%s", bb) + } +} diff --git a/pkg/functions/function_source.go b/pkg/functions/function_source.go new file mode 100644 index 0000000000..c4f2576eda --- /dev/null +++ b/pkg/functions/function_source.go @@ -0,0 +1,41 @@ +package functions + +import ( + "fmt" + "strings" + + giturls "github.com/chainguard-dev/git-urls" +) + +// Source is the repository a function is built from on the cluster: the +// values of --source, --revision and --source-dir. +type Source struct { + // URL of the repository. + URL string `yaml:"url,omitempty"` + // Revision to build: a branch, a tag or a commit, as git's fetch takes it. + // Empty means the remote's default branch. + Revision string `yaml:"revision,omitempty"` + // Dir is the directory within the repository holding the function. + // Empty means the repository root. + Dir string `yaml:"dir,omitempty"` +} + +// validateSource validates the source option from Function config +func validateSource(source Source) (errors []string) { + if source.URL != "" { + _, err := giturls.ParseTransport(source.URL) + if err != nil { + _, err = giturls.ParseScp(source.URL) + } + if err != nil { + errMsg := fmt.Sprintf("specified option \"source.url=%s\" is not valid", source.URL) + + originalErr := err.Error() + if !strings.HasSuffix(originalErr, "is not a valid transport") { + errMsg = fmt.Sprintf("%s, error: %s", errMsg, originalErr) + } + errors = append(errors, errMsg) + } + } + return +} diff --git a/pkg/functions/function_source_unit_test.go b/pkg/functions/function_source_unit_test.go new file mode 100644 index 0000000000..93be5217b5 --- /dev/null +++ b/pkg/functions/function_source_unit_test.go @@ -0,0 +1,90 @@ +package functions + +import ( + "testing" +) + +// TestSource_Load ensures a func.yaml with build.source is read as written: +// url, revision and dir, with no migration and no warning involved. +func TestSource_Load(t *testing.T) { + f, err := NewFunction("testdata/source") + if err != nil { + t.Fatal(err) + } + want := Source{URL: "https://example.com/alice/testfunc.git", Revision: "v1.2.0", Dir: "functions/testfunc"} + if f.Build.Source != want { + t.Errorf("expected source %+v, got %+v", want, f.Build.Source) + } + if f.SpecVersion != LastSpecVersion() { + t.Errorf("expected the fixture at the latest spec version %q, got %q", LastSpecVersion(), f.SpecVersion) + } +} + +func Test_validateSource(t *testing.T) { + + tests := []struct { + name string + source Source + errs int + }{ + { + "correct 'Source - only URL https", + Source{ + URL: "https://myrepo/foo.git", + }, + 0, + }, + { + "correct 'Source - only URL scp", + Source{ + URL: "git@myrepo:foo.git", + }, + 0, + }, + { + "correct 'Source - URL + revision", + Source{ + URL: "https://myrepo/foo.git", + Revision: "mybranch", + }, + 0, + }, + { + "correct 'Source - URL + dir", + Source{ + URL: "https://myrepo/foo.git", + Dir: "my-folder", + }, + 0, + }, + { + "correct 'Source - URL + revision & dir", + Source{ + URL: "https://myrepo/foo.git", + Revision: "mybranch", + Dir: "my-folder", + }, + 0, + }, + { + "incorrect 'Source - bad URL", + Source{ + URL: "foo", + }, + 1, + }, + { + "correct 'Source - not mandatory", + Source{}, + 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := validateSource(tt.source); len(got) != tt.errs { + t.Errorf("validateSource() = %v\n got %d errors but want %d", got, len(got), tt.errs) + } + }) + } +} diff --git a/pkg/functions/testdata/migrations/v0.37.0/func.yaml b/pkg/functions/testdata/migrations/v0.37.0/func.yaml new file mode 100644 index 0000000000..700e31ebaf --- /dev/null +++ b/pkg/functions/testdata/migrations/v0.37.0/func.yaml @@ -0,0 +1,9 @@ +specVersion: 0.36.0 +name: testfunc +runtime: go +created: 2023-01-01T00:00:00.000000+00:00 +build: + git: + url: https://example.com/alice/testfunc.git + revision: feature + contextDir: functions/testfunc diff --git a/pkg/functions/testdata/source/.gitignore b/pkg/functions/testdata/source/.gitignore new file mode 100644 index 0000000000..965f0d4ef0 --- /dev/null +++ b/pkg/functions/testdata/source/.gitignore @@ -0,0 +1,5 @@ + +# Functions use the .func directory for local runtime data which should +# generally not be tracked in source control. To instruct the system to track +# .func in source control, comment the following line (prefix it with '# '). +/.func diff --git a/pkg/functions/testdata/source/func.yaml b/pkg/functions/testdata/source/func.yaml new file mode 100644 index 0000000000..c1fb769745 --- /dev/null +++ b/pkg/functions/testdata/source/func.yaml @@ -0,0 +1,9 @@ +specVersion: 0.37.0 +name: testfunc +runtime: go +created: 2026-01-01T00:00:00.000000+00:00 +build: + source: + url: https://example.com/alice/testfunc.git + revision: v1.2.0 + dir: functions/testfunc diff --git a/pkg/k8s/openshift.go b/pkg/k8s/openshift.go index f47258618c..66bd461858 100644 --- a/pkg/k8s/openshift.go +++ b/pkg/k8s/openshift.go @@ -209,8 +209,8 @@ func (o OpenshiftMetadataDecorator) UpdateAnnotations(f fn.Function, annotations if annotations == nil { annotations = map[string]string{} } - annotations[annotationOpenShiftVcsUri] = f.Build.Git.URL - annotations[annotationOpenShiftVcsRef] = f.Build.Git.Revision + annotations[annotationOpenShiftVcsUri] = f.Build.Source.URL + annotations[annotationOpenShiftVcsRef] = f.Build.Source.Revision return annotations } diff --git a/pkg/mcp/tools_deploy.go b/pkg/mcp/tools_deploy.go index 1cdc8670cb..2734fe0eb6 100644 --- a/pkg/mcp/tools_deploy.go +++ b/pkg/mcp/tools_deploy.go @@ -42,9 +42,9 @@ type DeployInput struct { Registry *string `json:"registry,omitempty" jsonschema:"Container registry for function image"` Image *string `json:"image,omitempty" jsonschema:"Full image name (overrides registry)"` Namespace *string `json:"namespace,omitempty" jsonschema:"Kubernetes namespace to deploy into"` - GitURL *string `json:"gitUrl,omitempty" jsonschema:"Git URL containing the function source"` - GitBranch *string `json:"gitBranch,omitempty" jsonschema:"Git branch for remote deployment"` - GitDir *string `json:"gitDir,omitempty" jsonschema:"Directory inside the Git repository"` + Source *string `json:"source,omitempty" jsonschema:"Repository to build the function from on the cluster, for remote deployment"` + Revision *string `json:"revision,omitempty" jsonschema:"Revision of source to build: a branch, a tag or a commit"` + SourceDir *string `json:"sourceDir,omitempty" jsonschema:"Directory within source containing the function"` BuilderImage *string `json:"builderImage,omitempty" jsonschema:"Custom builder image"` Domain *string `json:"domain,omitempty" jsonschema:"Domain for the function route"` Platform *string `json:"platform,omitempty" jsonschema:"Target platform (e.g., linux/amd64)"` @@ -66,9 +66,9 @@ func (i DeployInput) Args() []string { args = appendStringFlag(args, "--registry", i.Registry) args = appendStringFlag(args, "--image", i.Image) args = appendStringFlag(args, "--namespace", i.Namespace) - args = appendStringFlag(args, "--git-url", i.GitURL) - args = appendStringFlag(args, "--git-branch", i.GitBranch) - args = appendStringFlag(args, "--git-dir", i.GitDir) + args = appendStringFlag(args, "--source", i.Source) + args = appendStringFlag(args, "--revision", i.Revision) + args = appendStringFlag(args, "--source-dir", i.SourceDir) args = appendStringFlag(args, "--builder-image", i.BuilderImage) args = appendStringFlag(args, "--domain", i.Domain) args = appendStringFlag(args, "--platform", i.Platform) diff --git a/pkg/mcp/tools_deploy_test.go b/pkg/mcp/tools_deploy_test.go index 940ded5fe5..5184843b19 100644 --- a/pkg/mcp/tools_deploy_test.go +++ b/pkg/mcp/tools_deploy_test.go @@ -46,9 +46,9 @@ func TestTool_Deploy_Args(t *testing.T) { "registry": {"registry", "--registry", "ghcr.io/user"}, "image": {"image", "--image", "ghcr.io/user/my-func:latest"}, "namespace": {"namespace", "--namespace", "prod"}, - "gitUrl": {"gitUrl", "--git-url", "https://github.com/user/repo"}, - "gitBranch": {"gitBranch", "--git-branch", "main"}, - "gitDir": {"gitDir", "--git-dir", "functions/my-func"}, + "source": {"source", "--source", "https://github.com/user/repo"}, + "revision": {"revision", "--revision", "main"}, + "sourceDir": {"sourceDir", "--source-dir", "functions/my-func"}, "builderImage": {"builderImage", "--builder-image", "custom-builder:latest"}, "domain": {"domain", "--domain", "example.com"}, "platform": {"platform", "--platform", "linux/amd64"}, diff --git a/pkg/operator/syncer.go b/pkg/operator/syncer.go index a54af32297..24b316c97a 100644 --- a/pkg/operator/syncer.go +++ b/pkg/operator/syncer.go @@ -31,9 +31,9 @@ func WithCredentialsProvider(cp oci.CredentialsProvider) SyncerOpt { } func (s *Syncer) Sync(ctx context.Context, f fn.Function) error { - repoURL := f.Build.Git.URL - repoBranch := f.Build.Git.Revision - repoPath := f.Build.Git.ContextDir + repoURL := f.Build.Source.URL + repoBranch := f.Build.Source.Revision + repoPath := f.Build.Source.Dir if repoURL == "" { resolved, err := funcgit.ResolveRemoteURL(f.Root) diff --git a/pkg/pipelines/tekton/gitlab_int_test.go b/pkg/pipelines/tekton/gitlab_int_test.go index b13e56ddaa..73bb050db2 100644 --- a/pkg/pipelines/tekton/gitlab_int_test.go +++ b/pkg/pipelines/tekton/gitlab_int_test.go @@ -85,7 +85,7 @@ func TestInt_Gitlab(t *testing.T) { Created: time.Now(), Invoke: "none", Build: fn.BuildSpec{ - Git: fn.Git{ + Source: fn.Source{ URL: strings.TrimSuffix(glabEnv.HTTPProjectURL, ".git"), Revision: "devel", }, diff --git a/pkg/pipelines/tekton/pipelines_pac_provider.go b/pkg/pipelines/tekton/pipelines_pac_provider.go index 87e3bd2fd6..73d8a209ca 100644 --- a/pkg/pipelines/tekton/pipelines_pac_provider.go +++ b/pkg/pipelines/tekton/pipelines_pac_provider.go @@ -258,14 +258,14 @@ func (pp *PipelinesProvider) createRemotePACResources(ctx context.Context, f fn. } } - if err := git.CreateWebHook(ctx, f.Build.Git.URL, controllerURL, metadata.WebhookSecret, metadata.PersonalAccessToken); err != nil { + if err := git.CreateWebHook(ctx, f.Build.Source.URL, controllerURL, metadata.WebhookSecret, metadata.PersonalAccessToken); err != nil { // Error: POST https://api.github.com/repos/foobar/test-function/hooks: 422 Validation Failed [{Resource:Hook Field: Code:custom Message:Hook already exists on this repository}] if !strings.Contains(err.Error(), "Hook already exists") { return err } - fmt.Printf(" ✅ Webhook already exists on repository %v\n", f.Build.Git.URL) + fmt.Printf(" ✅ Webhook already exists on repository %v\n", f.Build.Source.URL) } else { - fmt.Printf(" ✅ Webhook is created on repository %v\n", f.Build.Git.URL) + fmt.Printf(" ✅ Webhook is created on repository %v\n", f.Build.Source.URL) } return nil diff --git a/pkg/pipelines/tekton/pipelines_pac_provider_test.go b/pkg/pipelines/tekton/pipelines_pac_provider_test.go index 8fe536f6a1..5e4e32dc0e 100644 --- a/pkg/pipelines/tekton/pipelines_pac_provider_test.go +++ b/pkg/pipelines/tekton/pipelines_pac_provider_test.go @@ -70,7 +70,7 @@ func Test_deleteAllPipelineTemplates(t *testing.T) { } f.Build.Builder = builders.Pack - f.Build.Git.URL = "https://foo.bar/repo/function" + f.Build.Source.URL = "https://foo.bar/repo/function" f.Image = "docker.io/alice/" + f.Name f.Registry = TestRegistry diff --git a/pkg/pipelines/tekton/pipelines_provider.go b/pkg/pipelines/tekton/pipelines_provider.go index bba07e4da2..564e4ea7f6 100644 --- a/pkg/pipelines/tekton/pipelines_provider.go +++ b/pkg/pipelines/tekton/pipelines_provider.go @@ -186,7 +186,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn return "", f, err } - if f.Build.Git.URL == "" { + if f.Build.Source.URL == "" { // Use direct upload to PVC if Git is not set up. // The uploaded func.yaml is synthesized from this in-memory f (see diff --git a/pkg/pipelines/tekton/resources.go b/pkg/pipelines/tekton/resources.go index be9c36b10a..c76031be95 100644 --- a/pkg/pipelines/tekton/resources.go +++ b/pkg/pipelines/tekton/resources.go @@ -54,7 +54,7 @@ func getBuilderImage(f fn.Function) (name string) { func getPipelineName(f fn.Function) string { var source string - if f.Build.Git.URL == "" { + if f.Build.Source.URL == "" { source = "upload" } else { source = "git" diff --git a/pkg/pipelines/tekton/resources_pac.go b/pkg/pipelines/tekton/resources_pac.go index cff5fa33e3..646968572d 100644 --- a/pkg/pipelines/tekton/resources_pac.go +++ b/pkg/pipelines/tekton/resources_pac.go @@ -55,7 +55,7 @@ func ensurePACRepositoryExists(ctx context.Context, f fn.Function, namespace str Annotations: f.Deploy.Annotations, }, Spec: v1alpha1.RepositorySpec{ - URL: f.Build.Git.URL, + URL: f.Build.Source.URL, GitProvider: &v1alpha1.GitProvider{ Type: metadata.GitProvider, Secret: &v1alpha1.Secret{ diff --git a/pkg/pipelines/tekton/templates.go b/pkg/pipelines/tekton/templates.go index b853194bcf..362ce1a057 100644 --- a/pkg/pipelines/tekton/templates.go +++ b/pkg/pipelines/tekton/templates.go @@ -143,14 +143,14 @@ func createPipelineTemplatePAC(f fn.Function, labels map[string]string) error { // createPipelineRunTemplatePAC creates a PipelineRun template used for PAC on-cluster build // it creates the resource in the project directory func createPipelineRunTemplatePAC(f fn.Function, labels map[string]string) error { - contextDir := f.Build.Git.ContextDir + contextDir := f.Build.Source.Dir if contextDir == "" && f.Build.Builder == builders.S2I { // TODO(lkingland): could instead update S2I to interpret empty string // as cwd, such that builder-specific code can be kept out of here. contextDir = "." } - pipelinesTargetBranch := f.Build.Git.Revision + pipelinesTargetBranch := f.Build.Source.Revision if pipelinesTargetBranch == "" { pipelinesTargetBranch = defaultPipelinesTargetBranch } @@ -350,14 +350,14 @@ func createAndApplyPipelineTemplate(f fn.Function, namespace string, labels map[ // createAndApplyPipelineRunTemplate creates and applies PipelineRun template for a standard on-cluster build // all resources are created on the fly, if there's a PipelineRun defined in the project directory, it is used instead func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels map[string]string) error { - contextDir := f.Build.Git.ContextDir + contextDir := f.Build.Source.Dir if contextDir == "" && f.Build.Builder == builders.S2I { // TODO(lkingland): could instead update S2I to interpret empty string // as cwd, such that builder-specific code can be kept out of here. contextDir = "." } - pipelinesTargetBranch := f.Build.Git.Revision + pipelinesTargetBranch := f.Build.Source.Revision if pipelinesTargetBranch == "" { pipelinesTargetBranch = defaultPipelinesTargetBranch } @@ -410,7 +410,7 @@ func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels m TlsVerify: tlsVerify, Commit: commit, - RepoUrl: f.Build.Git.URL, + RepoUrl: f.Build.Source.URL, Revision: pipelinesTargetBranch, } diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json index ee142243d3..429123c16c 100644 --- a/schema/func_yaml-schema.json +++ b/schema/func_yaml-schema.json @@ -4,10 +4,10 @@ "definitions": { "BuildSpec": { "properties": { - "git": { + "source": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/Git", - "description": "Git stores information about an optionally associated git repository." + "$ref": "#/definitions/Source", + "description": "Source is the repository the function is built from on the cluster,\nwhen it is not the local directory: url, revision and dir." }, "builderImages": { "patternProperties": { @@ -262,21 +262,6 @@ "type": "object", "description": "Function" }, - "Git": { - "properties": { - "url": { - "type": "string" - }, - "revision": { - "type": "string" - }, - "contextDir": { - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - }, "HealthEndpoints": { "properties": { "liveness": { @@ -571,6 +556,25 @@ "additionalProperties": false, "type": "object" }, + "Source": { + "properties": { + "url": { + "type": "string", + "description": "URL of the repository." + }, + "revision": { + "type": "string", + "description": "Revision to build: a branch, a tag or a commit, as git's fetch takes it.\nEmpty means the remote's default branch." + }, + "dir": { + "type": "string", + "description": "Dir is the directory within the repository holding the function.\nEmpty means the repository root." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Source is the repository a function is built from on the cluster: the values of --source, --revision and --source-dir." + }, "Volume": { "properties": { "secret": {