github.com/paketo-buildpacks/packit@v1.3.2-0.20211206231111-86b75c657449/servicebindings/resolver.go (about) 1 package servicebindings 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 ) 10 11 // Binding represents metadata related to an external service. 12 type Binding struct { 13 14 // Name is the name of the binding. 15 Name string 16 17 // Path is the path to the binding directory. 18 Path string 19 20 // Type is the type of the binding. 21 Type string 22 23 // Provider is the provider of the binding. 24 Provider string 25 26 // Entries is the set of entries that make up the binding. 27 Entries map[string]*Entry 28 } 29 30 // Resolver resolves service bindings according to the kubernetes binding spec: 31 // https://github.com/k8s-service-bindings/spec#workload-projection. 32 // 33 // It also supports backwards compatibility with the legacy service binding spec: 34 // https://github.com/buildpacks/spec/blob/main/extensions/bindings.md 35 type Resolver struct { 36 bindingRoot string 37 bindings []Binding 38 } 39 40 // NewResolver returns a new service binding resolver. 41 func NewResolver() *Resolver { 42 return &Resolver{} 43 } 44 45 // Resolve returns all bindings matching the given type and optional provider (case-insensitive). To match on type only, 46 // provider may be an empty string. Returns an error if there are problems loading bindings from the file system. 47 // 48 // The location of bindings is given by one of the following, in order of precedence: 49 // 50 // 1. SERVICE_BINDING_ROOT environment variable 51 // 2. CNB_BINDINGS environment variable, if above is not set 52 // 3. `<platformDir>/bindings`, if both above are not set 53 func (r *Resolver) Resolve(typ, provider, platformDir string) ([]Binding, error) { 54 if newRoot := bindingRoot(platformDir); r.bindingRoot != newRoot { 55 r.bindingRoot = newRoot 56 bindings, err := loadBindings(r.bindingRoot) 57 if err != nil { 58 return nil, fmt.Errorf("failed to load bindings from '%s': %w", r.bindingRoot, err) 59 } 60 r.bindings = bindings 61 } 62 63 var resolved []Binding 64 for _, binding := range r.bindings { 65 if (strings.EqualFold(binding.Type, typ)) && 66 (provider == "" || strings.EqualFold(binding.Provider, provider)) { 67 resolved = append(resolved, binding) 68 } 69 } 70 return resolved, nil 71 } 72 73 // ResolveOne returns a single binding matching the given type and optional provider (case-insensitive). To match on 74 // type only, provider may be an empty string. Returns an error if the number of matched bindings is not exactly one, or 75 // if there are problems loading bindings from the file system. 76 // 77 // The location of bindings is given by one of the following, in order of precedence: 78 // 79 // 1. SERVICE_BINDING_ROOT environment variable 80 // 2. CNB_BINDINGS environment variable, if above is not set 81 // 3. `<platformDir>/bindings`, if both above are not set 82 83 func (r *Resolver) ResolveOne(typ, provider, platformDir string) (Binding, error) { 84 bindings, err := r.Resolve(typ, provider, platformDir) 85 if err != nil { 86 return Binding{}, err 87 } 88 if len(bindings) != 1 { 89 return Binding{}, fmt.Errorf("found %d bindings for type '%s' and provider '%s' but expected exactly 1", len(bindings), typ, provider) 90 } 91 return bindings[0], nil 92 } 93 94 func loadBindings(bindingRoot string) ([]Binding, error) { 95 files, err := os.ReadDir(bindingRoot) 96 if os.IsNotExist(err) { 97 return nil, nil 98 } else if err != nil { 99 return nil, err 100 } 101 102 var bindings []Binding 103 for _, file := range files { 104 isLegacy, err := isLegacyBinding(bindingRoot, file.Name()) 105 if err != nil { 106 return nil, err 107 } 108 109 var binding Binding 110 if isLegacy { 111 binding, err = loadLegacyBinding(bindingRoot, file.Name()) 112 } else { 113 binding, err = loadBinding(bindingRoot, file.Name()) 114 } 115 if err != nil { 116 return nil, fmt.Errorf("failed to read binding '%s': %w", file.Name(), err) 117 } 118 bindings = append(bindings, binding) 119 } 120 return bindings, nil 121 } 122 123 func bindingRoot(platformDir string) string { 124 root := os.Getenv("SERVICE_BINDING_ROOT") 125 if root == "" { 126 root = os.Getenv("CNB_BINDINGS") 127 } 128 129 if root == "" { 130 root = filepath.Join(platformDir, "bindings") 131 } 132 return root 133 } 134 135 // According to the legacy spec (https://github.com/buildpacks/spec/blob/main/extensions/bindings.md), a legacy binding 136 // has a `metadata` directory within the binding path. 137 func isLegacyBinding(bindingRoot, name string) (bool, error) { 138 info, err := os.Stat(filepath.Join(bindingRoot, name, "metadata")) 139 if err == nil { 140 return info.IsDir(), nil 141 } else if os.IsNotExist(err) { 142 return false, nil 143 } 144 return false, err 145 } 146 147 // See: https://github.com/k8s-service-bindings/spec#workload-projection 148 func loadBinding(bindingRoot, name string) (Binding, error) { 149 binding := Binding{ 150 Name: name, 151 Path: filepath.Join(bindingRoot, name), 152 Entries: map[string]*Entry{}, 153 } 154 155 entries, err := loadEntries(filepath.Join(binding.Path)) 156 if err != nil { 157 return Binding{}, err 158 } 159 160 typ, ok := entries["type"] 161 if !ok { 162 return Binding{}, errors.New("missing 'type'") 163 } 164 binding.Type, err = typ.ReadString() 165 if err != nil { 166 return Binding{}, err 167 } 168 delete(entries, "type") 169 170 provider, ok := entries["provider"] 171 if ok { 172 binding.Provider, err = provider.ReadString() 173 if err != nil { 174 return Binding{}, err 175 } 176 delete(entries, "provider") 177 } 178 179 binding.Entries = entries 180 181 return binding, nil 182 } 183 184 // See: https://github.com/buildpacks/spec/blob/main/extensions/bindings.md 185 func loadLegacyBinding(bindingRoot, name string) (Binding, error) { 186 binding := Binding{ 187 Name: name, 188 Path: filepath.Join(bindingRoot, name), 189 Entries: map[string]*Entry{}, 190 } 191 192 metadata, err := loadEntries(filepath.Join(binding.Path, "metadata")) 193 if err != nil { 194 return Binding{}, err 195 } 196 197 typ, ok := metadata["kind"] 198 if !ok { 199 return Binding{}, errors.New("missing 'kind'") 200 } 201 binding.Type, err = typ.ReadString() 202 if err != nil { 203 return Binding{}, err 204 } 205 delete(metadata, "kind") 206 207 provider, ok := metadata["provider"] 208 if !ok { 209 return Binding{}, errors.New("missing 'provider'") 210 } 211 binding.Provider, err = provider.ReadString() 212 if err != nil { 213 return Binding{}, err 214 } 215 delete(metadata, "provider") 216 217 binding.Entries = metadata 218 219 secrets, err := loadEntries(filepath.Join(binding.Path, "secret")) 220 if err != nil && !os.IsNotExist(err) { 221 return Binding{}, err 222 } 223 if err == nil { 224 for k, v := range secrets { 225 binding.Entries[k] = v 226 } 227 } 228 229 return binding, nil 230 } 231 232 func loadEntries(path string) (map[string]*Entry, error) { 233 entries := map[string]*Entry{} 234 files, err := os.ReadDir(path) 235 if err != nil { 236 return nil, err 237 } 238 239 for _, file := range files { 240 entries[file.Name()] = NewEntry(filepath.Join(path, file.Name())) 241 } 242 return entries, nil 243 }