Skip to content

Commit 95c6a70

Browse files
committed
Add cachedir pooling and refactor git.go
1 parent 881d03e commit 95c6a70

17 files changed

Lines changed: 943 additions & 721 deletions

File tree

pkg/apiserver/webhooks.go

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func createValidatingWebhook(ctx context.Context, cfg *WebhookConfig, caCert []b
329329
Resources: []string{porchapi.PackageRevisionGVR.Resource},
330330
},
331331
}},
332-
AdmissionReviewVersions: []string{"v1", "v1beta1"},
332+
AdmissionReviewVersions: []string{"v1"},
333333
SideEffects: &none,
334334
FailurePolicy: &fail,
335335
TimeoutSeconds: &cfg.timeout,
@@ -357,7 +357,7 @@ func createValidatingWebhook(ctx context.Context, cfg *WebhookConfig, caCert []b
357357
Resources: []string{"repositories"},
358358
},
359359
}},
360-
AdmissionReviewVersions: []string{"v1", "v1beta1"},
360+
AdmissionReviewVersions: []string{"v1"},
361361
SideEffects: &none,
362362
FailurePolicy: &fail,
363363
TimeoutSeconds: &cfg.timeout,
@@ -661,12 +661,28 @@ func validateRepository(w http.ResponseWriter, r *http.Request, porchClient clie
661661

662662
var attempted configapi.Repository
663663
if err := json.Unmarshal(admissionReviewRequest.Request.Object.Raw, &attempted); err != nil {
664+
klog.Errorf("failed to unmarshal repository object: %v", err)
664665
writeErr(fmt.Sprintf("could not unmarshal repository: %v", err), &w)
665666
return
666667
}
667668

669+
// For UPDATE operations, check if URL or directory is being modified
670+
if admissionReviewRequest.Request.Operation == admissionv1.Update {
671+
var existing configapi.Repository
672+
if err := json.Unmarshal(admissionReviewRequest.Request.OldObject.Raw, &existing); err != nil {
673+
klog.Errorf("failed to unmarshal existing repository object: %v", err)
674+
writeErr(fmt.Sprintf("could not unmarshal existing repository: %v", err), &w)
675+
return
676+
}
677+
678+
if err := validateRepositoryModification(&existing, &attempted, admissionReviewRequest, &w); err != nil {
679+
return
680+
}
681+
}
682+
668683
var repoList configapi.RepositoryList
669684
if err := porchClient.List(context.Background(), &repoList); err != nil {
685+
klog.Errorf("failed to list repositories: %v", err)
670686
writeErr(fmt.Sprintf("could not list repositories: %v", err), &w)
671687
return
672688
}
@@ -676,22 +692,8 @@ func validateRepository(w http.ResponseWriter, r *http.Request, porchClient clie
676692
continue
677693
}
678694
if isConflict(&existing, &attempted) {
679-
resp := &admissionv1.AdmissionResponse{
680-
Allowed: false,
681-
Result: &metav1.Status{
682-
Status: "Failure",
683-
Message: fmt.Sprintf("Repository conflict with existing repository: %s/%s", existing.Namespace, existing.Name),
684-
Reason: "RepositoryConflict",
685-
},
686-
}
687-
responseBytes, _ := constructResponse(resp, admissionReviewRequest)
688-
w.Header().Set("Content-Type", "application/json")
689-
_, err = w.Write(responseBytes)
690-
if err != nil {
691-
errMsg := fmt.Sprintf("error writing response: %v", err)
692-
writeErr(errMsg, &w)
693-
return
694-
}
695+
klog.Errorf("repository validation failed: conflict detected between attempted %s/%s and existing %s/%s", attempted.Namespace, attempted.Name, existing.Namespace, existing.Name)
696+
writeModificationResponse(fmt.Sprintf("Repository conflict with existing repository: %s/%s", existing.Namespace, existing.Name), "RepositoryConflict", admissionReviewRequest, &w)
695697
return
696698
}
697699
}
@@ -766,18 +768,64 @@ func isConflict(existing, attempted *configapi.Repository) bool {
766768
}
767769

