github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/render/github/render.go (about)

     1  package github
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/go-kit/kit/log"
    13  	"github.com/go-kit/kit/log/level"
    14  	"github.com/pkg/errors"
    15  	"github.com/replicatedhq/libyaml"
    16  	"github.com/replicatedhq/ship/pkg/api"
    17  	"github.com/replicatedhq/ship/pkg/constants"
    18  	"github.com/replicatedhq/ship/pkg/lifecycle/render/root"
    19  	"github.com/replicatedhq/ship/pkg/specs/apptype"
    20  	"github.com/replicatedhq/ship/pkg/specs/githubclient"
    21  	"github.com/replicatedhq/ship/pkg/specs/gogetter"
    22  	"github.com/replicatedhq/ship/pkg/state"
    23  	"github.com/replicatedhq/ship/pkg/templates"
    24  	"github.com/replicatedhq/ship/pkg/util"
    25  	"github.com/spf13/afero"
    26  	"github.com/spf13/viper"
    27  )
    28  
    29  // Renderer is something that can render a helm asset as part of a planner.Plan
    30  type Renderer interface {
    31  	Execute(
    32  		rootFs root.Fs,
    33  		asset api.GitHubAsset,
    34  		configGroups []libyaml.ConfigGroup,
    35  		renderRoot string,
    36  		meta api.ReleaseMetadata,
    37  		templateContext map[string]interface{},
    38  	) func(ctx context.Context) error
    39  }
    40  
    41  var _ Renderer = &LocalRenderer{}
    42  
    43  // LocalRenderer pulls proxied github files from pg
    44  // and pulls no proxy github files directly via git or the git client
    45  type LocalRenderer struct {
    46  	Logger         log.Logger
    47  	Fs             afero.Afero
    48  	BuilderBuilder *templates.BuilderBuilder
    49  	Viper          *viper.Viper
    50  	StateManager   state.Manager
    51  }
    52  
    53  func NewRenderer(
    54  	logger log.Logger,
    55  	fs afero.Afero,
    56  	viper *viper.Viper,
    57  	builderBuilder *templates.BuilderBuilder,
    58  	stateManager state.Manager,
    59  ) Renderer {
    60  	return &LocalRenderer{
    61  		Logger:         logger,
    62  		Fs:             fs,
    63  		Viper:          viper,
    64  		BuilderBuilder: builderBuilder,
    65  		StateManager:   stateManager,
    66  	}
    67  }
    68  
    69  // refactored from planner.plan but I neeeeed tests
    70  func (r *LocalRenderer) Execute(
    71  	rootFs root.Fs,
    72  	asset api.GitHubAsset,
    73  	configGroups []libyaml.ConfigGroup,
    74  	renderRoot string,
    75  	meta api.ReleaseMetadata,
    76  	templateContext map[string]interface{},
    77  ) func(ctx context.Context) error {
    78  	return func(ctx context.Context) error {
    79  		debug := level.Debug(log.With(r.Logger, "step.type", "render", "render.phase", "execute", "asset.type", "github", "dest", asset.Dest, "description", asset.Description))
    80  
    81  		debug.Log("event", "execute")
    82  		basePath := filepath.Dir(asset.Dest)
    83  		debug.Log("event", "mkdirall.attempt", "root", rootFs.RootPath, "dest", asset.Dest, "basePath", basePath)
    84  		if err := rootFs.MkdirAll(basePath, 0755); err != nil {
    85  			debug.Log("event", "mkdirall.fail", "err", err, "root", rootFs.RootPath, "dest", asset.Dest, "basePath", basePath)
    86  			return errors.Wrapf(err, "write directory to %s", asset.Dest)
    87  		}
    88  
    89  		builder, err := r.BuilderBuilder.FullBuilder(meta, configGroups, templateContext)
    90  		if err != nil {
    91  			return errors.Wrap(err, "init builder")
    92  		}
    93  
    94  		debug.Log("event", "resolveProxyGithubAssets")
    95  		files := filterGithubContents(meta.GithubContents, asset)
    96  		if len(files) == 0 {
    97  			level.Info(r.Logger).Log("msg", "no proxy files for asset", "repo", asset.Repo, "path", asset.Path)
    98  			r.debugDumpKnownGithubFiles(meta, asset)
    99  
   100  			if asset.Source == "public" || !asset.Proxy {
   101  				debug.Log("event", "resolveNoProxyGithubAssets")
   102  				err := r.resolveNoProxyGithubAssets(asset, builder, renderRoot)
   103  				if err != nil {
   104  					return errors.Wrap(err, "resolveNoProxyGithubAssets")
   105  				}
   106  			} else {
   107  				return fmt.Errorf("github asset %s returned no files in %s at %s", asset.Repo, asset.Path, asset.Ref)
   108  			}
   109  		}
   110  
   111  		return r.resolveProxyGithubAssets(asset, builder, rootFs, files)
   112  	}
   113  }
   114  
   115  func (r *LocalRenderer) debugDumpKnownGithubFiles(meta api.ReleaseMetadata, asset api.GitHubAsset) {
   116  	debugStr := "["
   117  	for _, content := range meta.GithubContents {
   118  		debugStr += fmt.Sprintf("%s, ", content.String())
   119  
   120  	}
   121  	debugStr += "]"
   122  
   123  	level.Debug(r.Logger).Log(
   124  		"msg", "github contents",
   125  		"repo", asset.Repo,
   126  		"path", asset.Path,
   127  		"releaseMeta", debugStr,
   128  	)
   129  }
   130  
   131  func filterGithubContents(githubContents []api.GithubContent, asset api.GitHubAsset) []api.GithubFile {
   132  	var filtered []api.GithubFile
   133  	for _, c := range githubContents {
   134  		if c.Repo == asset.Repo && strings.Trim(c.Path, "/") == strings.Trim(asset.Path, "/") && c.Ref == asset.Ref {
   135  			filtered = c.Files
   136  			break
   137  		}
   138  	}
   139  	return filtered
   140  }
   141  
   142  func (r *LocalRenderer) resolveProxyGithubAssets(asset api.GitHubAsset, builder *templates.Builder, rootFs root.Fs, files []api.GithubFile) error {
   143  	debug := level.Debug(log.With(r.Logger, "step.type", "render", "render.phase", "execute", "asset.type", "github", "dest", asset.Dest, "description", asset.Description))
   144  
   145  	for _, file := range files {
   146  		data, err := base64.StdEncoding.DecodeString(file.Data)
   147  		if err != nil {
   148  			return errors.Wrapf(err, "decode %s", file.Path)
   149  		}
   150  
   151  		built, err := builder.String(string(data))
   152  		if err != nil {
   153  			return errors.Wrapf(err, "building %s", file.Path)
   154  		}
   155  
   156  		filePath, err := getDestPath(file.Path, asset, builder)
   157  		if err != nil {
   158  			return errors.Wrapf(err, "determining destination for %s", file.Path)
   159  		}
   160  
   161  		basePath := filepath.Dir(filePath)
   162  		debug.Log("event", "mkdirall.attempt", "root", rootFs.RootPath, "dest", filePath, "basePath", basePath)
   163  		if err := rootFs.MkdirAll(basePath, 0755); err != nil {
   164  			debug.Log("event", "mkdirall.fail", "err", err, "root", rootFs.RootPath, "dest", filePath, "basePath", basePath)
   165  			return errors.Wrapf(err, "write directory to %s", filePath)
   166  		}
   167  
   168  		mode := os.FileMode(0644) // TODO: how to get mode info from github?
   169  		if asset.AssetShared.Mode != os.FileMode(0000) {
   170  			debug.Log("event", "applying override permissions", "override.filemode", asset.AssetShared.Mode, "override.filemode.int", int(asset.AssetShared.Mode))
   171  			mode = asset.AssetShared.Mode
   172  		}
   173  		if err := rootFs.WriteFile(filePath, []byte(built), mode); err != nil {
   174  			debug.Log("event", "execute.fail", "err", err)
   175  			return errors.Wrapf(err, "Write inline asset to %s", filePath)
   176  		}
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  func (r *LocalRenderer) resolveNoProxyGithubAssets(asset api.GitHubAsset, builder *templates.Builder, renderRoot string) error {
   183  	debug := level.Debug(log.With(r.Logger, "step.type", "render", "render.phase", "execute", "asset.type", "github", "dest", asset.Dest, "description", asset.Description))
   184  	debug.Log("event", "createUpstream")
   185  	upstream, err := createUpstreamURL(asset, builder)
   186  	if err != nil {
   187  		return errors.Wrapf(err, "create upstream url")
   188  	}
   189  
   190  	var fetcher apptype.FileFetcher
   191  	localFetchPath := filepath.Join(constants.InstallerPrefixPath, constants.GithubAssetSavePath)
   192  	fetcher = githubclient.NewGithubClient(r.Fs, r.Logger)
   193  	if r.Viper.GetBool("prefer-git") {
   194  		var isSingleFile bool
   195  		var subdir string
   196  		upstream, subdir, isSingleFile = gogetter.UntreeGithub(upstream)
   197  		fetcher = &gogetter.GoGetter{Logger: r.Logger, FS: r.Fs, Subdir: subdir, IsSingleFile: isSingleFile}
   198  	}
   199  
   200  	debug.Log("event", "getFiles", "upstream", upstream)
   201  	localPath, err := fetcher.GetFiles(context.Background(), upstream, localFetchPath)
   202  	if err != nil {
   203  		return errors.Wrap(err, "get files")
   204  	}
   205  
   206  	debug.Log("event", "getDestPath")
   207  	dest, err := r.getDestPathNoProxy(asset, builder, renderRoot)
   208  	if err != nil {
   209  		return errors.Wrap(err, "get dest path")
   210  	}
   211  
   212  	if filepath.Ext(asset.Path) != "" {
   213  		localPath = filepath.Join(localPath, asset.Path)
   214  	}
   215  
   216  	exists, err := r.Fs.Exists(filepath.Dir(dest))
   217  	if err != nil {
   218  		return errors.Wrap(err, "dest dir exists")
   219  	}
   220  
   221  	if !exists {
   222  		debug.Log("event", "mkdirall", "dir", filepath.Dir(dest))
   223  		if err := r.Fs.MkdirAll(filepath.Dir(dest), 0755); err != nil {
   224  			return errors.Wrap(err, "mkdir all dest dir")
   225  		}
   226  	}
   227  
   228  	debug.Log("event", "rename", "from", localPath, "dest", dest)
   229  	if err := r.Fs.Rename(localPath, dest); err != nil {
   230  		return errors.Wrap(err, "rename to dest")
   231  	}
   232  
   233  	if err := r.Fs.RemoveAll(localFetchPath); err != nil {
   234  		return errors.Wrap(err, "remove tmp github asset")
   235  	}
   236  
   237  	if err := templates.BuildDir(dest, &r.Fs, builder); err != nil {
   238  		return errors.Wrapf(err, "render templates in github asset %s", dest)
   239  	}
   240  
   241  	return nil
   242  }
   243  
   244  func getDestPath(githubPath string, asset api.GitHubAsset, builder *templates.Builder) (string, error) {
   245  	stripPath, err := builder.Bool(asset.StripPath, false)
   246  	if err != nil {
   247  		return "", errors.Wrapf(err, "parse boolean from %q", asset.StripPath)
   248  	}
   249  
   250  	destDir, err := builder.String(asset.Dest)
   251  	if err != nil {
   252  		return "", errors.Wrapf(err, "get destination directory from %q", asset.Dest)
   253  	}
   254  
   255  	if stripPath {
   256  		// remove asset.Path's directory from the beginning of githubPath
   257  		sourcePathDir := filepath.ToSlash(filepath.Dir(asset.Path)) + "/"
   258  		githubPath = strings.TrimPrefix(githubPath, sourcePathDir)
   259  
   260  		// handle cases where the source path was a dir but a trailing slash was not included
   261  		if !strings.HasSuffix(asset.Path, "/") {
   262  			sourcePathBase := filepath.Base(asset.Path) + "/"
   263  			githubPath = strings.TrimPrefix(githubPath, sourcePathBase)
   264  		}
   265  	}
   266  
   267  	combinedPath := filepath.Join(destDir, githubPath)
   268  
   269  	err = util.IsLegalPath(combinedPath)
   270  	if err != nil {
   271  		return "", errors.Wrap(err, "write github asset")
   272  	}
   273  
   274  	return combinedPath, nil
   275  }
   276  
   277  func (r *LocalRenderer) getDestPathNoProxy(asset api.GitHubAsset, builder *templates.Builder, renderRoot string) (string, error) {
   278  
   279  	debug := level.Debug(log.With(r.Logger, "method", "getdestPathNoProxy", "asset.type", "github", "dest", asset.Dest, "renderRoot", renderRoot, "proxy", false))
   280  	assetPath := asset.Path
   281  	stripPath, err := builder.Bool(asset.StripPath, false)
   282  	if err != nil {
   283  		return "", errors.Wrapf(err, "parse boolean from %q", asset.StripPath)
   284  	}
   285  
   286  	destDir, err := builder.String(asset.Dest)
   287  	if err != nil {
   288  		return "", errors.Wrapf(err, "get destination directory from %q", asset.Dest)
   289  	}
   290  
   291  	if stripPath {
   292  		if filepath.Ext(assetPath) != "" {
   293  			assetPath = filepath.Base(assetPath)
   294  		} else {
   295  			assetPath = ""
   296  		}
   297  	}
   298  
   299  	destPath := filepath.Join(renderRoot, destDir, assetPath)
   300  	debug.Log("event", "destPath.resolve", "path", destPath)
   301  	return destPath, nil
   302  }
   303  
   304  func createUpstreamURL(asset api.GitHubAsset, builder *templates.Builder) (string, error) {
   305  	var assetType string
   306  	assetPath, err := builder.String(asset.Path)
   307  	if err != nil {
   308  		return "", errors.Wrapf(err, "build asset path %s", asset.Path)
   309  	}
   310  	assetBasePath := filepath.Base(assetPath)
   311  	if filepath.Ext(assetBasePath) != "" {
   312  		assetType = "blob"
   313  	} else {
   314  		assetType = "tree"
   315  	}
   316  
   317  	assetRef := "master"
   318  	templatedAssetRef, err := builder.String(asset.Ref)
   319  	if err != nil {
   320  		return "", errors.Wrapf(err, "build asset ref %s", asset.Ref)
   321  	}
   322  
   323  	if templatedAssetRef != "" {
   324  		assetRef = templatedAssetRef
   325  	}
   326  
   327  	assetRepo, err := builder.String(asset.Repo)
   328  	if err != nil {
   329  		return "", errors.Wrapf(err, "build asset repo %s", asset.Repo)
   330  	}
   331  
   332  	return path.Join("github.com", assetRepo, assetType, assetRef, assetPath), nil
   333  }