github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/oci/build.go (about) 1 // Copyright 2023-2024 The Inspektor Gadget authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package oci 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "os" 25 26 "github.com/distribution/reference" 27 "github.com/opencontainers/image-spec/specs-go" 28 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 29 "gopkg.in/yaml.v2" 30 "oras.land/oras-go/v2" 31 "oras.land/oras-go/v2/content" 32 "oras.land/oras-go/v2/errdef" 33 34 metadatav1 "github.com/inspektor-gadget/inspektor-gadget/pkg/metadata/v1" 35 ) 36 37 const ( 38 ArchAmd64 = "amd64" 39 ArchArm64 = "arm64" 40 ArchWasm = "wasm" 41 ) 42 43 const ( 44 eBPFObjectMediaType = "application/vnd.gadget.ebpf.program.v1+binary" 45 wasmObjectMediaType = "application/vnd.gadget.wasm.program.v1+binary" 46 btfgenMediaType = "application/vnd.gadget.btfgen.v1+binary" 47 metadataMediaType = "application/vnd.gadget.config.v1+yaml" 48 ) 49 50 type ObjectPath struct { 51 // Path to the EBPF object 52 EBPF string 53 // Optional path to the Wasm file 54 Wasm string 55 // Optional path to tarball containing BTF files generated with btfgen 56 Btfgen string 57 } 58 59 type BuildGadgetImageOpts struct { 60 // Source path of the eBPF program. Currently it's not used for compilation purposes 61 EBPFSourcePath string 62 // List of eBPF objects to include in the image. The key is the architecture and the value 63 // are the paths to the objects. 64 ObjectPaths map[string]*ObjectPath 65 // Path to the metadata file. 66 MetadataPath string 67 // If true, the metadata is updated to follow changes in the eBPF objects. 68 UpdateMetadata bool 69 // If true, the metadata is validated before creating the image. 70 ValidateMetadata bool 71 // Date and time on which the image is built (date-time string as defined by RFC 3339). 72 CreatedDate string 73 } 74 75 // BuildGadgetImage creates an OCI image with the objects provided in opts. The image parameter in 76 // the "name:tag" format is used to name and tag the created image. If it's empty the image is not 77 // named. 78 func BuildGadgetImage(ctx context.Context, opts *BuildGadgetImageOpts, image string) (*GadgetImageDesc, error) { 79 ociStore, err := getLocalOciStore() 80 if err != nil { 81 return nil, fmt.Errorf("getting oci store: %w", err) 82 } 83 84 if opts.UpdateMetadata { 85 if err := createOrUpdateMetadataFile(ctx, opts); err != nil { 86 return nil, fmt.Errorf("updating metadata file: %w", err) 87 } 88 } 89 90 if opts.ValidateMetadata { 91 if err := validateMetadataFile(ctx, opts); err != nil && !errors.Is(err, os.ErrNotExist) { 92 return nil, fmt.Errorf("validating metadata file: %w", err) 93 } 94 } 95 96 indexDesc, err := createImageIndex(ctx, ociStore, opts) 97 if err != nil { 98 return nil, fmt.Errorf("creating image index: %w", err) 99 } 100 101 imageDesc := &GadgetImageDesc{ 102 Digest: indexDesc.Digest.String(), 103 } 104 105 if image != "" { 106 targetImage, err := normalizeImageName(image) 107 if err != nil { 108 return nil, fmt.Errorf("normalizing image: %w", err) 109 } 110 111 err = ociStore.Tag(ctx, indexDesc, targetImage.String()) 112 if err != nil { 113 return nil, fmt.Errorf("tagging manifest: %w", err) 114 } 115 116 imageDesc.Repository = targetImage.Name() 117 if ref, ok := targetImage.(reference.Tagged); ok { 118 imageDesc.Tag = ref.Tag() 119 } 120 } 121 122 if err := fixGeneratedFilesOwner(opts); err != nil { 123 return nil, fmt.Errorf("fixing generated files owner: %w", err) 124 } 125 126 return imageDesc, nil 127 } 128 129 func pushDescriptorIfNotExists(ctx context.Context, target oras.Target, desc ocispec.Descriptor, contentReader io.Reader) error { 130 err := target.Push(ctx, desc, contentReader) 131 if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { 132 return fmt.Errorf("pushing descriptor: %w", err) 133 } 134 return nil 135 } 136 137 func createLayerDesc(ctx context.Context, target oras.Target, progFilePath, mediaType string) (ocispec.Descriptor, error) { 138 progBytes, err := os.ReadFile(progFilePath) 139 if err != nil { 140 return ocispec.Descriptor{}, fmt.Errorf("reading eBPF program file: %w", err) 141 } 142 progDesc := content.NewDescriptorFromBytes(mediaType, progBytes) 143 144 err = pushDescriptorIfNotExists(ctx, target, progDesc, bytes.NewReader(progBytes)) 145 if err != nil { 146 return ocispec.Descriptor{}, fmt.Errorf("pushing %q layer: %w", mediaType, err) 147 } 148 149 return progDesc, nil 150 } 151 152 func annotationsFromMetadata(metadataBytes []byte) (map[string]string, error) { 153 metadata := &metadatav1.GadgetMetadata{} 154 if err := yaml.NewDecoder(bytes.NewReader(metadataBytes)).Decode(&metadata); err != nil { 155 return nil, fmt.Errorf("decoding metadata file: %w", err) 156 } 157 158 // Suggested annotations for the OCI image 159 // https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys 160 annotations := map[string]string{ 161 ocispec.AnnotationTitle: metadata.Name, 162 ocispec.AnnotationDescription: metadata.Description, 163 ocispec.AnnotationURL: metadata.HomepageURL, 164 ocispec.AnnotationDocumentation: metadata.DocumentationURL, 165 ocispec.AnnotationSource: metadata.SourceURL, 166 } 167 168 for k, v := range metadata.Annotations { 169 annotations[k] = v 170 } 171 return annotations, nil 172 } 173 174 func createMetadataDesc(ctx context.Context, target oras.Target, metadataFilePath string) (ocispec.Descriptor, error) { 175 metadataBytes, err := os.ReadFile(metadataFilePath) 176 if err != nil { 177 return ocispec.Descriptor{}, fmt.Errorf("reading metadata file: %w", err) 178 } 179 defDesc := content.NewDescriptorFromBytes(metadataMediaType, metadataBytes) 180 defDesc.Annotations, err = annotationsFromMetadata(metadataBytes) 181 if err != nil { 182 return ocispec.Descriptor{}, fmt.Errorf("reading annotations from metadata file: %w", err) 183 } 184 185 err = pushDescriptorIfNotExists(ctx, target, defDesc, bytes.NewReader(metadataBytes)) 186 if err != nil { 187 return ocispec.Descriptor{}, fmt.Errorf("pushing metadata file: %w", err) 188 } 189 return defDesc, nil 190 } 191 192 func createEmptyDesc(ctx context.Context, target oras.Target) (ocispec.Descriptor, error) { 193 emptyDesc := ocispec.DescriptorEmptyJSON 194 err := pushDescriptorIfNotExists(ctx, target, emptyDesc, bytes.NewReader(emptyDesc.Data)) 195 if err != nil { 196 return ocispec.Descriptor{}, fmt.Errorf("pushing empty descriptor: %w", err) 197 } 198 return emptyDesc, nil 199 } 200 201 func createManifestForTarget(ctx context.Context, target oras.Target, metadataFilePath, arch string, paths *ObjectPath, createdDate string) (ocispec.Descriptor, error) { 202 layerDescs := []ocispec.Descriptor{} 203 204 progDesc, err := createLayerDesc(ctx, target, paths.EBPF, eBPFObjectMediaType) 205 if err != nil { 206 return ocispec.Descriptor{}, fmt.Errorf("creating and pushing eBPF descriptor: %w", err) 207 } 208 layerDescs = append(layerDescs, progDesc) 209 210 if paths.Wasm != "" { 211 wasmDesc, err := createLayerDesc(ctx, target, paths.Wasm, wasmObjectMediaType) 212 if err != nil { 213 return ocispec.Descriptor{}, fmt.Errorf("creating and pushing wasm descriptor: %w", err) 214 } 215 layerDescs = append(layerDescs, wasmDesc) 216 } 217 218 if paths.Btfgen != "" { 219 btfDesc, err := createLayerDesc(ctx, target, paths.Btfgen, btfgenMediaType) 220 if err != nil { 221 return ocispec.Descriptor{}, fmt.Errorf("creating and pushing btfgen descriptor: %w", err) 222 } 223 layerDescs = append(layerDescs, btfDesc) 224 } 225 226 var defDesc ocispec.Descriptor 227 // artifactType must be only set when the config.mediaType is set to 228 // MediaTypeEmptyJSON. In our case, when the metadata file is not provided: 229 // https://github.com/opencontainers/image-spec/blob/f5f87016de46439ccf91b5381cf76faaae2bc28f/manifest.md?plain=1#L170 230 var artifactType string 231 232 if _, err := os.Stat(metadataFilePath); err == nil { 233 // Read the metadata file into a byte array 234 defDesc, err = createMetadataDesc(ctx, target, metadataFilePath) 235 if err != nil { 236 return ocispec.Descriptor{}, fmt.Errorf("creating metadata descriptor: %w", err) 237 } 238 defDesc.Annotations[ocispec.AnnotationCreated] = createdDate 239 } else { 240 // Create an empty descriptor 241 defDesc, err = createEmptyDesc(ctx, target) 242 if err != nil { 243 return ocispec.Descriptor{}, fmt.Errorf("creating empty descriptor: %w", err) 244 } 245 artifactType = eBPFObjectMediaType 246 247 // Even without metadata, we can still set some annotations 248 defDesc.Annotations = map[string]string{ 249 ocispec.AnnotationCreated: createdDate, 250 } 251 } 252 253 // Create the manifest which combines everything and push it to the memory store 254 manifest := ocispec.Manifest{ 255 Versioned: specs.Versioned{ 256 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 257 }, 258 Config: defDesc, 259 Layers: layerDescs, 260 Annotations: defDesc.Annotations, 261 ArtifactType: artifactType, 262 } 263 264 manifestJson, err := json.Marshal(manifest) 265 if err != nil { 266 return ocispec.Descriptor{}, fmt.Errorf("marshalling manifest: %w", err) 267 } 268 manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifestJson) 269 manifestDesc.Platform = &ocispec.Platform{ 270 Architecture: arch, 271 OS: "linux", 272 } 273 manifestDesc.Annotations = manifest.Annotations 274 275 exists, err := target.Exists(ctx, manifestDesc) 276 if err != nil { 277 return ocispec.Descriptor{}, fmt.Errorf("checking if manifest exists: %w", err) 278 } 279 if exists { 280 return manifestDesc, nil 281 } 282 err = pushDescriptorIfNotExists(ctx, target, manifestDesc, bytes.NewReader(manifestJson)) 283 if err != nil { 284 return ocispec.Descriptor{}, fmt.Errorf("pushing manifest: %w", err) 285 } 286 287 return manifestDesc, nil 288 } 289 290 func createImageIndex(ctx context.Context, target oras.Target, o *BuildGadgetImageOpts) (ocispec.Descriptor, error) { 291 // Read the eBPF program files and push them to the memory store 292 layers := []ocispec.Descriptor{} 293 294 for arch, paths := range o.ObjectPaths { 295 manifestDesc, err := createManifestForTarget(ctx, target, o.MetadataPath, arch, paths, o.CreatedDate) 296 if err != nil { 297 return ocispec.Descriptor{}, fmt.Errorf("creating %s manifest: %w", arch, err) 298 } 299 layers = append(layers, manifestDesc) 300 } 301 302 if len(layers) == 0 { 303 return ocispec.Descriptor{}, fmt.Errorf("no eBPF objects found") 304 } 305 306 // Create the index which combines the architectures and push it to the memory store 307 index := ocispec.Index{ 308 Versioned: specs.Versioned{ 309 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 310 }, 311 MediaType: ocispec.MediaTypeImageIndex, 312 Manifests: layers, 313 Annotations: layers[0].Annotations, 314 } 315 indexJson, err := json.Marshal(index) 316 if err != nil { 317 return ocispec.Descriptor{}, fmt.Errorf("marshalling manifest: %w", err) 318 } 319 indexDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexJson) 320 indexDesc.Annotations = index.Annotations 321 322 err = pushDescriptorIfNotExists(ctx, target, indexDesc, bytes.NewReader(indexJson)) 323 if err != nil { 324 return ocispec.Descriptor{}, fmt.Errorf("pushing manifest index: %w", err) 325 } 326 return indexDesc, nil 327 }