768770
func isNestedConflict(a, b string) bool {
769-
aParts := strings.Split(a, "/")
770-
bParts := strings.Split(b, "/")
771+
// Check if one path is nested within the other using filepath.Rel
772+
relAtoB, err1 := filepath.Rel(a, b)
773+
relBtoA, err2 := filepath.Rel(b, a)
771774

772-
// a is base of b
773-
if len(aParts) < len(bParts) && strings.Join(bParts[:len(aParts)], "/") == a {
774-
return true
775+
// If either relative path doesn't start with "../", it means one is nested in the other
776+
if err1 == nil && !strings.HasPrefix(relAtoB, "../") && relAtoB != "." {
777+
return true // b is nested within a
775778
}
776-
777-
// b is base of a
778-
if len(bParts) < len(aParts) && strings.Join(aParts[:len(bParts)], "/") == b {
779-
return true
779+
if err2 == nil && !strings.HasPrefix(relBtoA, "../") && relBtoA != "." {
780+
return true // a is nested within b
780781
}
781782

782783
return false
783784
}
785+
786+
func validateRepositoryModification(existing, attempted *configapi.Repository, admissionReviewRequest *admissionv1.AdmissionReview, w *http.ResponseWriter) error {
787+
if isURLModified(existing, attempted) {
788+
klog.Errorf("repository validation failed: URL modification not allowed for %s/%s - delete the existing repository and create it if you want to change the URL", attempted.Namespace, attempted.Name)
789+
writeModificationResponse("Repository URL cannot be modified after creation. Please delete the existing repository and create it if you want to change the URL", "URLModificationNotAllowed", admissionReviewRequest, w)
790+
return fmt.Errorf("URL modification not allowed")
791+
}
792+
793+
if isDirectoryModified(existing, attempted) {
794+
klog.Errorf("repository validation failed: directory modification not allowed for %s/%s - delete the existing repository and create it if you want to change the directory", attempted.Namespace, attempted.Name)
795+
writeModificationResponse("Repository directory cannot be modified after creation. Please delete the existing repository and create it if you want to change the directory", "DirectoryModificationNotAllowed", admissionReviewRequest, w)
796+
return fmt.Errorf("directory modification not allowed")
797+
}
798+
799+
return nil
800+
}
801+
802+
func isURLModified(existing, attempted *configapi.Repository) bool {
803+
return existing.Spec.Git.Repo != attempted.Spec.Git.Repo
804+
}
805+
806+
func isDirectoryModified(existing, attempted *configapi.Repository) bool {
807+
return existing.Spec.Git.Directory != attempted.Spec.Git.Directory
808+
}
809+
810+
func writeModificationResponse(message, reason string, admissionReviewRequest *admissionv1.AdmissionReview, w *http.ResponseWriter) {
811+
resp := &admissionv1.AdmissionResponse{
812+
Allowed: false,
813+
Result: &metav1.Status{
814+
Status: "Failure",
815+
Message: message,
816+
Reason: metav1.StatusReason(reason),
817+
},
818+
}
819+
responseBytes, err := constructResponse(resp, admissionReviewRequest)
820+
if err != nil {
821+
klog.Errorf("failed to construct modification response: %v", err)
822+
writeErr(fmt.Sprintf("error constructing response: %v", err), w)
823+
return
824+
}
825+
(*w).Header().Set("Content-Type", "application/json")
826+
_, err = (*w).Write(responseBytes)
827+
if err != nil {
828+
errMsg := fmt.Sprintf("error writing response: %v", err)
829+
writeErr(errMsg, w)
830+
}
831+
}

pkg/cache/crcache/cache.go

Lines changed: 6 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ var tracer = otel.Tracer("crcache")
3434
type Cache struct {
3535
repositories map[repository.RepositoryKey]*cachedRepository
3636
mainLock *sync.RWMutex
37-
locks map[repository.RepositoryKey]*sync.Mutex
38-
cacheLocks map[string]*sync.Mutex
3937
metadataStore meta.MetadataStore
4038
options cachetypes.CacheOptions
4139
}
@@ -53,18 +51,10 @@ func (c *Cache) OpenRepository(ctx context.Context, repositorySpec *configapi.Re
5351
return nil, err
5452
}
5553

56-
cacheKey := c.getCacheKey(repositorySpec)
57-
cacheLock := c.getOrCreateCacheLock(cacheKey)
58-
cacheLock.Lock()
59-
defer cacheLock.Unlock()
60-
61-
lock := c.getOrInsertLock(key)
62-
lock.Lock()
63-
defer lock.Unlock()
54+
c.mainLock.Lock()
55+
defer c.mainLock.Unlock()
6456

65-
c.mainLock.RLock()
6657
if repo, ok := c.repositories[key]; ok && repo != nil {
67-
c.mainLock.RUnlock()
6858
// Keep the spec updated in the cache.
6959
repo.repoSpec = repositorySpec
7060
// Check external repo connectivity
@@ -79,18 +69,14 @@ func (c *Cache) OpenRepository(ctx context.Context, repositorySpec *configapi.Re
7969
}
8070
return repo, nil
8171
}
82-
c.mainLock.RUnlock()
8372

8473
externalRepo, err := externalrepo.CreateRepositoryImpl(ctx, repositorySpec, c.options.ExternalRepoOptions)
8574
if err != nil {
8675
return nil, err
8776
}
8877

8978
cachedRepo := newRepository(key, repositorySpec, externalRepo, c.metadataStore, c.options)
90-
91-
c.mainLock.Lock()
9279
c.repositories[key] = cachedRepo
93-
c.mainLock.Unlock()
9480

9581
return cachedRepo, nil
9682
}
@@ -115,45 +101,14 @@ func (c *Cache) CloseRepository(ctx context.Context, repositorySpec *configapi.R
115101
return nil
116102
}
117103

118-
cacheKey := c.getCacheKey(repositorySpec)
119-
cacheLock := c.getOrCreateCacheLock(cacheKey)
120-
cacheLock.Lock()
121-
defer cacheLock.Unlock()
122-
123-
// check if repositorySpec shares the underlying cached repo with another repository
124-
for _, r := range allRepos {
125-
if r.Name == repositorySpec.Name && r.Namespace == repositorySpec.Namespace {
126-
continue
127-
}
128-
// For Git repositories, check sharing based on URL only
129-
if repositorySpec.Spec.Type == configapi.RepositoryTypeGit &&
130-
r.Spec.Type == configapi.RepositoryTypeGit &&
131-
r.Spec.Git.Repo == repositorySpec.Spec.Git.Repo {
132-
// do not close cached repo if it is shared, but cancel the polling goroutine
133-
klog.Infof("Not closing cached repository %q because it is shared", key)
134-
return nil
135-
}
136-
}
137-
138-
lock := c.getOrInsertLock(key)
139-
lock.Lock()
140-
defer lock.Unlock()
141-
142-
if ok {
104+
defer func() {
143105
c.mainLock.Lock()
144-
delete(c.locks, key)
145106
delete(c.repositories, key)
146107
c.mainLock.Unlock()
108+
}()
147109

148-
if repo != nil {
149-
return repo.Close(ctx)
150-
} else {
151-
klog.Warningf("cached repository with key %q had stored value nil", key)
152-
}
153-
} else {
154-
c.mainLock.Lock()
155-
delete(c.locks, key)
156-
c.mainLock.Unlock()
110+
if repo != nil {
111+
return repo.Close(ctx)
157112
}
158113

159114
return nil
@@ -176,43 +131,3 @@ func (c *Cache) GetRepository(repoKey repository.RepositoryKey) repository.Repos
176131
defer c.mainLock.RUnlock()
177132
return c.repositories[repoKey]
178133
}
179-
180-
func (c *Cache) getOrInsertLock(key repository.RepositoryKey) *sync.Mutex {
181-
c.mainLock.RLock()
182-
if lock, exists := c.locks[key]; exists {
183-
c.mainLock.RUnlock()
184-
return lock
185-
}
186-
c.mainLock.RUnlock()
187-
188-
c.mainLock.Lock()
189-
lock := &sync.Mutex{}
190-
c.locks[key] = lock
191-
c.mainLock.Unlock()
192-
193-
return lock
194-
}
195-
196-
func (c *Cache) getCacheKey(repositorySpec *configapi.Repository) string {
197-
if repositorySpec.Spec.Type == configapi.RepositoryTypeGit {
198-
return repositorySpec.Spec.Git.Repo
199-
}
200-
return repositorySpec.Name + "---" + repositorySpec.Namespace
201-
}
202-
203-
func (c *Cache) getOrCreateCacheLock(cacheKey string) *sync.Mutex {
204-
c.mainLock.Lock()
205-
defer c.mainLock.Unlock()
206-
207-
if c.cacheLocks == nil {
208-
c.cacheLocks = make(map[string]*sync.Mutex)
209-
}
210-
211-
if lock, exists := c.cacheLocks[cacheKey]; exists {
212-
return lock
213-
}
214-
215-
lock := &sync.Mutex{}
216-
c.cacheLocks[cacheKey] = lock
217-
return lock
218-
}

pkg/cache/crcache/cache_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,6 @@ func openRepositoryFromArchive(t *testing.T, ctx context.Context, testPath, name
271271
fakeClient := k8sfake.NewClientBuilder().WithScheme(scheme).WithObjects(apiRepo).Build()
272272
cache := &Cache{
273273
repositories: map[repository.RepositoryKey]*cachedRepository{},
274-
locks: map[repository.RepositoryKey]*sync.Mutex{},
275274
mainLock: &sync.RWMutex{},
276275
metadataStore: metadataStore,
277276
options: cachetypes.CacheOptions{

pkg/cache/crcache/crcachefactory.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ type CrCacheFactory struct {
3131
func (f *CrCacheFactory) NewCache(_ context.Context, options cachetypes.CacheOptions) (cachetypes.Cache, error) {
3232
return &Cache{
3333
repositories: map[repository.RepositoryKey]*cachedRepository{},
34-
locks: map[repository.RepositoryKey]*sync.Mutex{},
3534
mainLock: &sync.RWMutex{},
3635
metadataStore: meta.NewCrdMetadataStore(options.CoreClient),
3736
options: options,

pkg/cache/crcache/repository.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ func (r *cachedRepository) getRefreshError() error {
148148
}
149149

150150
func (r *cachedRepository) getPackageRevisions(ctx context.Context, filter repository.ListPackageRevisionFilter, forceRefresh bool) ([]repository.PackageRevision, error) {
151-
klog.Infof("Cache::OpenRepository(%s) fetching packages", r.Key())
151+
if forceRefresh {
152+
klog.Infof("Cache::OpenRepository(%s) fetching packages from external repository", r.Key())
153+
} else {
154+
klog.V(2).Infof("Cache::OpenRepository(%s) using cached packages", r.Key())
155+
}
152156
_, packageRevisions, err := r.getCachedPackages(ctx, forceRefresh)
153157
if err != nil {
154158
return nil, err

pkg/cache/crcache/repository_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,8 @@ import (
3434
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3535
"k8s.io/apimachinery/pkg/runtime"
3636
"k8s.io/apimachinery/pkg/types"
37-
"sigs.k8s.io/controller-runtime/pkg/client"
3837
)
3938

40-
type PorchClient interface {
41-
List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
42-
}
43-
4439
func TestCachedRepoRefresh(t *testing.T) {
4540
mockRepo := mockrepo.NewMockRepository(t)
4641
mockMeta := mockmeta.NewMockMetadataStore(t)

0 commit comments

Comments
 (0)