...

Source file src/github.com/opencontainers/image-spec/schema/validator.go

Documentation: github.com/opencontainers/image-spec/schema

     1  // Copyright 2016 The Linux Foundation
     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 schema
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"regexp"
    24  
    25  	digest "github.com/opencontainers/go-digest"
    26  	v1 "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"github.com/xeipuuv/gojsonschema"
    28  )
    29  
    30  // Validator wraps a media type string identifier
    31  // and implements validation against a JSON schema.
    32  type Validator string
    33  
    34  type validateFunc func(r io.Reader) error
    35  
    36  var mapValidate = map[Validator]validateFunc{
    37  	ValidatorMediaTypeImageConfig: validateConfig,
    38  	ValidatorMediaTypeDescriptor:  validateDescriptor,
    39  	ValidatorMediaTypeImageIndex:  validateIndex,
    40  	ValidatorMediaTypeManifest:    validateManifest,
    41  }
    42  
    43  // ValidationError contains all the errors that happened during validation.
    44  type ValidationError struct {
    45  	Errs []error
    46  }
    47  
    48  func (e ValidationError) Error() string {
    49  	return fmt.Sprintf("%v", e.Errs)
    50  }
    51  
    52  // Validate validates the given reader against the schema of the wrapped media type.
    53  func (v Validator) Validate(src io.Reader) error {
    54  	buf, err := io.ReadAll(src)
    55  	if err != nil {
    56  		return fmt.Errorf("unable to read the document file: %w", err)
    57  	}
    58  
    59  	if f, ok := mapValidate[v]; ok {
    60  		if f == nil {
    61  			return fmt.Errorf("internal error: mapValidate[%q] is nil", v)
    62  		}
    63  		err = f(bytes.NewReader(buf))
    64  		if err != nil {
    65  			return err
    66  		}
    67  	}
    68  
    69  	sl := newFSLoaderFactory(schemaNamespaces, FileSystem()).New(specs[v])
    70  	ml := gojsonschema.NewStringLoader(string(buf))
    71  
    72  	result, err := gojsonschema.Validate(sl, ml)
    73  	if err != nil {
    74  		return fmt.Errorf("schema %s: unable to validate: %w", v,
    75  			WrapSyntaxError(bytes.NewReader(buf), err))
    76  	}
    77  
    78  	if result.Valid() {
    79  		return nil
    80  	}
    81  
    82  	errs := make([]error, 0, len(result.Errors()))
    83  	for _, desc := range result.Errors() {
    84  		errs = append(errs, fmt.Errorf("%s", desc))
    85  	}
    86  
    87  	return ValidationError{
    88  		Errs: errs,
    89  	}
    90  }
    91  
    92  type unimplemented string
    93  
    94  func (v unimplemented) Validate(_ io.Reader) error {
    95  	return fmt.Errorf("%s: unimplemented", v)
    96  }
    97  
    98  func validateManifest(r io.Reader) error {
    99  	header := v1.Manifest{}
   100  
   101  	buf, err := io.ReadAll(r)
   102  	if err != nil {
   103  		return fmt.Errorf("error reading the io stream: %w", err)
   104  	}
   105  
   106  	err = json.Unmarshal(buf, &header)
   107  	if err != nil {
   108  		return fmt.Errorf("manifest format mismatch: %w", err)
   109  	}
   110  
   111  	if header.Config.MediaType != string(v1.MediaTypeImageConfig) {
   112  		fmt.Printf("warning: config %s has an unknown media type: %s\n", header.Config.Digest, header.Config.MediaType)
   113  	}
   114  
   115  	for _, layer := range header.Layers {
   116  		if layer.MediaType != string(v1.MediaTypeImageLayer) &&
   117  			layer.MediaType != string(v1.MediaTypeImageLayerGzip) &&
   118  			layer.MediaType != string(v1.MediaTypeImageLayerZstd) &&
   119  			layer.MediaType != string(v1.MediaTypeImageLayerNonDistributable) && //nolint:staticcheck
   120  			layer.MediaType != string(v1.MediaTypeImageLayerNonDistributableGzip) && //nolint:staticcheck
   121  			layer.MediaType != string(v1.MediaTypeImageLayerNonDistributableZstd) { //nolint:staticcheck
   122  			fmt.Printf("warning: layer %s has an unknown media type: %s\n", layer.Digest, layer.MediaType)
   123  		}
   124  	}
   125  	return nil
   126  }
   127  
   128  func validateDescriptor(r io.Reader) error {
   129  	header := v1.Descriptor{}
   130  
   131  	buf, err := io.ReadAll(r)
   132  	if err != nil {
   133  		return fmt.Errorf("error reading the io stream: %w", err)
   134  	}
   135  
   136  	err = json.Unmarshal(buf, &header)
   137  	if err != nil {
   138  		return fmt.Errorf("descriptor format mismatch: %w", err)
   139  	}
   140  
   141  	err = header.Digest.Validate()
   142  	if errors.Is(err, digest.ErrDigestUnsupported) {
   143  		// we ignore unsupported algorithms
   144  		fmt.Printf("warning: unsupported digest: %q: %v\n", header.Digest, err)
   145  		return nil
   146  	}
   147  	return err
   148  }
   149  
   150  func validateIndex(r io.Reader) error {
   151  	header := v1.Index{}
   152  
   153  	buf, err := io.ReadAll(r)
   154  	if err != nil {
   155  		return fmt.Errorf("error reading the io stream: %w", err)
   156  	}
   157  
   158  	err = json.Unmarshal(buf, &header)
   159  	if err != nil {
   160  		return fmt.Errorf("index format mismatch: %w", err)
   161  	}
   162  
   163  	for _, manifest := range header.Manifests {
   164  		if manifest.MediaType != string(v1.MediaTypeImageManifest) {
   165  			fmt.Printf("warning: manifest %s has an unknown media type: %s\n", manifest.Digest, manifest.MediaType)
   166  		}
   167  		if manifest.Platform != nil {
   168  			checkPlatform(manifest.Platform.OS, manifest.Platform.Architecture)
   169  			checkArchitecture(manifest.Platform.Architecture, manifest.Platform.Variant)
   170  		}
   171  
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func validateConfig(r io.Reader) error {
   178  	header := v1.Image{}
   179  
   180  	buf, err := io.ReadAll(r)
   181  	if err != nil {
   182  		return fmt.Errorf("error reading the io stream: %w", err)
   183  	}
   184  
   185  	err = json.Unmarshal(buf, &header)
   186  	if err != nil {
   187  		return fmt.Errorf("config format mismatch: %w", err)
   188  	}
   189  
   190  	checkPlatform(header.OS, header.Architecture)
   191  	checkArchitecture(header.Architecture, header.Variant)
   192  
   193  	envRegexp := regexp.MustCompile(`^[^=]+=.*$`)
   194  	for _, e := range header.Config.Env {
   195  		if !envRegexp.MatchString(e) {
   196  			return fmt.Errorf("unexpected env: %q", e)
   197  		}
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  func checkArchitecture(Architecture string, Variant string) {
   204  	validCombins := map[string][]string{
   205  		"arm":      {"", "v6", "v7", "v8"},
   206  		"arm64":    {"", "v8"},
   207  		"386":      {""},
   208  		"amd64":    {""},
   209  		"ppc64":    {""},
   210  		"ppc64le":  {""},
   211  		"mips64":   {""},
   212  		"mips64le": {""},
   213  		"s390x":    {""},
   214  		"riscv64":  {""},
   215  	}
   216  	for arch, variants := range validCombins {
   217  		if arch == Architecture {
   218  			for _, variant := range variants {
   219  				if variant == Variant {
   220  					return
   221  				}
   222  			}
   223  			fmt.Printf("warning: combination of architecture %q and variant %q is not valid.\n", Architecture, Variant)
   224  		}
   225  	}
   226  	fmt.Printf("warning: architecture %q is not supported yet.\n", Architecture)
   227  }
   228  
   229  func checkPlatform(OS string, Architecture string) {
   230  	validCombins := map[string][]string{
   231  		"android":   {"arm"},
   232  		"darwin":    {"386", "amd64", "arm", "arm64"},
   233  		"dragonfly": {"amd64"},
   234  		"freebsd":   {"386", "amd64", "arm"},
   235  		"linux":     {"386", "amd64", "arm", "arm64", "ppc64", "ppc64le", "mips64", "mips64le", "s390x", "riscv64"},
   236  		"netbsd":    {"386", "amd64", "arm"},
   237  		"openbsd":   {"386", "amd64", "arm"},
   238  		"plan9":     {"386", "amd64"},
   239  		"solaris":   {"amd64"},
   240  		"windows":   {"386", "amd64"}}
   241  	for os, archs := range validCombins {
   242  		if os == OS {
   243  			for _, arch := range archs {
   244  				if arch == Architecture {
   245  					return
   246  				}
   247  			}
   248  			fmt.Printf("warning: combination of os %q and architecture %q is invalid.\n", OS, Architecture)
   249  		}
   250  	}
   251  	fmt.Printf("warning: operating system %q of the bundle is not supported yet.\n", OS)
   252  }
   253  

View as plain text