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  }