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 }