Skip to content

Commit e3f22ec

Browse files
authored
Add support for function monorepos (#37)
1 parent 6009564 commit e3f22ec

9 files changed

Lines changed: 165 additions & 26 deletions

File tree

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,31 @@ The status will include:
133133
- Middleware update conditions
134134
- Whether the function needs rebuilding due to outdated middleware
135135

136+
## Advanced Use Cases
137+
138+
### Functions in Monorepos
139+
140+
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:
141+
142+
```yaml
143+
apiVersion: functions.dev/v1alpha1
144+
kind: Function
145+
metadata:
146+
name: my-function
147+
namespace: default
148+
spec:
149+
repository:
150+
url: https://github.com/your-org/your-monorepo.git
151+
path: functions/my-function
152+
authSecretRef:
153+
name: git-credentials
154+
registry:
155+
authSecretRef:
156+
name: registry-credentials
157+
```
158+
159+
The operator will clone the repository and use the specified path as the function root directory.
160+
136161
## Development
137162

138163
### Local Development Cluster

internal/controller/function_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (r *FunctionReconciler) prepareSource(ctx context.Context, function *v1alph
145145
}
146146
}
147147

148-
repo, err := r.GitManager.CloneRepository(ctx, function.Spec.Repository.URL, branchReference, gitAuthSecret.Data)
148+
repo, err := r.GitManager.CloneRepository(ctx, function.Spec.Repository.URL, function.Spec.Repository.Path, branchReference, gitAuthSecret.Data)
149149
if err != nil {
150150
function.MarkSourceNotReady("GitCloneFailed", "Failed to clone repository: %s", err.Error())
151151
return nil, nil, fmt.Errorf("failed to setup git repository: %w", err)

internal/controller/function_controller_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ var _ = Describe("Function Controller", func() {
114114
funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil)
115115
funcMock.EXPECT().Deploy(mock.Anything, mock.Anything, resourceNamespace, funccli.DeployOptions{}).Return(nil)
116116

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

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

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

179179
return &git.Repository{
180180
CloneDir: tempDir,
181+
SubPath: ".",
181182
}
182183
}

internal/git/Manager_mock.go

Lines changed: 20 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/git/manager.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const (
2020
)
2121

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

2626
func NewManager() Manager {
@@ -29,7 +29,7 @@ func NewManager() Manager {
2929

3030
type managerImpl struct{}
3131

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

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

5858
return &Repository{
5959
CloneDir: targetDir,
60+
SubPath: subPath,
6061
}, nil
6162
}
6263

internal/git/repository.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ package git
22

33
import (
44
"os"
5+
"path"
56
)
67

78
type Repository struct {
89
CloneDir string
10+
SubPath string
911
}
1012

1113
func (r *Repository) Path() string {
12-
return r.CloneDir
14+
return path.Join(r.CloneDir, r.SubPath)
1315
}
1416

1517
func (r *Repository) Cleanup() error {

test/e2e/func_deploy_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"os"
2222
"os/exec"
23+
"path/filepath"
2324
"time"
2425

2526
functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1"
@@ -126,6 +127,101 @@ var _ = Describe("Operator", func() {
126127
Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed())
127128
})
128129
})
130+
Context("with a function in a subdirectory in a monorepo", func() {
131+
var repoURL string
132+
var repoDir string
133+
const subPath = "function-subdir"
134+
var functionName, functionNamespace string
135+
136+
BeforeEach(func() {
137+
// Create repository provider resources with automatic cleanup
138+
username, password, _, cleanup, err := repoProvider.CreateRandomUser()
139+
Expect(err).NotTo(HaveOccurred())
140+
DeferCleanup(cleanup)
141+
142+
_, repoURL, cleanup, err = repoProvider.CreateRandomRepo(username, false)
143+
Expect(err).NotTo(HaveOccurred())
144+
DeferCleanup(cleanup)
145+
146+
// Initialize repository with function code
147+
repoDir, err = utils.InitializeRepoWithFunctionInSubDir(repoURL, subPath, username, password, "go")
148+
Expect(err).NotTo(HaveOccurred())
149+
DeferCleanup(os.RemoveAll, repoDir)
150+
151+
functionNamespace, err = utils.GetTestNamespace()
152+
Expect(err).NotTo(HaveOccurred())
153+
DeferCleanup(cleanupNamespaces, functionNamespace)
154+
155+
functionDir := filepath.Join(repoDir, subPath)
156+
157+
// Deploy function using func CLI
158+
out, err := utils.RunFunc("deploy",
159+
"--namespace", functionNamespace,
160+
"--path", functionDir,
161+
"--registry", registry,
162+
fmt.Sprintf("--registry-insecure=%t", registryInsecure))
163+
Expect(err).NotTo(HaveOccurred())
164+
_, _ = fmt.Fprint(GinkgoWriter, out)
165+
166+
// Cleanup func deployment
167+
DeferCleanup(func() {
168+
_, _ = utils.RunFunc("delete", "--path", functionDir, "--namespace", functionNamespace)
169+
})
170+
171+
// Commit func.yaml changes
172+
err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", filepath.Join(subPath, "func.yaml"))
173+
Expect(err).NotTo(HaveOccurred())
174+
})
175+
176+
AfterEach(func() {
177+
logFailedTestDetails(functionName, functionNamespace)
178+
179+
// Cleanup function resource
180+
if functionName != "" {
181+
cmd := exec.Command("kubectl", "delete", "function", functionName, "-n", functionNamespace, "--ignore-not-found")
182+
_, err := utils.Run(cmd)
183+
Expect(err).NotTo(HaveOccurred())
184+
}
185+
})
186+
187+
It("should mark the function as ready", func() {
188+
// Create a Function resource
189+
function := &functionsdevv1alpha1.Function{
190+
ObjectMeta: metav1.ObjectMeta{
191+
GenerateName: "my-function-",
192+
Namespace: functionNamespace,
193+
},
194+
Spec: functionsdevv1alpha1.FunctionSpec{
195+
Repository: functionsdevv1alpha1.FunctionSpecRepository{
196+
URL: repoURL,
197+
Path: subPath,
198+
},
199+
},
200+
}
201+
202+
err := k8sClient.Create(ctx, function)
203+
Expect(err).NotTo(HaveOccurred())
204+
205+
functionName = function.Name
206+
207+
funcBecomeReady := func(g Gomega) {
208+
fn := &functionsdevv1alpha1.Function{}
209+
err := k8sClient.Get(ctx, types.NamespacedName{Name: function.Name, Namespace: function.Namespace}, fn)
210+
g.Expect(err).NotTo(HaveOccurred())
211+
212+
for _, cond := range fn.Status.Conditions {
213+
if cond.Type == functionsdevv1alpha1.TypeReady {
214+
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
215+
return
216+
}
217+
}
218+
g.Expect(false).To(BeTrue(), "Ready condition not found")
219+
}
220+
221+
// redeploy could take a bit longer therefore give a bit more time
222+
Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed())
223+
})
224+
})
129225
Context("with a not yet deployed function", func() {
130226
var repoURL string
131227
var repoDir string

test/e2e/func_middleware_update_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ var _ = Describe("Middleware Update", func() {
6767
// Initialize repository with function code using OLD func CLI version
6868
// v1.20.1 has no middleware-version label and uses instance-compatible templates
6969
oldFuncVersion := "v1.20.1"
70-
repoDir, err = utils.InitializeRepoWithFunctionVersion(repoURL, username, password, "go", oldFuncVersion)
70+
repoDir, err = utils.InitializeRepoWithFunctionVersion(repoURL, ".", username, password, "go", oldFuncVersion)
7171
Expect(err).NotTo(HaveOccurred())
7272
DeferCleanup(os.RemoveAll, repoDir)
7373

test/utils/git.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"os"
2222
"os/exec"
23+
"path/filepath"
2324
"strings"
2425

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

3435
// InitializeRepoWithFunction creates a function project and pushes it to the Gitea repo
3536
func InitializeRepoWithFunction(repoURL, username, password, language string) (string, error) {
36-
return InitializeRepoWithFunctionVersion(repoURL, username, password, language, "")
37+
return InitializeRepoWithFunctionVersion(repoURL, ".", username, password, language, "")
38+
}
39+
40+
// InitializeRepoWithFunctionInSubDir creates a function project in a repos subdirectory and pushes it to the Gitea repo
41+
func InitializeRepoWithFunctionInSubDir(repoURL, subDir, username, password, language string) (string, error) {
42+
return InitializeRepoWithFunctionVersion(repoURL, subDir, username, password, language, "")
3743
}
3844

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

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

53+
functionPath := filepath.Join(repoDir, subdir)
54+
4755
// Initialize function (func init creates the directory)
4856
if version == "" {
49-
if _, err := RunFunc("init", "-l", language, repoDir); err != nil {
57+
if _, err := RunFunc("init", "-l", language, functionPath); err != nil {
5058
return "", fmt.Errorf("failed to init function: %w", err)
5159
}
5260
} else {
53-
if _, err := RunFuncWithVersion(version, "init", "-l", language, repoDir); err != nil {
61+
if _, err := RunFuncWithVersion(version, "init", "-l", language, functionPath); err != nil {
5462
return "", fmt.Errorf("failed to init function: %w", err)
5563
}
5664
}

0 commit comments

Comments
 (0)