github.com/BarDweller/libpak@v0.0.0-20230630201634-8dd5cfc15ec9/carton/package_dependency.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *      https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package carton
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  
    25  	"github.com/BarDweller/libpak/bard"
    26  	"github.com/BarDweller/libpak/internal"
    27  	"github.com/BurntSushi/toml"
    28  )
    29  
    30  type PackageDependency struct {
    31  	BuilderPath   string
    32  	BuildpackPath string
    33  	ID            string
    34  	Version       string
    35  	PackagePath   string
    36  }
    37  
    38  func (p PackageDependency) Update(options ...Option) {
    39  	config := Config{
    40  		exitHandler: internal.NewExitHandler(),
    41  	}
    42  
    43  	for _, option := range options {
    44  		config = option(config)
    45  	}
    46  
    47  	logger := bard.NewLogger(os.Stdout)
    48  	_, _ = fmt.Fprintf(logger.TitleWriter(), "\n%s\n", bard.FormatIdentity(p.ID, p.Version))
    49  
    50  	if p.BuilderPath != "" {
    51  		if err := updateFile(p.BuilderPath, updateByKey("buildpacks", p.ID, p.Version)); err != nil {
    52  			config.exitHandler.Error(fmt.Errorf("unable to update %s\n%w", p.BuilderPath, err))
    53  		}
    54  	}
    55  
    56  	if p.PackagePath != "" {
    57  		if err := updateFile(p.PackagePath, updateByKey("dependencies", p.ID, p.Version)); err != nil {
    58  			config.exitHandler.Error(fmt.Errorf("unable to update %s\n%w", p.PackagePath, err))
    59  		}
    60  	}
    61  
    62  	// Do we have a buildpack.toml with an order element? (composite buildpack)
    63  	if p.BuildpackPath != "" {
    64  		if err := updateFile(p.BuildpackPath, func(md map[string]interface{}) {
    65  			parts := strings.Split(p.ID, "/")
    66  			id := strings.Join(parts[len(parts)-2:], "/")
    67  
    68  			groupsUnwrapped, found := md["order"]
    69  			if !found {
    70  				return
    71  			}
    72  
    73  			groups, ok := groupsUnwrapped.([]map[string]interface{})
    74  			if !ok {
    75  				return
    76  			}
    77  
    78  			for _, group := range groups {
    79  				buildpacksUnwrapped, found := group["group"]
    80  				if !found {
    81  					continue
    82  				}
    83  
    84  				buildpacks, ok := buildpacksUnwrapped.([]interface{})
    85  				if !ok {
    86  					continue
    87  				}
    88  
    89  				for _, bpw := range buildpacks {
    90  					bp, ok := bpw.(map[string]interface{})
    91  					if !ok {
    92  						continue
    93  					}
    94  
    95  					bpIdUnwrappd, found := bp["id"]
    96  					if !found {
    97  						continue
    98  					}
    99  
   100  					bpId, ok := bpIdUnwrappd.(string)
   101  					if !ok {
   102  						continue
   103  					}
   104  
   105  					if bpId == id {
   106  						bp["version"] = p.Version
   107  					}
   108  				}
   109  			}
   110  		}); err != nil {
   111  			config.exitHandler.Error(fmt.Errorf("unable to update %s\n%w", p.BuildpackPath, err))
   112  		}
   113  	}
   114  }
   115  
   116  func updateByKey(key, id, version string) func(md map[string]interface{}) {
   117  	return func(md map[string]interface{}) {
   118  		valuesUnwrapped, found := md[key]
   119  		if !found {
   120  			return
   121  		}
   122  
   123  		values, ok := valuesUnwrapped.([]interface{})
   124  		if !ok {
   125  			return
   126  		}
   127  
   128  		for _, bpw := range values {
   129  			bp, ok := bpw.(map[string]interface{})
   130  			if !ok {
   131  				continue
   132  			}
   133  
   134  			uriUnwrapped, found := bp["uri"]
   135  			if !found {
   136  				continue
   137  			}
   138  
   139  			uri, ok := uriUnwrapped.(string)
   140  			if !ok {
   141  				continue
   142  			}
   143  
   144  			if strings.HasPrefix(uri, fmt.Sprintf("docker://%s", id)) {
   145  				parts := strings.Split(uri, ":")
   146  				bp["uri"] = fmt.Sprintf("%s:%s", strings.Join(parts[0:2], ":"), version)
   147  			}
   148  		}
   149  	}
   150  }
   151  
   152  func updateFile(cfgPath string, f func(md map[string]interface{})) error {
   153  	c, err := os.ReadFile(cfgPath)
   154  	if err != nil {
   155  		return fmt.Errorf("unable to read %s\n%w", cfgPath, err)
   156  	}
   157  
   158  	// save any leading comments, this is to preserve license headers
   159  	// inline comments will be lost
   160  	comments := []byte{}
   161  	for i, line := range bytes.SplitAfter(c, []byte("\n")) {
   162  		if bytes.HasPrefix(line, []byte("#")) || (i > 0 && len(bytes.TrimSpace(line)) == 0) {
   163  			comments = append(comments, line...)
   164  		} else {
   165  			break // stop on first comment
   166  		}
   167  	}
   168  
   169  	md := make(map[string]interface{})
   170  	if err := toml.Unmarshal(c, &md); err != nil {
   171  		return fmt.Errorf("unable to decode md %s\n%w", cfgPath, err)
   172  	}
   173  
   174  	f(md)
   175  
   176  	b, err := internal.Marshal(md)
   177  	if err != nil {
   178  		return fmt.Errorf("unable to encode md %s\n%w", cfgPath, err)
   179  	}
   180  
   181  	b = append(comments, b...)
   182  
   183  	if err := os.WriteFile(cfgPath, b, 0644); err != nil {
   184  		return fmt.Errorf("unable to write %s\n%w", cfgPath, err)
   185  	}
   186  
   187  	return nil
   188  }