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 <stefan.prodan@gmail.com>
This commit is contained in:
parent
9c6dc330ae
commit
4ec51ca306
|
@ -45,6 +45,12 @@ const (
|
||||||
// AzureOCIProvider provides support for OCI authentication using a Azure Service Principal,
|
// AzureOCIProvider provides support for OCI authentication using a Azure Service Principal,
|
||||||
// Managed Identity or Shared Key.
|
// Managed Identity or Shared Key.
|
||||||
AzureOCIProvider string = "azure"
|
AzureOCIProvider string = "azure"
|
||||||
|
|
||||||
|
// OCILayerExtract defines the operation type for extracting the content from an OCI artifact layer.
|
||||||
|
OCILayerExtract = "extract"
|
||||||
|
|
||||||
|
// OCILayerCopy defines the operation type for copying the content from an OCI artifact layer.
|
||||||
|
OCILayerCopy = "copy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OCIRepositorySpec defines the desired state of OCIRepository
|
// OCIRepositorySpec defines the desired state of OCIRepository
|
||||||
|
@ -156,6 +162,14 @@ type OCILayerSelector struct {
|
||||||
// first layer matching this type is selected.
|
// first layer matching this type is selected.
|
||||||
// +optional
|
// +optional
|
||||||
MediaType string `json:"mediaType,omitempty"`
|
MediaType string `json:"mediaType,omitempty"`
|
||||||
|
|
||||||
|
// Operation specifies how the selected layer should be processed.
|
||||||
|
// By default, the layer compressed content is extracted to storage.
|
||||||
|
// When the operation is set to 'copy', the layer compressed content
|
||||||
|
// is persisted to storage as it is.
|
||||||
|
// +kubebuilder:validation:Enum=extract;copy
|
||||||
|
// +optional
|
||||||
|
Operation string `json:"operation,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OCIRepositoryVerification verifies the authenticity of an OCI Artifact
|
// OCIRepositoryVerification verifies the authenticity of an OCI Artifact
|
||||||
|
@ -231,6 +245,15 @@ func (in *OCIRepository) GetLayerMediaType() string {
|
||||||
return in.Spec.LayerSelector.MediaType
|
return in.Spec.LayerSelector.MediaType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLayerOperation returns the layer selector operation (defaults to extract).
|
||||||
|
func (in *OCIRepository) GetLayerOperation() string {
|
||||||
|
if in.Spec.LayerSelector == nil || in.Spec.LayerSelector.Operation == "" {
|
||||||
|
return OCILayerExtract
|
||||||
|
}
|
||||||
|
|
||||||
|
return in.Spec.LayerSelector.Operation
|
||||||
|
}
|
||||||
|
|
||||||
// +genclient
|
// +genclient
|
||||||
// +genclient:Namespaced
|
// +genclient:Namespaced
|
||||||
// +kubebuilder:storageversion
|
// +kubebuilder:storageversion
|
||||||
|
|
|
@ -90,6 +90,15 @@ spec:
|
||||||
which should be extracted from the OCI Artifact. The first layer
|
which should be extracted from the OCI Artifact. The first layer
|
||||||
matching this type is selected.
|
matching this type is selected.
|
||||||
type: string
|
type: string
|
||||||
|
operation:
|
||||||
|
description: Operation specifies how the selected layer should
|
||||||
|
be processed. By default, the layer compressed content is extracted
|
||||||
|
to storage. When the operation is set to 'copy', the layer compressed
|
||||||
|
content is persisted to storage as it is.
|
||||||
|
enum:
|
||||||
|
- extract
|
||||||
|
- copy
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
provider:
|
provider:
|
||||||
default: generic
|
default: generic
|
||||||
|
|
|
@ -22,8 +22,10 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -499,6 +501,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
|
||||||
layer = layers[0]
|
layer = layers[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the compressed content from the selected layer
|
||||||
blob, err := layer.Compressed()
|
blob, err := layer.Compressed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := serror.NewGeneric(
|
e := serror.NewGeneric(
|
||||||
|
@ -509,9 +512,42 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = untar.Untar(blob, dir); err != nil {
|
// Persist layer content to storage using the specified operation
|
||||||
|
switch obj.GetLayerOperation() {
|
||||||
|
case sourcev1.OCILayerExtract:
|
||||||
|
if _, err = untar.Untar(blob, dir); err != nil {
|
||||||
|
e := serror.NewGeneric(
|
||||||
|
fmt.Errorf("failed to extract layer contents from artifact: %w", err),
|
||||||
|
sourcev1.OCILayerOperationFailedReason,
|
||||||
|
)
|
||||||
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
case sourcev1.OCILayerCopy:
|
||||||
|
metadata.Path = fmt.Sprintf("%s.tgz", metadata.Revision)
|
||||||
|
file, err := os.Create(filepath.Join(dir, metadata.Path))
|
||||||
|
if err != nil {
|
||||||
|
e := serror.NewGeneric(
|
||||||
|
fmt.Errorf("failed to create file to copy layer to: %w", err),
|
||||||
|
sourcev1.OCILayerOperationFailedReason,
|
||||||
|
)
|
||||||
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, blob)
|
||||||
|
if err != nil {
|
||||||
|
e := serror.NewGeneric(
|
||||||
|
fmt.Errorf("failed to copy layer from artifact: %w", err),
|
||||||
|
sourcev1.OCILayerOperationFailedReason,
|
||||||
|
)
|
||||||
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
default:
|
||||||
e := serror.NewGeneric(
|
e := serror.NewGeneric(
|
||||||
fmt.Errorf("failed to untar the first layer from artifact: %w", err),
|
fmt.Errorf("unsupported layer operation: %s", obj.GetLayerOperation()),
|
||||||
sourcev1.OCILayerOperationFailedReason,
|
sourcev1.OCILayerOperationFailedReason,
|
||||||
)
|
)
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
@ -915,14 +951,25 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
// Archive directory to storage
|
switch obj.GetLayerOperation() {
|
||||||
if err := r.Storage.Archive(&artifact, dir, nil); err != nil {
|
case sourcev1.OCILayerCopy:
|
||||||
e := serror.NewGeneric(
|
if err = r.Storage.CopyFromPath(&artifact, filepath.Join(dir, metadata.Path)); err != nil {
|
||||||
fmt.Errorf("unable to archive artifact to storage: %s", err),
|
e := serror.NewGeneric(
|
||||||
sourcev1.ArchiveOperationFailedReason,
|
fmt.Errorf("unable to copy artifact to storage: %w", err),
|
||||||
)
|
sourcev1.ArchiveOperationFailedReason,
|
||||||
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
)
|
||||||
return sreconcile.ResultEmpty, e
|
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := r.Storage.Archive(&artifact, dir, nil); err != nil {
|
||||||
|
e := serror.NewGeneric(
|
||||||
|
fmt.Errorf("unable to archive artifact to storage: %s", err),
|
||||||
|
sourcev1.ArchiveOperationFailedReason,
|
||||||
|
)
|
||||||
|
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record it on the object
|
// Record it on the object
|
||||||
|
|
|
@ -85,6 +85,7 @@ func TestOCIRepository_Reconcile(t *testing.T) {
|
||||||
semver string
|
semver string
|
||||||
digest string
|
digest string
|
||||||
mediaType string
|
mediaType string
|
||||||
|
operation string
|
||||||
assertArtifact []artifactFixture
|
assertArtifact []artifactFixture
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -93,6 +94,7 @@ func TestOCIRepository_Reconcile(t *testing.T) {
|
||||||
tag: podinfoVersions["6.1.6"].tag,
|
tag: podinfoVersions["6.1.6"].tag,
|
||||||
digest: podinfoVersions["6.1.6"].digest.Hex,
|
digest: podinfoVersions["6.1.6"].digest.Hex,
|
||||||
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
|
operation: sourcev1.OCILayerCopy,
|
||||||
assertArtifact: []artifactFixture{
|
assertArtifact: []artifactFixture{
|
||||||
{
|
{
|
||||||
expectedPath: "kustomize/deployment.yaml",
|
expectedPath: "kustomize/deployment.yaml",
|
||||||
|
@ -150,7 +152,12 @@ func TestOCIRepository_Reconcile(t *testing.T) {
|
||||||
}
|
}
|
||||||
if tt.mediaType != "" {
|
if tt.mediaType != "" {
|
||||||
obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: tt.mediaType}
|
obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: tt.mediaType}
|
||||||
|
|
||||||
|
if tt.operation != "" {
|
||||||
|
obj.Spec.LayerSelector.Operation = tt.operation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
||||||
|
|
||||||
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
||||||
|
|
|
@ -2635,6 +2635,21 @@ which should be extracted from the OCI Artifact. The
|
||||||
first layer matching this type is selected.</p>
|
first layer matching this type is selected.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>operation</code><br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>Operation specifies how the selected layer should be processed.
|
||||||
|
By default, the layer compressed content is extracted to storage.
|
||||||
|
When the operation is set to ‘copy’, the layer compressed content
|
||||||
|
is persisted to storage as it is.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue