Skip to content

Commit 05f9c0e

Browse files
committed
Add the OCI metadata to the internal artifact
Signed-off-by: Stefan Prodan <[email protected]>
1 parent 5072091 commit 05f9c0e

12 files changed

+112
-28
lines changed

api/v1beta2/artifact_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ type Artifact struct {
5454
// Size is the number of bytes in the file.
5555
// +optional
5656
Size *int64 `json:"size,omitempty"`
57+
58+
// Metadata holds upstream information such as OCI annotations.
59+
// +optional
60+
Metadata map[string]string `json:"metadata,omitempty"`
5761
}
5862

5963
// HasRevision returns if the given revision matches the current Revision of

api/v1beta2/ocirepository_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ limitations under the License.
1717
package v1beta2
1818

1919
import (
20-
"github.com/fluxcd/pkg/apis/meta"
21-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2220
"time"
21+
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
24+
"github.com/fluxcd/pkg/apis/meta"
2325
)
2426

2527
const (

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 7 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_buckets.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ spec:
384384
the last update of the Artifact.
385385
format: date-time
386386
type: string
387+
metadata:
388+
additionalProperties:
389+
type: string
390+
description: Metadata holds upstream information such as OCI annotations.
391+
type: object
387392
path:
388393
description: Path is the relative file path of the Artifact. It
389394
can be used to locate the file in the root of the Artifact storage

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,11 @@ spec:
559559
the last update of the Artifact.
560560
format: date-time
561561
type: string
562+
metadata:
563+
additionalProperties:
564+
type: string
565+
description: Metadata holds upstream information such as OCI annotations.
566+
type: object
562567
path:
563568
description: Path is the relative file path of the Artifact. It
564569
can be used to locate the file in the root of the Artifact storage
@@ -677,6 +682,12 @@ spec:
677682
the last update of the Artifact.
678683
format: date-time
679684
type: string
685+
metadata:
686+
additionalProperties:
687+
type: string
688+
description: Metadata holds upstream information such as OCI
689+
annotations.
690+
type: object
680691
path:
681692
description: Path is the relative file path of the Artifact.
682693
It can be used to locate the file in the root of the Artifact

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,11 @@ spec:
432432
the last update of the Artifact.
433433
format: date-time
434434
type: string
435+
metadata:
436+
additionalProperties:
437+
type: string
438+
description: Metadata holds upstream information such as OCI annotations.
439+
type: object
435440
path:
436441
description: Path is the relative file path of the Artifact. It
437442
can be used to locate the file in the root of the Artifact storage

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ spec:
362362
the last update of the Artifact.
363363
format: date-time
364364
type: string
365+
metadata:
366+
additionalProperties:
367+
type: string
368+
description: Metadata holds upstream information such as OCI annotations.
369+
type: object
365370
path:
366371
description: Path is the relative file path of the Artifact. It
367372
can be used to locate the file in the root of the Artifact storage

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ spec:
142142
the last update of the Artifact.
143143
format: date-time
144144
type: string
145+
metadata:
146+
additionalProperties:
147+
type: string
148+
description: Metadata holds upstream information such as OCI annotations.
149+
type: object
145150
path:
146151
description: Path is the relative file path of the Artifact. It
147152
can be used to locate the file in the root of the Artifact storage

controllers/ocirepository_controller.go

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
"github.com/google/go-containerregistry/pkg/authn/k8schain"
3434
"github.com/google/go-containerregistry/pkg/crane"
3535
"github.com/google/go-containerregistry/pkg/name"
36-
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
3736
"github.com/google/go-containerregistry/pkg/v1/remote"
3837
corev1 "k8s.io/api/core/v1"
3938
"k8s.io/apimachinery/pkg/runtime"
@@ -110,7 +109,7 @@ var ociRepositoryFailConditions = []string{
110109
// ociRepositoryReconcileFunc is the function type for all the v1beta2.OCIRepository
111110
// (sub)reconcile functions. The type implementations are grouped and
112111
// executed serially to perform the complete reconcile of the object.
113-
type ociRepositoryReconcileFunc func(ctx context.Context, obj *sourcev1.OCIRepository, digest *gcrv1.Hash, dir string) (sreconcile.Result, error)
112+
type ociRepositoryReconcileFunc func(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error)
114113

115114
// OCIRepositoryReconciler reconciles a v1beta2.OCIRepository object
116115
type OCIRepositoryReconciler struct {
@@ -261,16 +260,15 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
261260
}()
262261
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
263262

264-
hs := gcrv1.Hash{}
265263
var (
266-
res sreconcile.Result
267-
resErr error
268-
digest = hs.DeepCopy()
264+
res sreconcile.Result
265+
resErr error
266+
metadata = sourcev1.Artifact{}
269267
)
270268

271269
// Run the sub-reconcilers and build the result of reconciliation.
272270
for _, rec := range reconcilers {
273-
recResult, err := rec(ctx, obj, digest, tmpDir)
271+
recResult, err := rec(ctx, obj, &metadata, tmpDir)
274272
// Exit immediately on ResultRequeue.
275273
if recResult == sreconcile.ResultRequeue {
276274
return sreconcile.ResultRequeue, nil
@@ -286,14 +284,14 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
286284
res = sreconcile.LowestRequeuingResult(res, recResult)
287285
}
288286

289-
r.notify(ctx, oldObj, obj, digest, res, resErr)
287+
r.notify(ctx, oldObj, obj, res, resErr)
290288

291289
return res, resErr
292290
}
293291

294292
// reconcileSource fetches the upstream OCI artifact metadata and content.
295293
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
296-
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, digest *gcrv1.Hash, dir string) (sreconcile.Result, error) {
294+
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
297295
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
298296
defer cancel()
299297

@@ -352,9 +350,25 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
352350
}
353351

354352
// Set the internal revision to the remote digest hex
355-
imgDigest.DeepCopyInto(digest)
356353
revision := imgDigest.Hex
357354

355+
// Copy the OCI annotations to the internal artifact metadata
356+
manifest, err := img.Manifest()
357+
if err != nil {
358+
e := serror.NewGeneric(
359+
fmt.Errorf("failed to parse artifact manifest: %w", err),
360+
sourcev1.OCIOperationFailedReason,
361+
)
362+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
363+
return sreconcile.ResultEmpty, e
364+
}
365+
366+
m := &sourcev1.Artifact{
367+
Revision: revision,
368+
Metadata: manifest.Annotations,
369+
}
370+
m.DeepCopyInto(metadata)
371+
358372
// Mark observations about the revision on the object
359373
defer func() {
360374
if !obj.GetArtifact().HasRevision(revision) {
@@ -606,7 +620,7 @@ func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context,
606620
// The hostname of any URL in the Status of the object are updated, to ensure
607621
// they match the Storage server hostname of current runtime.
608622
func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
609-
obj *sourcev1.OCIRepository, _ *gcrv1.Hash, _ string) (sreconcile.Result, error) {
623+
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) {
610624
// Garbage collect previous advertised artifact(s) from storage
611625
_ = r.garbageCollect(ctx, obj)
612626

@@ -642,9 +656,9 @@ func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
642656
// On a successful archive, the Artifact in the Status of the object is set,
643657
// and the symlink in the Storage is updated to its path.
644658
func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
645-
obj *sourcev1.OCIRepository, digest *gcrv1.Hash, dir string) (sreconcile.Result, error) {
659+
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
646660
// Calculate revision
647-
revision := digest.Hex
661+
revision := metadata.Revision
648662

649663
// Create artifact
650664
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,
712726

713727
// Record it on the object
714728
obj.Status.Artifact = artifact.DeepCopy()
729+
obj.Status.Artifact.Metadata = metadata.Metadata
715730

716731
// Update symlink on a "best effort" basis
717732
url, err := r.Storage.Symlink(artifact, "latest.tar.gz")
@@ -798,7 +813,7 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
798813

799814
// notify emits notification related to the reconciliation.
800815
func (r *OCIRepositoryReconciler) notify(ctx context.Context,
801-
oldObj, newObj *sourcev1.OCIRepository, digest *gcrv1.Hash, res sreconcile.Result, resErr error) {
816+
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) {
802817
// Notify successful reconciliation for new artifact and recovery from any
803818
// failure.
804819
if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil {
@@ -812,7 +827,7 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
812827
oldChecksum = oldObj.GetArtifact().Checksum
813828
}
814829

815-
message := fmt.Sprintf("stored artifact with digest '%s' from '%s'", digest.String(), newObj.Spec.URL)
830+
message := fmt.Sprintf("stored artifact with digest '%s' from '%s'", newObj.Status.Artifact.Revision, newObj.Spec.URL)
816831

817832
// Notify on new artifact and failure recovery.
818833
if oldChecksum != newObj.GetArtifact().Checksum {

controllers/ocirepository_controller_test.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ import (
4444
"github.com/google/go-containerregistry/pkg/authn"
4545
"github.com/google/go-containerregistry/pkg/crane"
4646
"github.com/google/go-containerregistry/pkg/registry"
47-
v1 "github.com/google/go-containerregistry/pkg/v1"
47+
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
48+
"github.com/google/go-containerregistry/pkg/v1/mutate"
4849
. "github.com/onsi/gomega"
4950
corev1 "k8s.io/api/core/v1"
5051
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -163,11 +164,13 @@ func TestOCIRepository_Reconcile(t *testing.T) {
163164
obj.Generation == obj.Status.ObservedGeneration
164165
}, timeout).Should(BeTrue())
165166

166-
t.Log(obj.Spec.Reference)
167-
168167
// Check if the revision matches the expected digest
169168
g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest))
170169

170+
// Check if the metadata matches the expected annotations
171+
g.Expect(obj.Status.Artifact.Metadata["org.opencontainers.image.source"]).To(ContainSubstring("podinfo"))
172+
g.Expect(obj.Status.Artifact.Metadata["org.opencontainers.image.revision"]).To(ContainSubstring(tt.tag))
173+
171174
// Check if the artifact storage path matches the expected file path
172175
localPath := testStorage.LocalPath(*obj.Status.Artifact)
173176
t.Logf("artifact local path: %s", localPath)
@@ -252,6 +255,7 @@ func TestOCIRepository_SecretRef(t *testing.T) {
252255
ociURL := fmt.Sprintf("oci://%s", repositoryURL)
253256

254257
// Push Test Image
258+
image = setPodinfoImageAnnotations(image, "6.1.6")
255259
err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{
256260
Username: testRegistryUsername,
257261
Password: testRegistryPassword,
@@ -265,7 +269,7 @@ func TestOCIRepository_SecretRef(t *testing.T) {
265269
tests := []struct {
266270
name string
267271
url string
268-
digest v1.Hash
272+
digest gcrv1.Hash
269273
includeSecretRef bool
270274
includeServiceAccount bool
271275
}{
@@ -449,6 +453,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) {
449453
ociURL := fmt.Sprintf("oci://%s", repositoryURL)
450454

451455
// Push Test Image
456+
image = setPodinfoImageAnnotations(image, "6.1.6")
452457
err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{
453458
Username: testRegistryUsername,
454459
Password: testRegistryPassword,
@@ -462,7 +467,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) {
462467
tests := []struct {
463468
name string
464469
url string
465-
digest v1.Hash
470+
digest gcrv1.Hash
466471
repoUsername string
467472
repoPassword string
468473
includeSecretRef bool
@@ -644,7 +649,7 @@ func TestOCIRepository_CertSecret(t *testing.T) {
644649
name string
645650
url string
646651
tag string
647-
digest v1.Hash
652+
digest gcrv1.Hash
648653
certSecret *corev1.Secret
649654
expectreadyconition bool
650655
expectedstatusmessage string
@@ -760,7 +765,7 @@ type artifactFixture struct {
760765
type podinfoImage struct {
761766
url string
762767
tag string
763-
digest v1.Hash
768+
digest gcrv1.Hash
764769
}
765770

766771
func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Server) (*podinfoImage, error) {
@@ -770,6 +775,8 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se
770775
return nil, err
771776
}
772777

778+
image = setPodinfoImageAnnotations(image, tag)
779+
773780
url, err := url.Parse(imageServer.URL)
774781
if err != nil {
775782
return nil, err
@@ -784,7 +791,6 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se
784791

785792
// Push image
786793
err = crane.Push(image, repositoryURL, crane.WithTransport(imageServer.Client().Transport))
787-
788794
if err != nil {
789795
return nil, err
790796
}
@@ -802,8 +808,15 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se
802808
}, nil
803809
}
804810

805-
// These two taken verbatim from https://ericchiang.github.io/post/go-tls/
811+
func setPodinfoImageAnnotations(img gcrv1.Image, tag string) gcrv1.Image {
812+
metadata := map[string]string{
813+
"org.opencontainers.image.source": "https://github.com/stefanprodan/podinfo",
814+
"org.opencontainers.image.revision": fmt.Sprintf("%s/SHA", tag),
815+
}
816+
return mutate.Annotations(img, metadata).(gcrv1.Image)
817+
}
806818

819+
// These two taken verbatim from https://ericchiang.github.io/post/go-tls/
807820
func certTemplate() (*x509.Certificate, error) {
808821
// generate a random serial number (a real cert authority would
809822
// have some logic behind this)
@@ -842,8 +855,6 @@ func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv
842855
return
843856
}
844857

845-
// ----
846-
847858
func createTLSServer() (*httptest.Server, []byte, []byte, []byte, tls.Certificate, error) {
848859
var clientTLSCert tls.Certificate
849860
var rootCertPEM, clientCertPEM, clientKeyPEM []byte

controllers/suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ func setupRegistryServer(ctx context.Context) (*registryClientTestServer, error)
161161
server.registryHost = fmt.Sprintf("localhost:%d", port)
162162
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
163163
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
164+
config.Log.AccessLog.Disabled = true
165+
config.Log.Level = "error"
164166
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
165167
config.Auth = configuration.Auth{
166168
"htpasswd": configuration.Parameters{

docs/api/source.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,18 @@ int64
11901190
<p>Size is the number of bytes in the file.</p>
11911191
</td>
11921192
</tr>
1193+
<tr>
1194+
<td>
1195+
<code>metadata</code><br>
1196+
<em>
1197+
map[string]string
1198+
</em>
1199+
</td>
1200+
<td>
1201+
<em>(Optional)</em>
1202+
<p>Metadata holds upstream information such as OCI annotations.</p>
1203+
</td>
1204+
</tr>
11931205
</tbody>
11941206
</table>
11951207
</div>

0 commit comments

Comments
 (0)