Add the OCI metadata to the internal artifact

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
Stefan Prodan 2022-07-12 18:21:08 +03:00
parent 5072091eb5
commit 05f9c0ee2b
No known key found for this signature in database
GPG Key ID: 3299AEB0E4085BAF
12 changed files with 112 additions and 28 deletions

View File

@ -54,6 +54,10 @@ type Artifact struct {
// Size is the number of bytes in the file. // Size is the number of bytes in the file.
// +optional // +optional
Size *int64 `json:"size,omitempty"` Size *int64 `json:"size,omitempty"`
// Metadata holds upstream information such as OCI annotations.
// +optional
Metadata map[string]string `json:"metadata,omitempty"`
} }
// HasRevision returns if the given revision matches the current Revision of // HasRevision returns if the given revision matches the current Revision of

View File

@ -17,9 +17,11 @@ limitations under the License.
package v1beta2 package v1beta2
import ( import (
"github.com/fluxcd/pkg/apis/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta"
) )
const ( const (

View File

@ -37,6 +37,13 @@ func (in *Artifact) DeepCopyInto(out *Artifact) {
*out = new(int64) *out = new(int64)
**out = **in **out = **in
} }
if in.Metadata != nil {
in, out := &in.Metadata, &out.Metadata
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Artifact. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Artifact.

View File

@ -384,6 +384,11 @@ spec:
the last update of the Artifact. the last update of the Artifact.
format: date-time format: date-time
type: string type: string
metadata:
additionalProperties:
type: string
description: Metadata holds upstream information such as OCI annotations.
type: object
path: path:
description: Path is the relative file path of the Artifact. It description: Path is the relative file path of the Artifact. It
can be used to locate the file in the root of the Artifact storage can be used to locate the file in the root of the Artifact storage

View File

@ -559,6 +559,11 @@ spec:
the last update of the Artifact. the last update of the Artifact.
format: date-time format: date-time
type: string type: string
metadata:
additionalProperties:
type: string
description: Metadata holds upstream information such as OCI annotations.
type: object
path: path:
description: Path is the relative file path of the Artifact. It description: Path is the relative file path of the Artifact. It
can be used to locate the file in the root of the Artifact storage can be used to locate the file in the root of the Artifact storage
@ -677,6 +682,12 @@ spec:
the last update of the Artifact. the last update of the Artifact.
format: date-time format: date-time
type: string type: string
metadata:
additionalProperties:
type: string
description: Metadata holds upstream information such as OCI
annotations.
type: object
path: path:
description: Path is the relative file path of the Artifact. description: Path is the relative file path of the Artifact.
It can be used to locate the file in the root of the Artifact It can be used to locate the file in the root of the Artifact

View File

@ -432,6 +432,11 @@ spec:
the last update of the Artifact. the last update of the Artifact.
format: date-time format: date-time
type: string type: string
metadata:
additionalProperties:
type: string
description: Metadata holds upstream information such as OCI annotations.
type: object
path: path:
description: Path is the relative file path of the Artifact. It description: Path is the relative file path of the Artifact. It
can be used to locate the file in the root of the Artifact storage can be used to locate the file in the root of the Artifact storage

View File

@ -362,6 +362,11 @@ spec:
the last update of the Artifact. the last update of the Artifact.
format: date-time format: date-time
type: string type: string
metadata:
additionalProperties:
type: string
description: Metadata holds upstream information such as OCI annotations.
type: object
path: path:
description: Path is the relative file path of the Artifact. It description: Path is the relative file path of the Artifact. It
can be used to locate the file in the root of the Artifact storage can be used to locate the file in the root of the Artifact storage

View File

@ -142,6 +142,11 @@ spec:
the last update of the Artifact. the last update of the Artifact.
format: date-time format: date-time
type: string type: string
metadata:
additionalProperties:
type: string
description: Metadata holds upstream information such as OCI annotations.
type: object
path: path:
description: Path is the relative file path of the Artifact. It description: Path is the relative file path of the Artifact. It
can be used to locate the file in the root of the Artifact storage can be used to locate the file in the root of the Artifact storage

View File

@ -33,7 +33,6 @@ import (
"github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -110,7 +109,7 @@ var ociRepositoryFailConditions = []string{
// ociRepositoryReconcileFunc is the function type for all the v1beta2.OCIRepository // ociRepositoryReconcileFunc is the function type for all the v1beta2.OCIRepository
// (sub)reconcile functions. The type implementations are grouped and // (sub)reconcile functions. The type implementations are grouped and
// executed serially to perform the complete reconcile of the object. // executed serially to perform the complete reconcile of the object.
type ociRepositoryReconcileFunc func(ctx context.Context, obj *sourcev1.OCIRepository, digest *gcrv1.Hash, dir string) (sreconcile.Result, error) type ociRepositoryReconcileFunc func(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error)
// OCIRepositoryReconciler reconciles a v1beta2.OCIRepository object // OCIRepositoryReconciler reconciles a v1beta2.OCIRepository object
type OCIRepositoryReconciler struct { type OCIRepositoryReconciler struct {
@ -261,16 +260,15 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
}() }()
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition) conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
hs := gcrv1.Hash{}
var ( var (
res sreconcile.Result res sreconcile.Result
resErr error resErr error
digest = hs.DeepCopy() metadata = sourcev1.Artifact{}
) )
// Run the sub-reconcilers and build the result of reconciliation. // Run the sub-reconcilers and build the result of reconciliation.
for _, rec := range reconcilers { for _, rec := range reconcilers {
recResult, err := rec(ctx, obj, digest, tmpDir) recResult, err := rec(ctx, obj, &metadata, tmpDir)
// Exit immediately on ResultRequeue. // Exit immediately on ResultRequeue.
if recResult == sreconcile.ResultRequeue { if recResult == sreconcile.ResultRequeue {
return sreconcile.ResultRequeue, nil return sreconcile.ResultRequeue, nil
@ -286,14 +284,14 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
res = sreconcile.LowestRequeuingResult(res, recResult) res = sreconcile.LowestRequeuingResult(res, recResult)
} }
r.notify(ctx, oldObj, obj, digest, res, resErr) r.notify(ctx, oldObj, obj, res, resErr)
return res, resErr return res, resErr
} }
// reconcileSource fetches the upstream OCI artifact metadata and content. // reconcileSource fetches the upstream OCI artifact metadata and content.
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early. // If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, digest *gcrv1.Hash, dir string) (sreconcile.Result, error) { func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel() defer cancel()
@ -352,9 +350,25 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
} }
// Set the internal revision to the remote digest hex // Set the internal revision to the remote digest hex
imgDigest.DeepCopyInto(digest)
revision := imgDigest.Hex revision := imgDigest.Hex
// Copy the OCI annotations to the internal artifact metadata
manifest, err := img.Manifest()
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to parse artifact manifest: %w", err),
sourcev1.OCIOperationFailedReason,
)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
m := &sourcev1.Artifact{
Revision: revision,
Metadata: manifest.Annotations,
}
m.DeepCopyInto(metadata)
// Mark observations about the revision on the object // Mark observations about the revision on the object
defer func() { defer func() {
if !obj.GetArtifact().HasRevision(revision) { if !obj.GetArtifact().HasRevision(revision) {
@ -606,7 +620,7 @@ func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context,
// The hostname of any URL in the Status of the object are updated, to ensure // The hostname of any URL in the Status of the object are updated, to ensure
// they match the Storage server hostname of current runtime. // they match the Storage server hostname of current runtime.
func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context, func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
obj *sourcev1.OCIRepository, _ *gcrv1.Hash, _ string) (sreconcile.Result, error) { obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) {
// Garbage collect previous advertised artifact(s) from storage // Garbage collect previous advertised artifact(s) from storage
_ = r.garbageCollect(ctx, obj) _ = r.garbageCollect(ctx, obj)
@ -642,9 +656,9 @@ func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
// On a successful archive, the Artifact in the Status of the object is set, // On a successful archive, the Artifact in the Status of the object is set,
// and the symlink in the Storage is updated to its path. // and the symlink in the Storage is updated to its path.
func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
obj *sourcev1.OCIRepository, digest *gcrv1.Hash, dir string) (sreconcile.Result, error) { obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
// Calculate revision // Calculate revision
revision := digest.Hex revision := metadata.Revision
// Create artifact // Create artifact
artifact := r.Storage.NewArtifactFor(obj.Kind, obj, revision, fmt.Sprintf("%s.tar.gz", revision)) artifact := r.Storage.NewArtifactFor(obj.Kind, obj, revision, fmt.Sprintf("%s.tar.gz", revision))
@ -712,6 +726,7 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
// Record it on the object // Record it on the object
obj.Status.Artifact = artifact.DeepCopy() obj.Status.Artifact = artifact.DeepCopy()
obj.Status.Artifact.Metadata = metadata.Metadata
// Update symlink on a "best effort" basis // Update symlink on a "best effort" basis
url, err := r.Storage.Symlink(artifact, "latest.tar.gz") url, err := r.Storage.Symlink(artifact, "latest.tar.gz")
@ -798,7 +813,7 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
// notify emits notification related to the reconciliation. // notify emits notification related to the reconciliation.
func (r *OCIRepositoryReconciler) notify(ctx context.Context, func (r *OCIRepositoryReconciler) notify(ctx context.Context,
oldObj, newObj *sourcev1.OCIRepository, digest *gcrv1.Hash, res sreconcile.Result, resErr error) { oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) {
// Notify successful reconciliation for new artifact and recovery from any // Notify successful reconciliation for new artifact and recovery from any
// failure. // failure.
if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil { if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil {
@ -812,7 +827,7 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
oldChecksum = oldObj.GetArtifact().Checksum oldChecksum = oldObj.GetArtifact().Checksum
} }
message := fmt.Sprintf("stored artifact with digest '%s' from '%s'", digest.String(), newObj.Spec.URL) message := fmt.Sprintf("stored artifact with digest '%s' from '%s'", newObj.Status.Artifact.Revision, newObj.Spec.URL)
// Notify on new artifact and failure recovery. // Notify on new artifact and failure recovery.
if oldChecksum != newObj.GetArtifact().Checksum { if oldChecksum != newObj.GetArtifact().Checksum {

View File

@ -44,7 +44,8 @@ import (
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1" gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -163,11 +164,13 @@ func TestOCIRepository_Reconcile(t *testing.T) {
obj.Generation == obj.Status.ObservedGeneration obj.Generation == obj.Status.ObservedGeneration
}, timeout).Should(BeTrue()) }, timeout).Should(BeTrue())
t.Log(obj.Spec.Reference)
// Check if the revision matches the expected digest // Check if the revision matches the expected digest
g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest)) g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest))
// Check if the metadata matches the expected annotations
g.Expect(obj.Status.Artifact.Metadata["org.opencontainers.image.source"]).To(ContainSubstring("podinfo"))
g.Expect(obj.Status.Artifact.Metadata["org.opencontainers.image.revision"]).To(ContainSubstring(tt.tag))
// Check if the artifact storage path matches the expected file path // Check if the artifact storage path matches the expected file path
localPath := testStorage.LocalPath(*obj.Status.Artifact) localPath := testStorage.LocalPath(*obj.Status.Artifact)
t.Logf("artifact local path: %s", localPath) t.Logf("artifact local path: %s", localPath)
@ -252,6 +255,7 @@ func TestOCIRepository_SecretRef(t *testing.T) {
ociURL := fmt.Sprintf("oci://%s", repositoryURL) ociURL := fmt.Sprintf("oci://%s", repositoryURL)
// Push Test Image // Push Test Image
image = setPodinfoImageAnnotations(image, "6.1.6")
err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{ err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{
Username: testRegistryUsername, Username: testRegistryUsername,
Password: testRegistryPassword, Password: testRegistryPassword,
@ -265,7 +269,7 @@ func TestOCIRepository_SecretRef(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
url string url string
digest v1.Hash digest gcrv1.Hash
includeSecretRef bool includeSecretRef bool
includeServiceAccount bool includeServiceAccount bool
}{ }{
@ -449,6 +453,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) {
ociURL := fmt.Sprintf("oci://%s", repositoryURL) ociURL := fmt.Sprintf("oci://%s", repositoryURL)
// Push Test Image // Push Test Image
image = setPodinfoImageAnnotations(image, "6.1.6")
err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{ err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{
Username: testRegistryUsername, Username: testRegistryUsername,
Password: testRegistryPassword, Password: testRegistryPassword,
@ -462,7 +467,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
url string url string
digest v1.Hash digest gcrv1.Hash
repoUsername string repoUsername string
repoPassword string repoPassword string
includeSecretRef bool includeSecretRef bool
@ -644,7 +649,7 @@ func TestOCIRepository_CertSecret(t *testing.T) {
name string name string
url string url string
tag string tag string
digest v1.Hash digest gcrv1.Hash
certSecret *corev1.Secret certSecret *corev1.Secret
expectreadyconition bool expectreadyconition bool
expectedstatusmessage string expectedstatusmessage string
@ -760,7 +765,7 @@ type artifactFixture struct {
type podinfoImage struct { type podinfoImage struct {
url string url string
tag string tag string
digest v1.Hash digest gcrv1.Hash
} }
func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Server) (*podinfoImage, error) { func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Server) (*podinfoImage, error) {
@ -770,6 +775,8 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se
return nil, err return nil, err
} }
image = setPodinfoImageAnnotations(image, tag)
url, err := url.Parse(imageServer.URL) url, err := url.Parse(imageServer.URL)
if err != nil { if err != nil {
return nil, err return nil, err
@ -784,7 +791,6 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se
// Push image // Push image
err = crane.Push(image, repositoryURL, crane.WithTransport(imageServer.Client().Transport)) err = crane.Push(image, repositoryURL, crane.WithTransport(imageServer.Client().Transport))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -802,8 +808,15 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se
}, nil }, nil
} }
// These two taken verbatim from https://ericchiang.github.io/post/go-tls/ func setPodinfoImageAnnotations(img gcrv1.Image, tag string) gcrv1.Image {
metadata := map[string]string{
"org.opencontainers.image.source": "https://github.com/stefanprodan/podinfo",
"org.opencontainers.image.revision": fmt.Sprintf("%s/SHA", tag),
}
return mutate.Annotations(img, metadata).(gcrv1.Image)
}
// These two taken verbatim from https://ericchiang.github.io/post/go-tls/
func certTemplate() (*x509.Certificate, error) { func certTemplate() (*x509.Certificate, error) {
// generate a random serial number (a real cert authority would // generate a random serial number (a real cert authority would
// have some logic behind this) // have some logic behind this)
@ -842,8 +855,6 @@ func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv
return return
} }
// ----
func createTLSServer() (*httptest.Server, []byte, []byte, []byte, tls.Certificate, error) { func createTLSServer() (*httptest.Server, []byte, []byte, []byte, tls.Certificate, error) {
var clientTLSCert tls.Certificate var clientTLSCert tls.Certificate
var rootCertPEM, clientCertPEM, clientKeyPEM []byte var rootCertPEM, clientCertPEM, clientKeyPEM []byte

View File

@ -161,6 +161,8 @@ func setupRegistryServer(ctx context.Context) (*registryClientTestServer, error)
server.registryHost = fmt.Sprintf("localhost:%d", port) server.registryHost = fmt.Sprintf("localhost:%d", port)
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port) config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.HTTP.DrainTimeout = time.Duration(10) * time.Second
config.Log.AccessLog.Disabled = true
config.Log.Level = "error"
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
config.Auth = configuration.Auth{ config.Auth = configuration.Auth{
"htpasswd": configuration.Parameters{ "htpasswd": configuration.Parameters{

View File

@ -1190,6 +1190,18 @@ int64
<p>Size is the number of bytes in the file.</p> <p>Size is the number of bytes in the file.</p>
</td> </td>
</tr> </tr>
<tr>
<td>
<code>metadata</code><br>
<em>
map[string]string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Metadata holds upstream information such as OCI annotations.</p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>