github.com/replicatedcom/ship@v0.50.0/pkg/specs/replicatedapp/resolver.go (about)

     1  package replicatedapp
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/url"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/go-kit/kit/log"
    14  	"github.com/go-kit/kit/log/level"
    15  	"github.com/mitchellh/cli"
    16  	"github.com/pkg/errors"
    17  	"github.com/replicatedhq/ship/pkg/api"
    18  	"github.com/replicatedhq/ship/pkg/constants"
    19  	"github.com/replicatedhq/ship/pkg/helpers/flags"
    20  	"github.com/replicatedhq/ship/pkg/specs/apptype"
    21  	"github.com/replicatedhq/ship/pkg/state"
    22  	"github.com/spf13/afero"
    23  	"github.com/spf13/viper"
    24  	yaml "gopkg.in/yaml.v3"
    25  )
    26  
    27  type shaSummer func(release state.ShipRelease) string
    28  type dater func() string
    29  type resolver struct {
    30  	Logger               log.Logger
    31  	Client               *GraphQLClient
    32  	FS                   afero.Afero
    33  	StateManager         state.Manager
    34  	UI                   cli.Ui
    35  	ShaSummer            shaSummer
    36  	Dater                dater
    37  	Runbook              string
    38  	SetChannelName       string
    39  	RunbookReleaseSemver string
    40  	SetChannelIcon       string
    41  	SetGitHubContents    []string
    42  	SetEntitlementsJSON  string
    43  	IsEdit               bool
    44  }
    45  
    46  // NewAppResolver builds a resolver from a Viper instance
    47  func NewAppResolver(
    48  	v *viper.Viper,
    49  	logger log.Logger,
    50  	fs afero.Afero,
    51  	graphql *GraphQLClient,
    52  	stateManager state.Manager,
    53  	ui cli.Ui,
    54  ) Resolver {
    55  	return &resolver{
    56  		Logger:               logger,
    57  		Client:               graphql,
    58  		FS:                   fs,
    59  		UI:                   ui,
    60  		Runbook:              flags.GetCurrentOrDeprecatedString(v, "runbook", "studio-file"),
    61  		SetChannelName:       flags.GetCurrentOrDeprecatedString(v, "set-channel-name", "studio-channel-name"),
    62  		SetChannelIcon:       flags.GetCurrentOrDeprecatedString(v, "set-channel-icon", "studio-channel-icon"),
    63  		SetGitHubContents:    v.GetStringSlice("set-github-contents"),
    64  		SetEntitlementsJSON:  v.GetString("set-entitlements-json"),
    65  		RunbookReleaseSemver: v.GetString("release-semver"),
    66  		IsEdit:               v.GetBool("isEdit"),
    67  		StateManager:         stateManager,
    68  		ShaSummer: func(release state.ShipRelease) string {
    69  			release.Entitlements.Signature = "" // entitlements signature is not stable
    70  			releaseJSON, err := json.Marshal(release)
    71  			if err != nil {
    72  				panic(errors.Wrap(err, "marshal release to json for content sha"))
    73  			}
    74  
    75  			return fmt.Sprintf("%x", sha256.Sum256(releaseJSON))
    76  		},
    77  		Dater: func() string {
    78  			// format consistent with what we get from GQL
    79  			return time.Now().UTC().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
    80  		},
    81  	}
    82  }
    83  
    84  type Resolver interface {
    85  	ResolveAppRelease(
    86  		ctx context.Context,
    87  		selector *Selector,
    88  		app apptype.LocalAppCopy,
    89  	) (*api.Release, error)
    90  	FetchRelease(
    91  		ctx context.Context,
    92  		selector *Selector,
    93  	) (*state.ShipRelease, error)
    94  	RegisterInstall(
    95  		ctx context.Context,
    96  		selector Selector,
    97  		release *api.Release,
    98  	) error
    99  	SetRunbook(
   100  		runbook string,
   101  	)
   102  	ResolveEditRelease(
   103  		ctx context.Context,
   104  	) (*api.Release, error)
   105  }
   106  
   107  // ResolveAppRelease uses the passed config options to get specs from pg.replicated.com or
   108  // from a local runbook if so configured
   109  func (r *resolver) ResolveAppRelease(ctx context.Context, selector *Selector, app apptype.LocalAppCopy) (*api.Release, error) {
   110  	debug := level.Debug(log.With(r.Logger, "method", "ResolveAppRelease"))
   111  
   112  	release, err := r.FetchRelease(ctx, selector)
   113  	if err != nil {
   114  		return nil, errors.Wrap(err, "fetch release")
   115  	}
   116  
   117  	license, err := r.FetchLicense(ctx, selector)
   118  	if err != nil {
   119  		return nil, errors.Wrap(err, "fetch license")
   120  	}
   121  
   122  	releaseName := release.ToReleaseMeta().ReleaseName()
   123  	debug.Log("event", "resolve.releaseName")
   124  
   125  	if err := r.StateManager.SerializeReleaseName(releaseName); err != nil {
   126  		debug.Log("event", "serialize.releaseName.fail", "err", err)
   127  		return nil, errors.Wrapf(err, "serialize helm release name")
   128  	}
   129  
   130  	result, err := r.persistRelease(release, license, selector)
   131  	if err != nil {
   132  		return nil, errors.Wrap(err, "persist and deserialize release")
   133  	}
   134  
   135  	result.Metadata.Type = app.GetType()
   136  
   137  	return result, nil
   138  }
   139  
   140  func (r *resolver) ResolveEditRelease(ctx context.Context) (*api.Release, error) {
   141  	stateData, err := r.StateManager.CachedState()
   142  	if err != nil {
   143  		return nil, errors.Wrap(err, "load state to resolve release")
   144  	}
   145  
   146  	result := &api.Release{
   147  		Metadata: *stateData.ReleaseMetadata(),
   148  	}
   149  
   150  	if r.Runbook == "" {
   151  		result.Metadata.Type = "replicated.app"
   152  	} else {
   153  		result.Metadata.Type = "runbook.replicated.app"
   154  	}
   155  
   156  	if err = yaml.Unmarshal([]byte(stateData.UpstreamContents().AppRelease.Spec), &result.Spec); err != nil {
   157  		return nil, errors.Wrapf(err, "decode spec from persisted release")
   158  	}
   159  
   160  	if err = r.persistSpec([]byte(stateData.UpstreamContents().AppRelease.Spec)); err != nil {
   161  		return nil, errors.Wrapf(err, "write persisted spec to disk")
   162  	}
   163  
   164  	return result, nil
   165  }
   166  
   167  func (r *resolver) FetchLicense(ctx context.Context, selector *Selector) (*license, error) {
   168  	debug := level.Debug(log.With(r.Logger, "method", "FetchLicense"))
   169  	if r.Runbook != "" {
   170  		debug.Log("event", "license.fetch", "msg", "can't resolve license with runbooks")
   171  		return &license{}, nil
   172  	}
   173  
   174  	if selector.LicenseID == "" {
   175  		// TODO: support with customer ID
   176  		debug.Log("event", "license.fetch", "msg", "can't resolve license without license ID")
   177  		return &license{}, nil
   178  	}
   179  
   180  	license, err := r.Client.GetLicense(selector)
   181  	if err != nil {
   182  		return nil, errors.Wrapf(err, "get license")
   183  	}
   184  
   185  	return license, nil
   186  }
   187  
   188  // FetchRelease gets the release without persisting anything
   189  func (r *resolver) FetchRelease(ctx context.Context, selector *Selector) (*state.ShipRelease, error) {
   190  	var err error
   191  	var release *state.ShipRelease
   192  
   193  	debug := level.Debug(log.With(r.Logger, "method", "FetchRelease"))
   194  	if r.Runbook != "" {
   195  		release, err = r.resolveRunbookRelease(selector)
   196  		if err != nil {
   197  			return nil, errors.Wrapf(err, "resolve runbook from %s", r.Runbook)
   198  		}
   199  	} else {
   200  		release, err = r.resolveCloudRelease(selector)
   201  		debug.Log("event", "spec.resolve", "err", err)
   202  		if err != nil {
   203  			return nil, errors.Wrapf(err, "resolve gql spec for %s", selector)
   204  		}
   205  	}
   206  	debug.Log("event", "spec.resolve.success", "err", err)
   207  	return release, nil
   208  }
   209  
   210  func (r *resolver) persistRelease(release *state.ShipRelease, license *license, selector *Selector) (*api.Release, error) {
   211  	debug := level.Debug(log.With(r.Logger, "method", "persistRelease"))
   212  
   213  	result := &api.Release{
   214  		Metadata: release.ToReleaseMeta(),
   215  	}
   216  	result.Metadata.CustomerID = selector.CustomerID
   217  	result.Metadata.InstallationID = selector.InstallationID
   218  	result.Metadata.LicenseID = selector.LicenseID
   219  	result.Metadata.AppSlug = selector.AppSlug
   220  	result.Metadata.License = license.ToLicenseMeta()
   221  	result.Metadata.Installed = r.Dater()
   222  
   223  	if err := r.StateManager.SerializeAppMetadata(result.Metadata); err != nil {
   224  		return nil, errors.Wrap(err, "serialize app metadata")
   225  	}
   226  
   227  	contentSHA := r.ShaSummer(*release)
   228  	if err := r.StateManager.SerializeContentSHA(contentSHA); err != nil {
   229  		return nil, errors.Wrap(err, "serialize content sha")
   230  	}
   231  
   232  	if err := yaml.Unmarshal([]byte(release.Spec), &result.Spec); err != nil {
   233  		return nil, errors.Wrapf(err, "decode spec")
   234  	}
   235  	debug.Log("phase", "load-specs", "status", "complete",
   236  		"resolved-spec", fmt.Sprintf("%+v", result.Spec),
   237  	)
   238  
   239  	if r.Runbook == "" {
   240  		releaseCopy := *release
   241  
   242  		upstreamContents := state.UpstreamContents{
   243  			AppRelease: &releaseCopy,
   244  		}
   245  		err := r.StateManager.SerializeUpstreamContents(&upstreamContents)
   246  		if err != nil {
   247  			return nil, errors.Wrap(err, "persist upstream contents")
   248  		}
   249  	}
   250  
   251  	return result, nil
   252  }
   253  
   254  func (r *resolver) resolveCloudRelease(selector *Selector) (*state.ShipRelease, error) {
   255  	debug := level.Debug(log.With(r.Logger, "method", "resolveCloudSpec"))
   256  
   257  	var release *state.ShipRelease
   258  	var err error
   259  	client := r.Client
   260  	if selector.CustomerID != "" {
   261  		debug.Log("phase", "load-specs", "from", "gql", "addr", client.GQLServer.String(), "method", "customerID")
   262  		release, err = client.GetRelease(selector)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  	} else {
   267  		debug.Log("phase", "load-specs", "from", "gql", "addr", client.GQLServer.String(), "method", "appSlug")
   268  		if selector.AppSlug == "" {
   269  			return nil, errors.New("either a customer ID or app slug must be provided")
   270  		}
   271  		release, err = client.GetSlugRelease(selector)
   272  		if err != nil {
   273  			if selector.LicenseID == "" {
   274  				debug.Log("event", "spec-resolve", "from", selector, "error", err)
   275  
   276  				var input string
   277  				input, err = r.UI.Ask("Please enter your license to continue: ")
   278  				if err != nil {
   279  					return nil, errors.Wrapf(err, "enter license from CLI")
   280  				}
   281  
   282  				selector.LicenseID = input
   283  
   284  				err = r.updateUpstream(*selector)
   285  				if err != nil {
   286  					return nil, errors.Wrapf(err, "persist updated upstream")
   287  				}
   288  
   289  				release, err = client.GetSlugRelease(selector)
   290  			}
   291  
   292  			if err != nil {
   293  				return nil, err
   294  			}
   295  		}
   296  	}
   297  
   298  	if err := r.persistSpec([]byte(release.Spec)); err != nil {
   299  		return nil, errors.Wrapf(err, "serialize last-used YAML to disk")
   300  	}
   301  	debug.Log("phase", "write-yaml", "from", release.Spec, "write-location", constants.ReleasePath)
   302  
   303  	return release, err
   304  }
   305  
   306  // persistSpec persists last-used YAML to disk at .ship/release.yml
   307  func (r *resolver) persistSpec(specYAML []byte) error {
   308  	if err := r.FS.MkdirAll(filepath.Dir(constants.ReleasePath), 0700); err != nil {
   309  		return errors.Wrap(err, "mkdir yaml")
   310  	}
   311  
   312  	if err := r.FS.WriteFile(constants.ReleasePath, specYAML, 0644); err != nil {
   313  		return errors.Wrap(err, "write yaml file")
   314  	}
   315  	return nil
   316  }
   317  
   318  func (r *resolver) RegisterInstall(ctx context.Context, selector Selector, release *api.Release) error {
   319  	if r.Runbook != "" {
   320  		return nil
   321  	}
   322  
   323  	debug := level.Debug(log.With(r.Logger, "method", "RegisterRelease"))
   324  
   325  	debug.Log("phase", "register", "with", "gql", "addr", r.Client.GQLServer.String())
   326  
   327  	err := r.Client.RegisterInstall(selector.GetBasicAuthUsername(), "", release.Metadata.ChannelID, release.Metadata.ReleaseID)
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	debug.Log("phase", "register", "status", "complete")
   333  
   334  	return nil
   335  }
   336  
   337  func (r *resolver) SetRunbook(runbook string) {
   338  	r.Runbook = runbook
   339  }
   340  
   341  func (r *resolver) loadFakeEntitlements() (*api.Entitlements, error) {
   342  	var entitlements api.Entitlements
   343  	if r.SetEntitlementsJSON == "" {
   344  		return &entitlements, nil
   345  	}
   346  	err := json.Unmarshal([]byte(r.SetEntitlementsJSON), &entitlements)
   347  	if err != nil {
   348  		return nil, errors.Wrap(err, "load entitlements json")
   349  	}
   350  	return &entitlements, nil
   351  }
   352  
   353  // read the upstream, get the host/path, and replace the query params with the ones from the provided selector
   354  func (r *resolver) updateUpstream(selector Selector) error {
   355  	currentState, err := r.StateManager.CachedState()
   356  	if err != nil {
   357  		return errors.Wrap(err, "retrieve state")
   358  	}
   359  	currentUpstream := currentState.Upstream()
   360  
   361  	parsedUpstream, err := url.Parse(currentUpstream)
   362  	if err != nil {
   363  		return errors.Wrap(err, "parse upstream")
   364  	}
   365  
   366  	if !strings.HasSuffix(parsedUpstream.Path, "/") {
   367  		parsedUpstream.Path += "/"
   368  	}
   369  
   370  	return r.StateManager.SerializeUpstream(parsedUpstream.Path + "?" + selector.String())
   371  }