1 // Package oci contains OCI functionality and types that are used to support 2 // Warehouse specific implementations in other packages. It is used to enforce 3 // Warehouse semantics on top of OCI semantics in a reusable way. 4 // 5 // Behavior that is specific to types of Warehouse packages (e.g., Pallets), 6 // should be placed in that package, not here. 7 package oci 8 9 import ( 10 "bytes" 11 "fmt" 12 13 v1 "github.com/google/go-containerregistry/pkg/v1" 14 "github.com/google/go-containerregistry/pkg/v1/mutate" 15 "github.com/google/go-containerregistry/pkg/v1/partial" 16 17 "edge-infra.dev/pkg/f8n/warehouse" 18 ) 19 20 // Artifact represents the intersection between v1.Image and v1.ImageIndex by 21 // embedding the two partial interfaces required for common OCI operations in 22 // google/go-containerregistry. This interface allows us to pass around warehouse 23 // artifacts without concerning callers about whether or not the artifact is an 24 // v1.Image or v1.ImageIndex. 25 // 26 // Artifacts can be used to represent both Pallets and Cluster package types, 27 // as well as any future Warehouse package types, as long as they embed this 28 // interface. 29 type Artifact interface { 30 // Describable represents anything that we can produce a descriptor for, 31 // e.g., what gets embedded in in v1.Image and v1.ImageIndex to reference 32 // other OCI entities. 33 // Specifically, that means things that implement partial.Describable can be 34 // appended or composed with existing artifacts via the mutate package. 35 partial.Describable 36 37 // WithRawManifest is the lowest common denominator which can be used to 38 // implement v1.Image and v1.ImageIndex. Nearly all implementations for those 39 // interfaces in google/go-containerregistry embed this interface, allowing 40 // us to easily use those packages when working with artifacts. 41 partial.WithRawManifest 42 } 43 44 // Unwrapper allows access to the underlying OCI artifact via the Unwrap method. 45 type Unwrapper interface { 46 // Unwrap returns the underlying oci.Artifact, allowing for matching based on 47 // ggcr interface types (v1.ImageIndex, v1.Image, etc) 48 Unwrap() Artifact 49 } 50 51 // Annotate adds a series of annotation maps to an OCI artifact and returns 52 // the annotated artifact. 53 // Note that OCI artifacts are immutable: this function is really creating a new 54 // artifact that is the input + the annotations. 55 // 56 // Does not do key collision checks. Must be casted back to original type (add example) 57 func Annotate(a partial.WithRawManifest, annos ...map[string]string) partial.WithRawManifest { 58 m := map[string]string{} 59 for _, anno := range annos { 60 for k, v := range anno { 61 m[k] = v 62 } 63 } 64 65 return mutate.Annotations(a, m) 66 } 67 68 // Annotations returns the OCI manifest annotations for the input artifact after 69 // inferring the concrete type. 70 func Annotations(a Artifact) (map[string]string, error) { 71 switch a := a.(type) { 72 case Unwrapper: 73 return Annotations(a.Unwrap()) 74 case v1.Image: 75 // lazily evaluate manifest to avoid doing it twice if input is Unwrapper 76 raw, err := a.RawManifest() 77 if err != nil { 78 return nil, fmt.Errorf("failed to load oci manifest from artifact: %w", err) 79 } 80 manifest, err := v1.ParseManifest(bytes.NewReader(raw)) 81 if err != nil { 82 return nil, fmt.Errorf("failed to parse image manifest: %w", err) 83 } 84 return manifest.Annotations, nil 85 case v1.ImageIndex: 86 // lazily evaluate manifest to avoid doing it twice if input is Unwrapper 87 raw, err := a.RawManifest() 88 if err != nil { 89 return nil, fmt.Errorf("failed to load oci manifest from artifact: %w", err) 90 } 91 manifest, err := v1.ParseIndexManifest(bytes.NewReader(raw)) 92 if err != nil { 93 return nil, fmt.Errorf("failed to parse image index manifest: %w", err) 94 } 95 return manifest.Annotations, nil 96 default: 97 return nil, fmt.Errorf("%w", ErrInvalidArtifact) 98 } 99 } 100 101 // Contains returns true if the v1.ImageIndex embeds a descriptor referencing the 102 // Artifact, after inferring the concrete type. The descriptor's ref name 103 // annotation must match the Artifacts name annotation and the digests must match. 104 func Contains(idx v1.ImageIndex, a Artifact) (bool, error) { 105 digest, err := a.Digest() 106 if err != nil { 107 return false, err 108 } 109 annos, err := Annotations(a) 110 if err != nil { 111 return false, err 112 } 113 114 idxm, err := idx.IndexManifest() 115 if err != nil { 116 return false, err 117 } 118 119 for _, m := range idxm.Manifests { 120 if m.Annotations[warehouse.AnnotationRefName] == 121 annos[warehouse.AnnotationName] && digest == m.Digest { 122 return true, nil 123 } 124 } 125 126 return false, nil 127 } 128 129 // IsPackage determines if the v1.Image is a standalone package or not by checking 130 // if it is embedded into the Artifact and has the same name as the 131 // Artifact. e.g., v1.Images named shoot will be embedded into a v1.ImageIndex 132 // named shoot in order to represent different provider variants, or if shoot has 133 // dependencies. 134 // 135 // If the Artifact is nil, the Image is assumed to be a package. Anything other 136 // than nil or an ImageIndex will return an error, as it means you have goofed 137 // up pretty thoroughly. 138 // 139 // If an ImageIndex contains an Image, that represents either an ImageIndex for 140 // a package with provider-specific variants (shoot:gke, shoot:sds) or a package 141 // with dependencies. This is a useful signal while walking artifacts from root 142 // nodes. If you only care about walking the root nodes of each individual package 143 // in the graph, you can use this function to skip walking Images 144 func IsPackage(img v1.Image, parent Artifact) (bool, error) { 145 imgAnnos, err := Annotations(img) 146 if err != nil { 147 return false, err 148 } 149 150 switch a := parent.(type) { 151 case Unwrapper: 152 return IsPackage(img, a.Unwrap()) 153 case v1.ImageIndex: 154 manifest, err := a.IndexManifest() 155 if err != nil { 156 return false, err 157 } 158 159 return imgAnnos[warehouse.AnnotationName] != 160 manifest.Annotations[warehouse.AnnotationName], nil 161 162 // confirm that idx embeds image explicitly 163 // this level of safety isn't explicitly required in the context of 164 // using oci.Walk, since oci.Walk is traversing the descriptors it is 165 // guaranteed that the parent embeds the input img. however, that 166 // isn't the case if some other client was using IsPackage() outside of 167 // the conext of walk. if that use case ever shows up, uncomment this 168 // code: 169 // refs, err := Contains(a, img) 170 // if err != nil { 171 // return false, err 172 // } 173 // return !refs, nil 174 case nil: 175 return true, nil 176 default: 177 return false, fmt.Errorf("non-ImageIndex type %T was provided as parent", a) 178 } 179 } 180 181 // IsComposite returns true if the Artifact is a v1.ImageIndex which only contains 182 // embedded descriptors referencing other v1.ImageIndexes or standalone packages 183 // that are v1.Images. This means that the index is a composite index that only 184 // exists to compose multiple distinct package together. 185 // 186 // If the Artifact is anything but an ImageIndex, it exits gracefully with false. 187 func IsComposite(a Artifact) (bool, error) { 188 switch a := a.(type) { 189 case Unwrapper: 190 return IsComposite(a.Unwrap()) 191 case v1.ImageIndex: 192 manifest, err := a.IndexManifest() 193 if err != nil { 194 return false, err 195 } 196 for _, m := range manifest.Manifests { 197 if !m.MediaType.IsIndex() && 198 m.Annotations[warehouse.AnnotationRefName] == 199 manifest.Annotations[warehouse.AnnotationName] { 200 return false, nil 201 } 202 } 203 return true, nil 204 default: 205 return false, nil 206 } 207 } 208 209 // TODO: keep? doc? 210 func ArtifactFromIdx(idx v1.ImageIndex, d v1.Descriptor) (Artifact, error) { 211 switch { 212 case d.MediaType.IsImage(): 213 return idx.Image(d.Digest) 214 case d.MediaType.IsIndex(): 215 return idx.ImageIndex(d.Digest) 216 default: 217 return nil, fmt.Errorf("%w: unexpected mediaType %s", 218 ErrInvalidArtifact, d.MediaType) 219 } 220 } 221