Skip to content

Commit 32e40e0

Browse files
committed
Sparse Checkout Directories in GitRepositories.
- Add `.spec.sparseCheckout` and `.status.observedSparseCheckout` fields to `GitRepository`. - Add controller support to send the sparse checkout directories to go-git via pkg methods. - Use `.status/observedSparseCheckout` to detect drift in configuration. - Trim leading "./" in directory paths. - Validate spec configuration by checking directories specified in spec exist in the cloned repository after successful checkout - Add tests for testing the observed sparse checkout behavior. - Add docs describing the new fields. Signed-off-by: Dipti Pai <[email protected]>
1 parent 849b4de commit 32e40e0

File tree

7 files changed

+193
-1
lines changed

7 files changed

+193
-1
lines changed

api/v1/gitrepository_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ type GitRepositorySpec struct {
148148
// should be included in the Artifact produced for this GitRepository.
149149
// +optional
150150
Include []GitRepositoryInclude `json:"include,omitempty"`
151+
152+
// SparseCheckout specifies a list of directories to checkout when cloning
153+
// the repository. If specified, only these directories are included in the
154+
// Artifact produced for this GitRepository.
155+
// +optional
156+
SparseCheckout []string `json:"sparseCheckout,omitempty"`
151157
}
152158

153159
// GitRepositoryInclude specifies a local reference to a GitRepository which
@@ -266,6 +272,11 @@ type GitRepositoryStatus struct {
266272
// +optional
267273
ObservedInclude []GitRepositoryInclude `json:"observedInclude,omitempty"`
268274

275+
// ObservedSparseCheckout is the observed list of directories used to
276+
// produce the current Artifact.
277+
// +optional
278+
ObservedSparseCheckout []string `json:"observedSparseCheckout,omitempty"`
279+
269280
// SourceVerificationMode is the last used verification mode indicating
270281
// which Git object(s) have been verified.
271282
// +optional

api/v1/zz_generated.deepcopy.go

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

config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ spec:
174174
required:
175175
- name
176176
type: object
177+
sparseCheckout:
178+
description: |-
179+
SparseCheckout specifies a list of directories to checkout when cloning
180+
the repository. If specified, only these directories are included in the
181+
Artifact produced for this GitRepository.
182+
items:
183+
type: string
184+
type: array
177185
suspend:
178186
description: |-
179187
Suspend tells the controller to suspend the reconciliation of this
@@ -443,6 +451,13 @@ spec:
443451
ObservedRecurseSubmodules is the observed resource submodules
444452
configuration used to produce the current Artifact.
445453
type: boolean
454+
observedSparseCheckout:
455+
description: |-
456+
ObservedSparseCheckout is the observed list of directories used to
457+
produce the current Artifact.
458+
items:
459+
type: string
460+
type: array
446461
sourceVerificationMode:
447462
description: |-
448463
SourceVerificationMode is the last used verification mode indicating

docs/api/v1/source.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,20 @@ the GitRepository as cloned from the URL, using their default settings.</p>
523523
should be included in the Artifact produced for this GitRepository.</p>
524524
</td>
525525
</tr>
526+
<tr>
527+
<td>
528+
<code>sparseCheckout</code><br>
529+
<em>
530+
[]string
531+
</em>
532+
</td>
533+
<td>
534+
<em>(Optional)</em>
535+
<p>SparseCheckout specifies a list of directories to checkout when cloning
536+
the repository. If specified, only these directories are included in the
537+
Artifact produced for this GitRepository.</p>
538+
</td>
539+
</tr>
526540
</table>
527541
</td>
528542
</tr>
@@ -1863,6 +1877,20 @@ the GitRepository as cloned from the URL, using their default settings.</p>
18631877
should be included in the Artifact produced for this GitRepository.</p>
18641878
</td>
18651879
</tr>
1880+
<tr>
1881+
<td>
1882+
<code>sparseCheckout</code><br>
1883+
<em>
1884+
[]string
1885+
</em>
1886+
</td>
1887+
<td>
1888+
<em>(Optional)</em>
1889+
<p>SparseCheckout specifies a list of directories to checkout when cloning
1890+
the repository. If specified, only these directories are included in the
1891+
Artifact produced for this GitRepository.</p>
1892+
</td>
1893+
</tr>
18661894
</tbody>
18671895
</table>
18681896
</div>
@@ -1983,6 +2011,19 @@ produce the current Artifact.</p>
19832011
</tr>
19842012
<tr>
19852013
<td>
2014+
<code>observedSparseCheckout</code><br>
2015+
<em>
2016+
[]string
2017+
</em>
2018+
</td>
2019+
<td>
2020+
<em>(Optional)</em>
2021+
<p>ObservedSparseCheckout is the observed list of directories used to
2022+
produce the current Artifact.</p>
2023+
</td>
2024+
</tr>
2025+
<tr>
2026+
<td>
19862027
<code>sourceVerificationMode</code><br>
19872028
<em>
19882029
<a href="#source.toolkit.fluxcd.io/v1.GitVerificationMode">

docs/spec/v1/gitrepositories.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,28 @@ list](#default-exclusions), and may overrule the [`.sourceignore` file
590590
exclusions](#sourceignore-file). See [excluding files](#excluding-files)
591591
for more information.
592592

593+
### Sparse checkout
594+
595+
`.spec.sparseCheckout` is an optional field to specify list of directories to
596+
checkout when cloning the repository. If specified, only the specified directory
597+
contents will be present in the artifact produced for this repository.
598+
599+
```yaml
600+
apiVersion: source.toolkit.fluxcd.io/v1
601+
kind: GitRepository
602+
metadata:
603+
name: podinfo
604+
namespace: default
605+
spec:
606+
interval: 5m
607+
url: https://github.com/stefanprodan/podinfo
608+
ref:
609+
branch: master
610+
sparseCheckout:
611+
- charts
612+
- kustomize
613+
```
614+
593615
### Suspend
594616

595617
`.spec.suspend` is an optional field to suspend the reconciliation of a
@@ -1132,6 +1154,27 @@ status:
11321154
...
11331155
```
11341156

1157+
### Observed Sparse Checkout
1158+
1159+
The source-controller reports observed sparse checkout in the GitRepository's
1160+
`.status.observedSparseCheckout`. The observed sparse checkout is the latest
1161+
`.spec.sparseCheckout` value which resulted in a [ready
1162+
state](#ready-gitrepository), or stalled due to error it can not recover from
1163+
without human intervention. The value is the same as the [sparseCheckout in
1164+
spec](#sparse-checkout). It indicates the sparse checkout configuration used in
1165+
building the current artifact in storage. It is also used by the controller to
1166+
determine if an artifact needs to be rebuilt.
1167+
1168+
Example:
1169+
```yaml
1170+
status:
1171+
...
1172+
observedSparseCheckout:
1173+
- charts
1174+
- kustomize
1175+
...
1176+
```
1177+
11351178
### Source Verification Mode
11361179

11371180
The source-controller reports the Git object(s) it verified in the Git

internal/controller/gitrepository_controller.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,16 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
590590
ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info("git repository checked out", "url", obj.Spec.URL, "revision", commitReference(obj, commit))
591591
conditions.Delete(obj, sourcev1.FetchFailedCondition)
592592

593+
// Validate sparse checkout paths after successful checkout.
594+
if err := r.validateSparseCheckoutPaths(ctx, obj, dir); err != nil {
595+
e := serror.NewStalling(
596+
fmt.Errorf("failed to sparse checkout directories : %w", err),
597+
sourcev1.GitOperationFailedReason,
598+
)
599+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
600+
return sreconcile.ResultEmpty, e
601+
}
602+
593603
// Verify commit signature
594604
if result, err := r.verifySignature(ctx, obj, *commit); err != nil || result == sreconcile.ResultEmpty {
595605
return result, err
@@ -812,6 +822,7 @@ func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, sp *pat
812822
obj.Status.ObservedIgnore = obj.Spec.Ignore
813823
obj.Status.ObservedRecurseSubmodules = obj.Spec.RecurseSubmodules
814824
obj.Status.ObservedInclude = obj.Spec.Include
825+
obj.Status.ObservedSparseCheckout = obj.Spec.SparseCheckout
815826

816827
// Remove the deprecated symlink.
817828
// TODO(hidde): remove 2 minor versions from introduction of v1.
@@ -884,6 +895,7 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, sp *patc
884895
// performs a git checkout.
885896
func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1.GitRepository,
886897
authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool) (*git.Commit, error) {
898+
887899
// Configure checkout strategy.
888900
cloneOpts := repository.CloneConfig{
889901
RecurseSubmodules: obj.Spec.RecurseSubmodules,
@@ -896,7 +908,14 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1
896908
cloneOpts.SemVer = ref.SemVer
897909
cloneOpts.RefName = ref.Name
898910
}
899-
911+
if obj.Spec.SparseCheckout != nil {
912+
// Trim any leading "./" in the directory paths since underlying go-git API does not honor them.
913+
sparseCheckoutDirs := make([]string, len(obj.Spec.SparseCheckout))
914+
for i, path := range obj.Spec.SparseCheckout {
915+
sparseCheckoutDirs[i] = strings.TrimPrefix(path, "./")
916+
}
917+
cloneOpts.SparseCheckoutDirectories = sparseCheckoutDirs
918+
}
900919
// Only if the object has an existing artifact in storage, attempt to
901920
// short-circuit clone operation. reconcileStorage has already verified
902921
// that the artifact exists.
@@ -1172,6 +1191,14 @@ func gitContentConfigChanged(obj *sourcev1.GitRepository, includes *artifactSet)
11721191
if requiresVerification(obj) {
11731192
return true
11741193
}
1194+
if len(obj.Spec.SparseCheckout) != len(obj.Status.ObservedSparseCheckout) {
1195+
return true
1196+
}
1197+
for index, dir := range obj.Spec.SparseCheckout {
1198+
if dir != obj.Status.ObservedSparseCheckout[index] {
1199+
return true
1200+
}
1201+
}
11751202

11761203
// Convert artifactSet to index addressable artifacts and ensure that it and
11771204
// the included artifacts include all the include from the spec.
@@ -1206,6 +1233,19 @@ func gitContentConfigChanged(obj *sourcev1.GitRepository, includes *artifactSet)
12061233
return false
12071234
}
12081235

1236+
// validateSparseCheckoutPaths checks if the sparse checkout paths exist in the cloned repository.
1237+
func (r *GitRepositoryReconciler) validateSparseCheckoutPaths(ctx context.Context, obj *sourcev1.GitRepository, dir string) error {
1238+
if obj.Spec.SparseCheckout != nil {
1239+
for _, path := range obj.Spec.SparseCheckout {
1240+
fullPath := filepath.Join(dir, path)
1241+
if _, err := os.Lstat(fullPath); err != nil {
1242+
return fmt.Errorf("sparse checkout dir '%s' does not exist in repository: %w", path, err)
1243+
}
1244+
}
1245+
}
1246+
return nil
1247+
}
1248+
12091249
// Returns true if both GitRepositoryIncludes are equal.
12101250
func gitRepositoryIncludeEqual(a, b sourcev1.GitRepositoryInclude) bool {
12111251
if a.GitRepositoryRef != b.GitRepositoryRef {

internal/controller/gitrepository_controller_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3073,6 +3073,38 @@ func TestGitContentConfigChanged(t *testing.T) {
30733073
},
30743074
want: false,
30753075
},
3076+
{
3077+
name: "unobserved sparse checkout",
3078+
obj: sourcev1.GitRepository{
3079+
Spec: sourcev1.GitRepositorySpec{SparseCheckout: []string{"a/b/c", "x/y/z"}},
3080+
Status: sourcev1.GitRepositoryStatus{ObservedSparseCheckout: []string{"a/b/c"}},
3081+
},
3082+
want: true,
3083+
},
3084+
{
3085+
name: "unobserved case sensitive sparse checkout",
3086+
obj: sourcev1.GitRepository{
3087+
Spec: sourcev1.GitRepositorySpec{SparseCheckout: []string{"a/b/c", "x/y/Z"}},
3088+
Status: sourcev1.GitRepositoryStatus{ObservedSparseCheckout: []string{"a/b/c", "x/y/z"}},
3089+
},
3090+
want: true,
3091+
},
3092+
{
3093+
name: "observed sparse checkout",
3094+
obj: sourcev1.GitRepository{
3095+
Spec: sourcev1.GitRepositorySpec{SparseCheckout: []string{"a/b/c", "x/y/z"}},
3096+
Status: sourcev1.GitRepositoryStatus{ObservedSparseCheckout: []string{"a/b/c", "x/y/z"}},
3097+
},
3098+
want: false,
3099+
},
3100+
{
3101+
name: "observed sparse checkout with leading slash",
3102+
obj: sourcev1.GitRepository{
3103+
Spec: sourcev1.GitRepositorySpec{SparseCheckout: []string{"./a/b/c", "./x/y/z"}},
3104+
Status: sourcev1.GitRepositoryStatus{ObservedSparseCheckout: []string{"./a/b/c", "./x/y/z"}},
3105+
},
3106+
want: false,
3107+
},
30763108
{
30773109
name: "unobserved include",
30783110
obj: sourcev1.GitRepository{

0 commit comments

Comments
 (0)