github.com/Datadog/cnab-go@v0.3.3-beta1.0.20191007143216-bba4b7e723d0/bundle/bundle.go (about)

     1  package bundle
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/Masterminds/semver"
    12  	"github.com/deislabs/cnab-go/bundle/definition"
    13  	"github.com/docker/go/canonical/json"
    14  	pkgErrors "github.com/pkg/errors"
    15  )
    16  
    17  // Bundle is a CNAB metadata document
    18  type Bundle struct {
    19  	SchemaVersion      string                 `json:"schemaVersion" yaml:"schemaVersion"`
    20  	Name               string                 `json:"name" yaml:"name"`
    21  	Version            string                 `json:"version" yaml:"version"`
    22  	Description        string                 `json:"description" yaml:"description"`
    23  	Keywords           []string               `json:"keywords,omitempty" yaml:"keywords,omitempty"`
    24  	Maintainers        []Maintainer           `json:"maintainers,omitempty" yaml:"maintainers,omitempty"`
    25  	InvocationImages   []InvocationImage      `json:"invocationImages" yaml:"invocationImages"`
    26  	Images             map[string]Image       `json:"images,omitempty" yaml:"images,omitempty"`
    27  	Actions            map[string]Action      `json:"actions,omitempty" yaml:"actions,omitempty"`
    28  	Parameters         map[string]Parameter   `json:"parameters,omitempty" yaml:"parameters,omitempty"`
    29  	Credentials        map[string]Credential  `json:"credentials,omitempty" yaml:"credentials,omitempty"`
    30  	Outputs            map[string]Output      `json:"outputs,omitempty" yaml:"outputs,omitempty"`
    31  	Definitions        definition.Definitions `json:"definitions,omitempty" yaml:"definitions,omitempty"`
    32  	License            string                 `json:"license,omitempty" yaml:"license,omitempty"`
    33  	RequiredExtensions []string               `json:"requiredExtensions,omitempty" yaml:"requiredExtensions,omitempty"`
    34  
    35  	// Custom extension metadata is a named collection of auxiliary data whose
    36  	// meaning is defined outside of the CNAB specification.
    37  	Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
    38  }
    39  
    40  //Unmarshal unmarshals a Bundle that was not signed.
    41  func Unmarshal(data []byte) (*Bundle, error) {
    42  	b := &Bundle{}
    43  	return b, json.Unmarshal(data, b)
    44  }
    45  
    46  // ParseReader reads CNAB metadata from a JSON string
    47  func ParseReader(r io.Reader) (Bundle, error) {
    48  	b := Bundle{}
    49  	err := json.NewDecoder(r).Decode(&b)
    50  	return b, err
    51  }
    52  
    53  // WriteFile serializes the bundle and writes it to a file as JSON.
    54  func (b Bundle) WriteFile(dest string, mode os.FileMode) error {
    55  	// FIXME: The marshal here should exactly match the Marshal in the signature code.
    56  	d, err := json.MarshalCanonical(b)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	return ioutil.WriteFile(dest, d, mode)
    61  }
    62  
    63  // WriteTo writes unsigned JSON to an io.Writer using the standard formatting.
    64  func (b Bundle) WriteTo(w io.Writer) (int64, error) {
    65  	d, err := json.MarshalCanonical(b)
    66  	if err != nil {
    67  		return 0, err
    68  	}
    69  	l, err := w.Write(d)
    70  	return int64(l), err
    71  }
    72  
    73  // LocationRef specifies a location within the invocation package
    74  type LocationRef struct {
    75  	Path      string `json:"path" yaml:"path"`
    76  	Field     string `json:"field" yaml:"field"`
    77  	MediaType string `json:"mediaType" yaml:"mediaType"`
    78  }
    79  
    80  // BaseImage contains fields shared across image types
    81  type BaseImage struct {
    82  	ImageType string            `json:"imageType" yaml:"imageType"`
    83  	Image     string            `json:"image" yaml:"image"`
    84  	Digest    string            `json:"contentDigest,omitempty" yaml:"contentDigest,omitempty"`
    85  	Size      uint64            `json:"size,omitempty" yaml:"size,omitempty"`
    86  	Labels    map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
    87  	MediaType string            `json:"mediaType,omitempty" yaml:"mediaType,omitempty"`
    88  }
    89  
    90  // Image describes a container image in the bundle
    91  type Image struct {
    92  	BaseImage   `yaml:",inline"`
    93  	Description string `json:"description" yaml:"description"` //TODO: change? see where it's being used? change to description?
    94  }
    95  
    96  // InvocationImage contains the image type and location for the installation of a bundle
    97  type InvocationImage struct {
    98  	BaseImage `yaml:",inline"`
    99  }
   100  
   101  // Location provides the location where a value should be written in
   102  // the invocation image.
   103  //
   104  // A location may be either a file (by path) or an environment variable.
   105  type Location struct {
   106  	Path                string `json:"path,omitempty" yaml:"path,omitempty"`
   107  	EnvironmentVariable string `json:"env,omitempty" yaml:"env,omitempty"`
   108  }
   109  
   110  // Maintainer describes a code maintainer of a bundle
   111  type Maintainer struct {
   112  	// Name is a user name or organization name
   113  	Name string `json:"name" yaml:"name"`
   114  	// Email is an optional email address to contact the named maintainer
   115  	Email string `json:"email,omitempty" yaml:"email,omitempty"`
   116  	// Url is an optional URL to an address for the named maintainer
   117  	URL string `json:"url,omitempty" yaml:"url,omitempty"`
   118  }
   119  
   120  // Action describes a custom (non-core) action.
   121  type Action struct {
   122  	// Modifies indicates whether this action modifies the release.
   123  	//
   124  	// If it is possible that an action modify a release, this must be set to true.
   125  	Modifies bool `json:"modifies,omitempty" yaml:"modifies,omitempty"`
   126  	// Stateless indicates that the action is purely informational, that credentials are not required, and that the runtime should not keep track of its invocation
   127  	Stateless bool `json:"stateless,omitempty" yaml:"stateless,omitempty"`
   128  	// Description describes the action as a user-readable string
   129  	Description string `json:"description,omitempty" yaml:"description,omitempty"`
   130  }
   131  
   132  // ValuesOrDefaults returns parameter values or the default parameter values. An error is returned when the parameter value does not pass
   133  // the schema validation or a required parameter is missing.
   134  func ValuesOrDefaults(vals map[string]interface{}, b *Bundle) (map[string]interface{}, error) {
   135  	res := map[string]interface{}{}
   136  
   137  	for name, param := range b.Parameters {
   138  		s, ok := b.Definitions[param.Definition]
   139  		if !ok {
   140  			return res, fmt.Errorf("unable to find definition for %s", name)
   141  		}
   142  		if val, ok := vals[name]; ok {
   143  			valErrs, err := s.Validate(val)
   144  			if err != nil {
   145  				return res, pkgErrors.Wrapf(err, "encountered an error validating parameter %s", name)
   146  			}
   147  			// This interface returns a single error. Validation can have multiple errors. For now return the first
   148  			// We should update this later.
   149  			if len(valErrs) > 0 {
   150  				valErr := valErrs[0]
   151  				return res, fmt.Errorf("cannot use value: %v as parameter %s: %s ", val, name, valErr.Error)
   152  			}
   153  			typedVal := s.CoerceValue(val)
   154  			res[name] = typedVal
   155  			continue
   156  		} else if param.Required {
   157  			return res, fmt.Errorf("parameter %q is required", name)
   158  		}
   159  		res[name] = s.Default
   160  	}
   161  	return res, nil
   162  }
   163  
   164  // Validate the bundle contents.
   165  func (b Bundle) Validate() error {
   166  	_, err := semver.NewVersion(b.SchemaVersion)
   167  	if err != nil {
   168  		return fmt.Errorf("invalid bundle schema version %q: %v", b.SchemaVersion, err)
   169  	}
   170  
   171  	if len(b.InvocationImages) == 0 {
   172  		return errors.New("at least one invocation image must be defined in the bundle")
   173  	}
   174  
   175  	if b.Version == "latest" {
   176  		return errors.New("'latest' is not a valid bundle version")
   177  	}
   178  
   179  	reqExt := make(map[string]bool, len(b.RequiredExtensions))
   180  	for _, requiredExtension := range b.RequiredExtensions {
   181  		// Verify the custom extension declared as required exists
   182  		if _, exists := b.Custom[requiredExtension]; !exists {
   183  			return fmt.Errorf("required extension '%s' is not defined in the Custom section of the bundle", requiredExtension)
   184  		}
   185  
   186  		// Check for duplicate entries
   187  		if _, exists := reqExt[requiredExtension]; exists {
   188  			return fmt.Errorf("required extension '%s' is already declared", requiredExtension)
   189  		}
   190  
   191  		// Populate map with required extension, for duplicate check above
   192  		reqExt[requiredExtension] = true
   193  	}
   194  
   195  	for _, img := range b.InvocationImages {
   196  		err := img.Validate()
   197  		if err != nil {
   198  			return err
   199  		}
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  // Validate the image contents.
   206  func (img InvocationImage) Validate() error {
   207  	switch img.ImageType {
   208  	case "docker", "oci":
   209  		return validateDockerish(img.Image)
   210  	default:
   211  		return nil
   212  	}
   213  }
   214  
   215  func validateDockerish(s string) error {
   216  	if !strings.Contains(s, ":") {
   217  		return errors.New("tag is required")
   218  	}
   219  	return nil
   220  }