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  }