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

     1  package web
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net/http"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/go-kit/kit/log"
    11  	"github.com/go-kit/kit/log/level"
    12  	"github.com/pkg/errors"
    13  	"github.com/replicatedhq/libyaml"
    14  	"github.com/replicatedhq/ship/pkg/api"
    15  	"github.com/replicatedhq/ship/pkg/lifecycle/render/root"
    16  	"github.com/replicatedhq/ship/pkg/templates"
    17  	"github.com/replicatedhq/ship/pkg/util"
    18  	"github.com/spf13/afero"
    19  	"github.com/spf13/viper"
    20  )
    21  
    22  type Renderer interface {
    23  	Execute(
    24  		rootFs root.Fs,
    25  		asset api.WebAsset,
    26  		meta api.ReleaseMetadata,
    27  		templateContext map[string]interface{},
    28  		configGroups []libyaml.ConfigGroup,
    29  	) func(ctx context.Context) error
    30  }
    31  
    32  var _ Renderer = &DefaultStep{}
    33  
    34  type DefaultStep struct {
    35  	Logger         log.Logger
    36  	Fs             afero.Afero
    37  	Viper          *viper.Viper
    38  	BuilderBuilder *templates.BuilderBuilder
    39  	Client         *http.Client
    40  }
    41  
    42  func NewStep(
    43  	logger log.Logger,
    44  	fs afero.Afero,
    45  	v *viper.Viper,
    46  	builderBuilder *templates.BuilderBuilder,
    47  ) Renderer {
    48  	return &DefaultStep{
    49  		Logger:         logger,
    50  		Fs:             fs,
    51  		Viper:          v,
    52  		BuilderBuilder: builderBuilder,
    53  		Client:         &http.Client{},
    54  	}
    55  }
    56  
    57  type Built struct {
    58  	URL        string
    59  	Dest       string
    60  	Method     string
    61  	Body       string
    62  	Headers    map[string][]string
    63  	BodyFormat string
    64  }
    65  
    66  func (p *DefaultStep) Execute(
    67  	rootFs root.Fs,
    68  	asset api.WebAsset,
    69  	meta api.ReleaseMetadata,
    70  	templateContext map[string]interface{},
    71  	configGroups []libyaml.ConfigGroup,
    72  ) func(ctx context.Context) error {
    73  
    74  	debug := level.Debug(log.With(p.Logger, "step.type", "render", "render.phase", "execute",
    75  		"asset.type", "web", "dest", asset.Dest, "description", asset.Description))
    76  
    77  	return func(ctx context.Context) error {
    78  		debug.Log("event", "execute")
    79  
    80  		if p.Viper.GetBool("no-web") {
    81  			return errors.New("web assets are disabled when no-web is set")
    82  		}
    83  
    84  		built, err := p.buildAsset(asset, meta, configGroups, templateContext)
    85  		if err != nil {
    86  			debug.Log("event", "build.fail", "err", err)
    87  			return errors.Wrapf(err, "Build web asset")
    88  		}
    89  
    90  		err = util.IsLegalPath(built.Dest)
    91  		if err != nil {
    92  			return errors.Wrap(err, "write web asset")
    93  		}
    94  
    95  		basePath := filepath.Dir(asset.Dest)
    96  		debug.Log("event", "mkdirall.attempt", "root", rootFs.RootPath, "dest", built.Dest, "basePath", basePath)
    97  		if err := rootFs.MkdirAll(basePath, 0755); err != nil {
    98  			debug.Log("event", "mkdirall.fail", "err", err, "root", rootFs.RootPath, "dest", built.Dest, "basePath", basePath)
    99  			return errors.Wrapf(err, "Create directory path %s", basePath)
   100  		}
   101  
   102  		if err := p.pullWebAsset(rootFs, built); err != nil {
   103  			debug.Log("event", "pullWebAsset.fail", "err", err)
   104  			return errors.Wrapf(err, "Get web asset from %s", asset.URL)
   105  		}
   106  
   107  		return nil
   108  	}
   109  }
   110  
   111  func (p *DefaultStep) buildAsset(
   112  	asset api.WebAsset,
   113  	meta api.ReleaseMetadata,
   114  	configGroups []libyaml.ConfigGroup,
   115  	templateContext map[string]interface{},
   116  ) (*Built, error) {
   117  
   118  	builder, err := p.BuilderBuilder.FullBuilder(meta, configGroups, templateContext)
   119  	if err != nil {
   120  		return nil, errors.Wrap(err, "init builder")
   121  	}
   122  
   123  	builtURL, err := builder.String(asset.URL)
   124  	if err != nil {
   125  		return nil, errors.Wrap(err, "building url")
   126  	}
   127  
   128  	builtDest, err := builder.String(asset.Dest)
   129  	if err != nil {
   130  		return nil, errors.Wrap(err, "building dest")
   131  	}
   132  
   133  	builtMethod, err := builder.String(asset.Method)
   134  	if err != nil {
   135  		return nil, errors.Wrap(err, "building method")
   136  	}
   137  
   138  	builtBody, err := builder.String(asset.Body)
   139  	if err != nil {
   140  		return nil, errors.Wrap(err, "building body")
   141  	}
   142  
   143  	builtBodyFormat, err := builder.String(asset.BodyFormat)
   144  	if err != nil {
   145  		return nil, errors.Wrap(err, "building content type")
   146  	}
   147  
   148  	builtHeaders := make(map[string][]string)
   149  	for header, listOfValues := range asset.Headers {
   150  		for _, value := range listOfValues {
   151  			builtHeaderVal, err := builder.String(value)
   152  			if err != nil {
   153  				return nil, errors.Wrap(err, "building header val")
   154  			}
   155  			builtHeaders[header] = append(builtHeaders[header], builtHeaderVal)
   156  		}
   157  	}
   158  	return &Built{
   159  		URL:        builtURL,
   160  		Dest:       builtDest,
   161  		Method:     builtMethod,
   162  		Body:       builtBody,
   163  		BodyFormat: builtBodyFormat,
   164  		Headers:    builtHeaders,
   165  	}, nil
   166  }
   167  
   168  func (p *DefaultStep) pullWebAsset(rootFs root.Fs, built *Built) error {
   169  	req, err := http.NewRequest(built.Method, built.URL, strings.NewReader(built.Body))
   170  	if err != nil {
   171  		return errors.Wrapf(err, "Request web asset from %s", built.URL)
   172  	}
   173  
   174  	if built.Method == "POST" {
   175  		req.Header.Set("Content-Type", built.BodyFormat)
   176  	}
   177  
   178  	if len(built.Headers) != 0 {
   179  		for header, listOfValues := range built.Headers {
   180  			for _, value := range listOfValues {
   181  				req.Header.Add(header, value)
   182  			}
   183  		}
   184  	}
   185  
   186  	resp, respErr := p.Client.Do(req)
   187  	if respErr != nil {
   188  		return errors.Wrapf(respErr, "%s web asset at %s", built.Method, built.URL)
   189  	}
   190  	defer resp.Body.Close() //nolint:errcheck
   191  
   192  	if resp.StatusCode > 299 {
   193  		return errors.Errorf("received response with status %d", resp.StatusCode)
   194  	}
   195  
   196  	file, err := rootFs.Create(built.Dest)
   197  	if err != nil {
   198  		return errors.Wrapf(err, "Create file %s", file)
   199  	}
   200  
   201  	if _, err := io.Copy(file, resp.Body); err != nil {
   202  		return errors.Wrapf(err, "Stream HTTP response body to %s", file.Name())
   203  	}
   204  	return errors.Wrapf(file.Close(), "close file at %s", built.Dest)
   205  }