Skip to content

Commit 4ec51ca

Browse files
committed
Add option to copy the OCI layer to storage
Add on optional field to the `OCIRepository.spec.layerSelector` called `operation` that accepts one of the following values: `extract` or `copy`. When the operation is set to `copy`, instead of extracting the compressed layer, the controller copies the compressed blob as it is to storage, thus keeping the original content unaltered. Signed-off-by: Stefan Prodan <[email protected]>
1 parent 9c6dc33 commit 4ec51ca

File tree

5 files changed

+111
-10
lines changed

5 files changed

+111
-10
lines changed

api/v1beta2/ocirepository_types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ const (
4545
// AzureOCIProvider provides support for OCI authentication using a Azure Service Principal,
4646
// Managed Identity or Shared Key.
4747
AzureOCIProvider string = "azure"
48+
49+
// OCILayerExtract defines the operation type for extracting the content from an OCI artifact layer.
50+
OCILayerExtract = "extract"
51+
52+
// OCILayerCopy defines the operation type for copying the content from an OCI artifact layer.
53+
OCILayerCopy = "copy"
4854
)
4955

5056
// OCIRepositorySpec defines the desired state of OCIRepository
@@ -156,6 +162,14 @@ type OCILayerSelector struct {
156162
// first layer matching this type is selected.
157163
// +optional
158164
MediaType string `json:"mediaType,omitempty"`
165+
166+
// Operation specifies how the selected layer should be processed.
167+
// By default, the layer compressed content is extracted to storage.
168+
// When the operation is set to 'copy', the layer compressed content
169+
// is persisted to storage as it is.
170+
// +kubebuilder:validation:Enum=extract;copy
171+
// +optional
172+
Operation string `json:"operation,omitempty"`
159173
}
160174

161175
// OCIRepositoryVerification verifies the authenticity of an OCI Artifact
@@ -231,6 +245,15 @@ func (in *OCIRepository) GetLayerMediaType() string {
231245
return in.Spec.LayerSelector.MediaType
232246
}
233247

248+
// GetLayerOperation returns the layer selector operation (defaults to extract).
249+
func (in *OCIRepository) GetLayerOperation() string {
250+
if in.Spec.LayerSelector == nil || in.Spec.LayerSelector.Operation == "" {
251+
return OCILayerExtract
252+
}
253+
254+
return in.Spec.LayerSelector.Operation
255+
}
256+
234257
// +genclient
235258
// +genclient:Namespaced
236259
// +kubebuilder:storageversion

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ spec:
9090
which should be extracted from the OCI Artifact. The first layer
9191
matching this type is selected.
9292
type: string
93+
operation:
94+
description: Operation specifies how the selected layer should
95+
be processed. By default, the layer compressed content is extracted
96+
to storage. When the operation is set to 'copy', the layer compressed
97+
content is persisted to storage as it is.
98+
enum:
99+
- extract
100+
- copy
101+
type: string
93102
type: object
94103
provider:
95104
default: generic

controllers/ocirepository_controller.go

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import (
2222
"crypto/x509"
2323
"errors"
2424
"fmt"
25+
"io"
2526
"net/http"
2627
"os"
28+
"path/filepath"
2729
"sort"
2830
"strings"
2931
"time"
@@ -499,6 +501,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
499501
layer = layers[0]
500502
}
501503

