github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/cnbutils/bindings/bindings.go (about) 1 // Package bindings provides utility function to create buildpack bindings folder structures 2 package bindings 3 4 import ( 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/pkg/errors" 14 k8sjson "sigs.k8s.io/json" 15 16 "github.com/SAP/jenkins-library/pkg/cnbutils" 17 "github.com/SAP/jenkins-library/pkg/config" 18 piperhttp "github.com/SAP/jenkins-library/pkg/http" 19 ) 20 21 type binding struct { 22 bindingData `json:",inline"` 23 Type string `json:"type"` 24 Data []bindingData `json:"data"` 25 } 26 27 type bindingData struct { 28 Key string `json:"key"` 29 Content *string `json:"content,omitempty"` 30 File *string `json:"file,omitempty"` 31 FromURL *string `json:"fromUrl,omitempty"` 32 VaultCredentialKey *string `json:"vaultCredentialKey,omitempty"` 33 } 34 35 type bindings map[string]binding 36 37 type bindingContentType int 38 39 const ( 40 fileBinding bindingContentType = iota 41 contentBinding 42 fromURLBinding 43 vaultBinding 44 ) 45 46 // Return error if: 47 // 1. Content is set + File or FromURL or VaultCredentialKey 48 // 2. File is set + FromURL or Content or VaultCredentialKey 49 // 3. FromURL is set + File or Content or VaultCredentialKey 50 // 4. VaultCredentialKey is set + File or FromURL or Content 51 // 5. Everything is set 52 func (b *bindingData) validate() error { 53 if !validName(b.Key) { 54 return fmt.Errorf("invalid key: '%s'", b.Key) 55 } 56 57 if b.Content == nil && b.File == nil && b.FromURL == nil && b.VaultCredentialKey == nil { 58 return errors.New("one of 'file', 'content', 'fromUrl' or 'vaultCredentialKey' properties must be specified") 59 } 60 61 onlyOneSet := (b.Content != nil && b.File == nil && b.FromURL == nil && b.VaultCredentialKey == nil) || 62 (b.Content == nil && b.File != nil && b.FromURL == nil && b.VaultCredentialKey == nil) || 63 (b.Content == nil && b.File == nil && b.FromURL != nil && b.VaultCredentialKey == nil) || 64 (b.Content == nil && b.File == nil && b.FromURL == nil && b.VaultCredentialKey != nil) 65 66 if !onlyOneSet { 67 return errors.New("only one of 'content', 'file', 'fromUrl' or 'vaultCredentialKey' can be set") 68 } 69 70 return nil 71 } 72 73 func (b *bindingData) bindingContentType() bindingContentType { 74 if b.File != nil { 75 return fileBinding 76 } 77 78 if b.Content != nil { 79 return contentBinding 80 } 81 82 if b.FromURL != nil { 83 return fromURLBinding 84 } 85 86 return vaultBinding 87 } 88 89 // ProcessBindings creates the given bindings in the platform directory 90 func ProcessBindings(utils cnbutils.BuildUtils, httpClient piperhttp.Sender, platformPath string, bindings map[string]interface{}) error { 91 typedBindings, err := toTyped(bindings) 92 if err != nil { 93 return errors.Wrap(err, "error while reading bindings") 94 } 95 96 for name, binding := range typedBindings { 97 if len(binding.Data) == 0 { 98 return fmt.Errorf("empty binding: '%s'", name) 99 } 100 for _, data := range binding.Data { 101 err = processBinding(utils, httpClient, platformPath, name, binding.Type, data) 102 if err != nil { 103 return err 104 } 105 } 106 } 107 108 return nil 109 } 110 111 func processBinding(utils cnbutils.BuildUtils, httpClient piperhttp.Sender, platformPath string, name string, bindingType string, data bindingData) error { 112 err := validateBinding(name, data) 113 if err != nil { 114 return err 115 } 116 117 bindingDir := filepath.Join(platformPath, "bindings", name) 118 err = utils.MkdirAll(bindingDir, 0755) 119 if err != nil { 120 return errors.Wrap(err, "failed to create binding directory") 121 } 122 123 err = utils.FileWrite(filepath.Join(bindingDir, "type"), []byte(bindingType), 0644) 124 if err != nil { 125 return errors.Wrap(err, "failed to write the 'type' binding file") 126 } 127 128 var bindingContent []byte 129 130 switch data.bindingContentType() { 131 case fileBinding: 132 bindingContent, err = utils.FileRead(*data.File) 133 if err != nil { 134 return errors.Wrap(err, "failed to copy binding file") 135 } 136 case contentBinding: 137 bindingContent = []byte(*data.Content) 138 case fromURLBinding: 139 response, err := httpClient.SendRequest(http.MethodGet, *data.FromURL, nil, nil, nil) 140 if err != nil { 141 return errors.Wrap(err, "failed to load binding from url") 142 } 143 144 bindingContent, err = io.ReadAll(response.Body) 145 defer response.Body.Close() 146 if err != nil { 147 return errors.Wrap(err, "error reading response") 148 } 149 case vaultBinding: 150 envVar := config.VaultCredentialEnvPrefixDefault + config.ConvertEnvVar(*data.VaultCredentialKey) 151 if bindingContentString, ok := os.LookupEnv(envVar); ok { 152 bindingContent = []byte(bindingContentString) 153 } else { 154 return fmt.Errorf("environment variable %q is not set (required by the %q binding)", envVar, name) 155 } 156 } 157 158 err = utils.FileWrite(filepath.Join(bindingDir, data.Key), bindingContent, 0644) 159 if err != nil { 160 return errors.Wrap(err, "failed to write binding") 161 } 162 163 return nil 164 } 165 166 func validateBinding(name string, data bindingData) error { 167 if !validName(name) { 168 return fmt.Errorf("invalid binding name: '%s'", name) 169 } 170 171 err := data.validate() 172 if err != nil { 173 return errors.Wrapf(err, "failed to validate binding '%s'", name) 174 } 175 return nil 176 } 177 178 func toTyped(rawMap map[string]interface{}) (bindings, error) { 179 typedBindings := bindings{} 180 181 for name, rawBinding := range rawMap { 182 var b binding 183 184 b, err := fromRaw(rawBinding) 185 if err != nil { 186 return nil, errors.Wrapf(err, "could not process binding '%s'", name) 187 } 188 189 if b.Key != "" { 190 b.Data = append(b.Data, bindingData{ 191 Key: b.Key, 192 Content: b.Content, 193 File: b.File, 194 FromURL: b.FromURL, 195 VaultCredentialKey: b.VaultCredentialKey, 196 }) 197 } 198 199 typedBindings[name] = b 200 } 201 202 return typedBindings, nil 203 } 204 205 func fromRaw(rawData interface{}) (binding, error) { 206 var new binding 207 208 jsonValue, err := json.Marshal(rawData) 209 if err != nil { 210 return binding{}, err 211 } 212 213 errs, err := k8sjson.UnmarshalStrict(jsonValue, &new, k8sjson.DisallowUnknownFields) 214 if err != nil { 215 return binding{}, err 216 } 217 218 if len(errs) != 0 { 219 for _, e := range errs { 220 if err == nil { 221 err = e 222 } else { 223 err = errors.Wrap(err, e.Error()) 224 } 225 } 226 err = errors.Wrap(err, "validation error") 227 return binding{}, err 228 } 229 230 return new, nil 231 } 232 233 func validName(name string) bool { 234 if name == "" || name == "." || name == ".." { 235 return false 236 } 237 238 return !strings.ContainsAny(name, "/") 239 }