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 }