504+
// Extract the compressed content from the selected layer
502505
blob, err := layer.Compressed()
503506
if err != nil {
504507
e := serror.NewGeneric(
@@ -509,9 +512,42 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
509512
return sreconcile.ResultEmpty, e
510513
}
511514

512-
if _, err = untar.Untar(blob, dir); err != nil {
515+
// Persist layer content to storage using the specified operation
516+
switch obj.GetLayerOperation() {
517+
case sourcev1.OCILayerExtract:
518+
if _, err = untar.Untar(blob, dir); err != nil {
519+
e := serror.NewGeneric(
520+
fmt.Errorf("failed to extract layer contents from artifact: %w", err),
521+
sourcev1.OCILayerOperationFailedReason,
522+
)
523+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
524+
return sreconcile.ResultEmpty, e
525+
}
526+
case sourcev1.OCILayerCopy:
527+
metadata.Path = fmt.Sprintf("%s.tgz", metadata.Revision)
528+
file, err := os.Create(filepath.Join(dir, metadata.Path))
529+
if err != nil {
530+
e := serror.NewGeneric(
531+
fmt.Errorf("failed to create file to copy layer to: %w", err),
532+
sourcev1.OCILayerOperationFailedReason,
533+
)
534+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
535+
return sreconcile.ResultEmpty, e
536+
}
537+
defer file.Close()
538+
539+
_, err = io.Copy(file, blob)
540+
if err != nil {
541+
e := serror.NewGeneric(
542+
fmt.Errorf("failed to copy layer from artifact: %w", err),
543+
sourcev1.OCILayerOperationFailedReason,
544+
)
545+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
546+
return sreconcile.ResultEmpty, e
547+
}
548+
default:
513549
e := serror.NewGeneric(
514-
fmt.Errorf("failed to untar the first layer from artifact: %w", err),
550+
fmt.Errorf("unsupported layer operation: %s", obj.GetLayerOperation()),
515551
sourcev1.OCILayerOperationFailedReason,
516552
)
517553
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
@@ -915,14 +951,25 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
915951
}
916952
defer unlock()
917953

918-
// Archive directory to storage
919-
if err := r.Storage.Archive(&artifact, dir, nil); err != nil {
920-
e := serror.NewGeneric(
921-
fmt.Errorf("unable to archive artifact to storage: %s", err),
922-
sourcev1.ArchiveOperationFailedReason,
923-
)
924-
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
925-
return sreconcile.ResultEmpty, e
954+
switch obj.GetLayerOperation() {
955+
case sourcev1.OCILayerCopy:
956+
if err = r.Storage.CopyFromPath(&artifact, filepath.Join(dir, metadata.Path)); err != nil {
957+
e := serror.NewGeneric(
958+
fmt.Errorf("unable to copy artifact to storage: %w", err),
959+
sourcev1.ArchiveOperationFailedReason,
960+
)
961+
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
962+
return sreconcile.ResultEmpty, e
963+
}
964+
default:
965+
if err := r.Storage.Archive(&artifact, dir, nil); err != nil {
966+
e := serror.NewGeneric(
967+
fmt.Errorf("unable to archive artifact to storage: %s", err),
968+
sourcev1.ArchiveOperationFailedReason,
969+
)
970+
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
971+
return sreconcile.ResultEmpty, e
972+
}
926973
}
927974

928975
// Record it on the object

controllers/ocirepository_controller_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func TestOCIRepository_Reconcile(t *testing.T) {
8585
semver string
8686
digest string
8787
mediaType string
88+
operation string
8889
assertArtifact []artifactFixture
8990
}{
9091
{
@@ -93,6 +94,7 @@ func TestOCIRepository_Reconcile(t *testing.T) {
9394
tag: podinfoVersions["6.1.6"].tag,
9495
digest: podinfoVersions["6.1.6"].digest.Hex,
9596
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
97+
operation: sourcev1.OCILayerCopy,
9698
assertArtifact: []artifactFixture{
9799
{
98100
expectedPath: "kustomize/deployment.yaml",
@@ -150,7 +152,12 @@ func TestOCIRepository_Reconcile(t *testing.T) {
150152
}
151153
if tt.mediaType != "" {
152154
obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: tt.mediaType}
155+
156+
if tt.operation != "" {
157+
obj.Spec.LayerSelector.Operation = tt.operation
158+
}
153159
}
160+
154161
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
155162

156163
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}

docs/api/source.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2635,6 +2635,21 @@ which should be extracted from the OCI Artifact. The
26352635
first layer matching this type is selected.</p>
26362636
</td>
26372637
</tr>
2638+
<tr>
2639+
<td>
2640+
<code>operation</code><br>
2641+
<em>
2642+
string
2643+
</em>
2644+
</td>
2645+
<td>
2646+
<em>(Optional)</em>
2647+
<p>Operation specifies how the selected layer should be processed.
2648+
By default, the layer compressed content is extracted to storage.
2649+
When the operation is set to &lsquo;copy&rsquo;, the layer compressed content
2650+
is persisted to storage as it is.</p>
2651+
</td>
2652+
</tr>
26382653
</tbody>
26392654
</table>
26402655
</div>

0 commit comments

Comments
 (0)