github.com/jdolitsky/cnab-go@v0.7.1-beta1/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  func (i *BaseImage) DeepCopy() *BaseImage {
    91  	i2 := *i
    92  	i2.Labels = make(map[string]string, len(i.Labels))
    93  	for key, value := range i.Labels {
    94  		i2.Labels[key] = value
    95  	}
    96  	return &i2
    97  }
    98  
    99  // Image describes a container image in the bundle
   100  type Image struct {
   101  	BaseImage   `yaml:",inline"`
   102  	Description string `json:"description" yaml:"description"` //TODO: change? see where it's being used? change to description?
   103  }
   104  
   105  func (i *Image) DeepCopy() *Image {
   106  	i2 := *i
   107  	i2.BaseImage = *i.BaseImage.DeepCopy()
   108  	return &i2
   109  }
   110  
   111  // InvocationImage contains the image type and location for the installation of a bundle
   112  type InvocationImage struct {
   113  	BaseImage `yaml:",inline"`
   114  }
   115  
   116  func (img *InvocationImage) DeepCopy() *InvocationImage {
   117  	img2 := *img
   118  	img2.BaseImage = *img.BaseImage.DeepCopy()
   119  	return &img2
   120  }
   121  
   122  // Location provides the location where a value should be written in
   123  // the invocation image.
   124  //
   125  // A location may be either a file (by path) or an environment variable.
   126  type Location struct {
   127  	Path                string `json:"path,omitempty" yaml:"path,omitempty"`
   128  	EnvironmentVariable string `json:"env,omitempty" yaml:"env,omitempty"`
   129  }
   130  
   131  // Maintainer describes a code maintainer of a bundle
   132  type Maintainer struct {
   133  	// Name is a user name or organization name
   134  	Name string `json:"name" yaml:"name"`
   135  	// Email is an optional email address to contact the named maintainer
   136  	Email string `json:"email,omitempty" yaml:"email,omitempty"`
   137  	// Url is an optional URL to an address for the named maintainer
   138  	URL string `json:"url,omitempty" yaml:"url,omitempty"`
   139  }
   140  
   141  // Action describes a custom (non-core) action.
   142  type Action struct {
   143  	// Modifies indicates whether this action modifies the release.
   144  	//
   145  	// If it is possible that an action modify a release, this must be set to true.
   146  	Modifies bool `json:"modifies,omitempty" yaml:"modifies,omitempty"`
   147  	// Stateless indicates that the action is purely informational, that credentials are not required, and that the runtime should not keep track of its invocation
   148  	Stateless bool `json:"stateless,omitempty" yaml:"stateless,omitempty"`
   149  	// Description describes the action as a user-readable string
   150  	Description string `json:"description,omitempty" yaml:"description,omitempty"`
   151  }
   152  
   153  // ValuesOrDefaults returns parameter values or the default parameter values. An error is returned when the parameter value does not pass
   154  // the schema validation or a required parameter is missing.
   155  func ValuesOrDefaults(vals map[string]interface{}, b *Bundle) (map[string]interface{}, error) {
   156  	res := map[string]interface{}{}
   157  
   158  	for name, param := range b.Parameters {
   159  		s, ok := b.Definitions[param.Definition]
   160  		if !ok {
   161  			return res, fmt.Errorf("unable to find definition for %s", name)
   162  		}
   163  		if val, ok := vals[name]; ok {
   164  			valErrs, err := s.Validate(val)
   165  			if err != nil {
   166  				return res, pkgErrors.Wrapf(err, "encountered an error validating parameter %s", name)
   167  			}
   168  			// This interface returns a single error. Validation can have multiple errors. For now return the first
   169  			// We should update this later.
   170  			if len(valErrs) > 0 {
   171  				valErr := valErrs[0]
   172  				return res, fmt.Errorf("cannot use value: %v as parameter %s: %s ", val, name, valErr.Error)
   173  			}
   174  			typedVal := s.CoerceValue(val)
   175  			res[name] = typedVal
   176  			continue
   177  		} else if param.Required {
   178  			return res, fmt.Errorf("parameter %q is required", name)
   179  		}
   180  		res[name] = s.Default
   181  	}
   182  	return res, nil
   183  }
   184  
   185  // Validate the bundle contents.
   186  func (b Bundle) Validate() error {
   187  	_, err := semver.NewVersion(b.SchemaVersion)
   188  	if err != nil {
   189  		return fmt.Errorf("invalid bundle schema version %q: %v", b.SchemaVersion, err)
   190  	}
   191  
   192  	if len(b.InvocationImages) == 0 {
   193  		return errors.New("at least one invocation image must be defined in the bundle")
   194  	}
   195  
   196  	if b.Version == "latest" {
   197  		return errors.New("'latest' is not a valid bundle version")
   198  	}
   199  
   200  	reqExt := make(map[string]bool, len(b.RequiredExtensions))
   201  	for _, requiredExtension := range b.RequiredExtensions {
   202  		// Verify the custom extension declared as required exists
   203  		if _, exists := b.Custom[requiredExtension]; !exists {
   204  			return fmt.Errorf("required extension '%s' is not defined in the Custom section of the bundle", requiredExtension)
   205  		}
   206  
   207  		// Check for duplicate entries
   208  		if _, exists := reqExt[requiredExtension]; exists {
   209  			return fmt.Errorf("required extension '%s' is already declared", requiredExtension)
   210  		}
   211  
   212  		// Populate map with required extension, for duplicate check above
   213  		reqExt[requiredExtension] = true
   214  	}
   215  
   216  	for _, img := range b.InvocationImages {
   217  		err := img.Validate()
   218  		if err != nil {
   219  			return err
   220  		}
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  // Validate the image contents.
   227  func (img InvocationImage) Validate() error {
   228  	switch img.ImageType {
   229  	case "docker", "oci":
   230  		return validateDockerish(img.Image)
   231  	default:
   232  		return nil
   233  	}
   234  }
   235  
   236  func validateDockerish(s string) error {
   237  	if !strings.Contains(s, ":") {
   238  		return errors.New("tag is required")
   239  	}
   240  	return nil
   241  }