Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,31 @@ The status will include:
- Middleware update conditions
- Whether the function needs rebuilding due to outdated middleware

## Advanced Use Cases

### Functions in Monorepos

For functions located in a subdirectory of a repository (e.g., in a monorepo), use the `repository.path` field to specify the path to your function:

```yaml
apiVersion: functions.dev/v1alpha1
kind: Function
metadata:
name: my-function
namespace: default
spec:
repository:
url: https://github.com/your-org/your-monorepo.git
path: functions/my-function
authSecretRef:
name: git-credentials
registry:
authSecretRef:
name: registry-credentials
```

The operator will clone the repository and use the specified path as the function root directory.

## Development

### Local Development Cluster
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/function_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (r *FunctionReconciler) prepareSource(ctx context.Context, function *v1alph
}
}

repo, err := r.GitManager.CloneRepository(ctx, function.Spec.Repository.URL, branchReference, gitAuthSecret.Data)
repo, err := r.GitManager.CloneRepository(ctx, function.Spec.Repository.URL, function.Spec.Repository.Path, branchReference, gitAuthSecret.Data)
if err != nil {
function.MarkSourceNotReady("GitCloneFailed", "Failed to clone repository: %s", err.Error())
return nil, nil, fmt.Errorf("failed to setup git repository: %w", err)
Expand Down
7 changes: 4 additions & 3 deletions internal/controller/function_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ var _ = Describe("Function Controller", func() {
funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil)
funcMock.EXPECT().Deploy(mock.Anything, mock.Anything, resourceNamespace, funccli.DeployOptions{}).Return(nil)

gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "my-branch", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil)
gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "my-branch", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil)
},
}),
Entry("should skip deploy when middleware already up to date", reconcileTestCase{
Expand All @@ -128,7 +128,7 @@ var _ = Describe("Function Controller", func() {
funcMock.EXPECT().GetLatestMiddlewareVersion(mock.Anything, mock.Anything, mock.Anything).Return("v1.0.0", nil)
funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil)

gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "my-branch", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil)
gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "my-branch", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil)
},
}),
Entry("should use main as default branch", reconcileTestCase{
Expand All @@ -146,7 +146,7 @@ var _ = Describe("Function Controller", func() {
funcMock.EXPECT().GetLatestMiddlewareVersion(mock.Anything, mock.Anything, mock.Anything).Return("v1.0.0", nil)
funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil)

gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "main", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil)
gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "main", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil)
},
}),
)
Expand Down Expand Up @@ -178,5 +178,6 @@ func createTmpGitRepo(function functions.Function) *git.Repository {

return &git.Repository{
CloneDir: tempDir,
SubPath: ".",
}
}
34 changes: 20 additions & 14 deletions internal/git/Manager_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions internal/git/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const (
)

type Manager interface {
CloneRepository(ctx context.Context, url, reference string, auth map[string][]byte) (*Repository, error)
CloneRepository(ctx context.Context, url, subPath, reference string, auth map[string][]byte) (*Repository, error)
}

