// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package validate import ( "bytes" "errors" "fmt" "strings" "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/logs" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" ) // Index validates that idx does not violate any invariants of the index format. func Index(idx v1.ImageIndex, opt ...Option) error { errs := []string{} if err := validateChildren(idx, opt...); err != nil { errs = append(errs, fmt.Sprintf("validating children: %v", err)) } if err := validateIndexManifest(idx); err != nil { errs = append(errs, fmt.Sprintf("validating index manifest: %v", err)) } if len(errs) != 0 { return errors.New(strings.Join(errs, "\n\n")) } return nil } type withLayer interface { Layer(v1.Hash) (v1.Layer, error) } func validateChildren(idx v1.ImageIndex, opt ...Option) error { manifest, err := idx.IndexManifest() if err != nil { return err } errs := []string{} for i, desc := range manifest.Manifests { switch desc.MediaType { case types.OCIImageIndex, types.DockerManifestList: idx, err := idx.ImageIndex(desc.Digest) if err != nil { return err } if err := Index(idx, opt...); err != nil { errs = append(errs, fmt.Sprintf("failed to validate index Manifests[%d](%s): %v", i, desc.Digest, err)) } if err := validateMediaType(idx, desc.MediaType); err != nil { errs = append(errs, fmt.Sprintf("failed to validate index MediaType[%d](%s): %v", i, desc.Digest, err)) } case types.OCIManifestSchema1, types.DockerManifestSchema2: img, err := idx.Image(desc.Digest) if err != nil { return err } if err := Image(img, opt...); err != nil { errs = append(errs, fmt.Sprintf("failed to validate image Manifests[%d](%s): %v", i, desc.Digest, err)) } if err := validateMediaType(img, desc.MediaType); err != nil { errs = append(errs, fmt.Sprintf("failed to validate image MediaType[%d](%s): %v", i, desc.Digest, err)) } if err := validatePlatform(img, desc.Platform); err != nil { errs = append(errs, fmt.Sprintf("failed to validate image platform[%d](%s): %v", i, desc.Digest, err)) } default: // Workaround for #819. if wl, ok := idx.(withLayer); ok { layer, err := wl.Layer(desc.Digest) if err != nil { return fmt.Errorf("failed to get layer Manifests[%d]: %w", i, err) } if err := Layer(layer, opt...); err != nil { lerr := fmt.Sprintf("failed to validate layer Manifests[%d](%s): %v", i, desc.Digest, err) if desc.MediaType.IsDistributable() { errs = append(errs, lerr) } else { logs.Warn.Printf("nondistributable layer failure: %v", lerr) } } } else { logs.Warn.Printf("Unexpected manifest: %s", desc.MediaType) } } } if len(errs) != 0 { return errors.New(strings.Join(errs, "\n")) } return nil } type withMediaType interface { MediaType() (types.MediaType, error) } func validateMediaType(i withMediaType, want types.MediaType) error { got, err := i.MediaType() if err != nil { return err } if want != got { return fmt.Errorf("mismatched mediaType: MediaType() = %v != %v", got, want) } return nil } func validateIndexManifest(idx v1.ImageIndex) error { digest, err := idx.Digest() if err != nil { return err } size, err := idx.Size() if err != nil { return err } rm, err := idx.RawManifest() if err != nil { return err } hash, _, err := v1.SHA256(bytes.NewReader(rm)) if err != nil { return err } m, err := idx.IndexManifest() if err != nil { return err } pm, err := v1.ParseIndexManifest(bytes.NewReader(rm)) if err != nil { return err } errs := []string{} if digest != hash { errs = append(errs, fmt.Sprintf("mismatched manifest digest: Digest()=%s, SHA256(RawManifest())=%s", digest, hash)) } if diff := cmp.Diff(pm, m); diff != "" { errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseIndexManifest(RawManifest()) +Manifest()) %s", diff)) } if size != int64(len(rm)) { errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm))) } if len(errs) != 0 { return errors.New(strings.Join(errs, "\n")) } return nil } func validatePlatform(img v1.Image, want *v1.Platform) error { if want == nil { return nil } cf, err := img.ConfigFile() if err != nil { return err } got := cf.Platform() if got == nil { return fmt.Errorf("config file missing platform fields") } if got.Equals(*want) { return nil } errs := []string{} if got.OS != want.OS { errs = append(errs, fmt.Sprintf("mismatched OS: %s != %s", got.OS, want.OS)) } if got.Architecture != want.Architecture { errs = append(errs, fmt.Sprintf("mismatched Architecture: %s != %s", got.Architecture, want.Architecture)) } if got.OSVersion != want.OSVersion { errs = append(errs, fmt.Sprintf("mismatched OSVersion: %s != %s", got.OSVersion, want.OSVersion)) } if got.OSVersion != want.OSVersion { errs = append(errs, fmt.Sprintf("mismatched OSVersion: %s != %s", got.OSVersion, want.OSVersion)) } if len(errs) == 0 { // If we got here, some features might be mismatched. Just add those... if len(got.Features) != 0 || len(want.Features) != 0 { errs = append(errs, fmt.Sprintf("mismatched Features: %v, %v", got.Features, want.Features)) } if len(got.OSFeatures) != 0 || len(want.OSFeatures) != 0 { errs = append(errs, fmt.Sprintf("mismatched OSFeatures: %v, %v", got.OSFeatures, want.OSFeatures)) } } return errors.New(strings.Join(errs, "\n")) }