func NewManager() Manager {
Expand All @@ -29,7 +29,7 @@ func NewManager() Manager {

type managerImpl struct{}

func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, reference string, auth map[string][]byte) (*Repository, error) {
func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, reference string, auth map[string][]byte) (*Repository, error) {
timer := prometheus.NewTimer(monitoring.GitCloneDuration)
defer timer.ObserveDuration()

Expand Down Expand Up @@ -57,6 +57,7 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, reference st

return &Repository{
CloneDir: targetDir,
SubPath: subPath,
}, nil
}

Expand Down
4 changes: 3 additions & 1 deletion internal/git/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package git

import (
"os"
"path"
)

type Repository struct {
CloneDir string
SubPath string
}

func (r *Repository) Path() string {
return r.CloneDir
return path.Join(r.CloneDir, r.SubPath)
}

func (r *Repository) Cleanup() error {
Expand Down
96 changes: 96 additions & 0 deletions test/e2e/func_deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"

functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1"
Expand Down Expand Up @@ -126,6 +127,101 @@ var _ = Describe("Operator", func() {
Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed())
})
})
Context("with a function in a subdirectory in a monorepo", func() {
var repoURL string
var repoDir string
const subPath = "function-subdir"
var functionName, functionNamespace string

BeforeEach(func() {
// Create repository provider resources with automatic cleanup
username, password, _, cleanup, err := repoProvider.CreateRandomUser()
Expect(err).NotTo(HaveOccurred())
DeferCleanup(cleanup)

_, repoURL, cleanup, err = repoProvider.CreateRandomRepo(username, false)
Expect(err).NotTo(HaveOccurred())
DeferCleanup(cleanup)

// Initialize repository with function code
repoDir, err = utils.InitializeRepoWithFunctionInSubDir(repoURL, subPath, username, password, "go")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(os.RemoveAll, repoDir)

functionNamespace, err = utils.GetTestNamespace()
Expect(err).NotTo(HaveOccurred())
DeferCleanup(cleanupNamespaces, functionNamespace)

functionDir := filepath.Join(repoDir, subPath)

// Deploy function using func CLI
out, err := utils.RunFunc("deploy",
"--namespace", functionNamespace,
"--path", functionDir,
"--registry", registry,
fmt.Sprintf("--registry-insecure=%t", registryInsecure))
Expect(err).NotTo(HaveOccurred())
_, _ = fmt.Fprint(GinkgoWriter, out)

// Cleanup func deployment
DeferCleanup(func() {
_, _ = utils.RunFunc("delete", "--path", functionDir, "--namespace", functionNamespace)
})

// Commit func.yaml changes
err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", filepath.Join(subPath, "func.yaml"))
Expect(err).NotTo(HaveOccurred())
})

AfterEach(func() {
logFailedTestDetails(functionName, functionNamespace)

// Cleanup function resource
if functionName != "" {
cmd := exec.Command("kubectl", "delete", "function", functionName, "-n", functionNamespace, "--ignore-not-found")
_, err := utils.Run(cmd)
Expect(err).NotTo(HaveOccurred())
}
})

It("should mark the function as ready", func() {
// Create a Function resource
function := &functionsdevv1alpha1.Function{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "my-function-",
Namespace: functionNamespace,
},
Spec: functionsdevv1alpha1.FunctionSpec{
Repository: functionsdevv1alpha1.FunctionSpecRepository{
URL: repoURL,
Path: subPath,
},
},
}

err := k8sClient.Create(ctx, function)
Expect(err).NotTo(HaveOccurred())

functionName = function.Name

funcBecomeReady := func(g Gomega) {
fn := &functionsdevv1alpha1.Function{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: function.Name, Namespace: function.Namespace}, fn)
g.Expect(err).NotTo(HaveOccurred())

for _, cond := range fn.Status.Conditions {
if cond.Type == functionsdevv1alpha1.TypeReady {
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
return
}
}
g.Expect(false).To(BeTrue(), "Ready condition not found")
}

// redeploy could take a bit longer therefore give a bit more time
Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed())
})
})
Context("with a not yet deployed function", func() {
var repoURL string
var repoDir string
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/func_middleware_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ var _ = Describe("Middleware Update", func() {
// Initialize repository with function code using OLD func CLI version
// v1.20.1 has no middleware-version label and uses instance-compatible templates
oldFuncVersion := "v1.20.1"
repoDir, err = utils.InitializeRepoWithFunctionVersion(repoURL, username, password, "go", oldFuncVersion)
repoDir, err = utils.InitializeRepoWithFunctionVersion(repoURL, ".", username, password, "go", oldFuncVersion)
Expect(err).NotTo(HaveOccurred())
DeferCleanup(os.RemoveAll, repoDir)

Expand Down
16 changes: 12 additions & 4 deletions test/utils/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"k8s.io/apimachinery/pkg/util/rand"
Expand All @@ -33,24 +34,31 @@ func buildAuthURL(repoURL, username, password string) string {

// InitializeRepoWithFunction creates a function project and pushes it to the Gitea repo
func InitializeRepoWithFunction(repoURL, username, password, language string) (string, error) {
return InitializeRepoWithFunctionVersion(repoURL, username, password, language, "")
return InitializeRepoWithFunctionVersion(repoURL, ".", username, password, language, "")
}

// InitializeRepoWithFunctionInSubDir creates a function project in a repos subdirectory and pushes it to the Gitea repo
func InitializeRepoWithFunctionInSubDir(repoURL, subDir, username, password, language string) (string, error) {
return InitializeRepoWithFunctionVersion(repoURL, subDir, username, password, language, "")
}

// InitializeRepoWithFunctionVersion creates a function project with a specific func CLI version
// If version is empty, uses the current func CLI
func InitializeRepoWithFunctionVersion(repoURL, username, password, language, version string) (string, error) {
func InitializeRepoWithFunctionVersion(repoURL, subdir, username, password, language, version string) (string, error) {
repoDir := fmt.Sprintf("%s/func-test-%s", os.TempDir(), rand.String(10))

// Build authenticated URL
authURL := buildAuthURL(repoURL, username, password)

functionPath := filepath.Join(repoDir, subdir)

// Initialize function (func init creates the directory)
if version == "" {
if _, err := RunFunc("init", "-l", language, repoDir); err != nil {
if _, err := RunFunc("init", "-l", language, functionPath); err != nil {
return "", fmt.Errorf("failed to init function: %w", err)
}
} else {
if _, err := RunFuncWithVersion(version, "init", "-l", language, repoDir); err != nil {
if _, err := RunFuncWithVersion(version, "init", "-l", language, functionPath); err != nil {
return "", fmt.Errorf("failed to init function: %w", err)
}
}
Expand Down
Loading