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

     1  package specs
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"path/filepath"
     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/ship/pkg/api"
    14  	"github.com/replicatedhq/ship/pkg/constants"
    15  	"github.com/replicatedhq/ship/pkg/specs/apptype"
    16  	"github.com/replicatedhq/ship/pkg/specs/replicatedapp"
    17  	"github.com/replicatedhq/ship/pkg/util"
    18  	yaml "gopkg.in/yaml.v3"
    19  )
    20  
    21  func (r *Resolver) ResolveUnforkRelease(ctx context.Context, upstream string, forked string) (*api.Release, error) {
    22  	debug := log.With(level.Debug(r.Logger), "method", "ResolveUnforkReleases")
    23  	r.ui.Info(fmt.Sprintf("Reading %s and %s ...", upstream, forked))
    24  
    25  	// Prepare the upstream
    26  	r.ui.Info("Determining upstream application type ...")
    27  	upstreamApp, err := r.appTypeInspector.DetermineApplicationType(ctx, upstream)
    28  	if err != nil {
    29  		return nil, errors.Wrapf(err, "determine type of %s", upstream)
    30  	}
    31  	debug.Log("event", "applicationType.resolve", "type", upstreamApp.GetType())
    32  	r.ui.Info(fmt.Sprintf("Detected upstream application type %s", upstreamApp.GetType()))
    33  
    34  	debug.Log("event", "versionedUpstream.resolve", "type", upstreamApp.GetType())
    35  	versionedUpstream, err := r.maybeCreateVersionedUpstream(upstream)
    36  	if err != nil {
    37  		return nil, errors.Wrap(err, "resolve versioned upstream")
    38  	}
    39  
    40  	debug.Log("event", "upstream.Serialize", "for", upstreamApp.GetLocalPath(), "upstream", versionedUpstream)
    41  	err = r.StateManager.SerializeUpstream(versionedUpstream)
    42  	if err != nil {
    43  		return nil, errors.Wrapf(err, "write upstream")
    44  	}
    45  
    46  	// Prepare the fork
    47  	r.ui.Info("Determining forked application type ...")
    48  	forkedApp, err := r.appTypeInspector.DetermineApplicationType(ctx, forked)
    49  	if err != nil {
    50  		return nil, errors.Wrapf(err, "determine type of %s", forked)
    51  	}
    52  
    53  	debug.Log("event", "applicationType.resolve", "type", forkedApp.GetType())
    54  	r.ui.Info(fmt.Sprintf("Detected forked application type %s", forkedApp.GetType()))
    55  
    56  	if forkedApp.GetType() == "helm" && upstreamApp.GetType() == "k8s" {
    57  		return nil, errors.New("Unsupported fork and upstream combination")
    58  	}
    59  
    60  	var forkedAsset api.Asset
    61  	switch forkedApp.GetType() {
    62  	case "helm":
    63  		forkedAsset = api.Asset{
    64  			Helm: &api.HelmAsset{
    65  				AssetShared: api.AssetShared{
    66  					Dest: constants.UnforkForkedBasePath,
    67  				},
    68  				Local: &api.LocalHelmOpts{
    69  					ChartRoot: constants.HelmChartForkedPath,
    70  				},
    71  				ValuesFrom: &api.ValuesFrom{
    72  					Path:        constants.HelmChartForkedPath,
    73  					SaveToState: true,
    74  				},
    75  				Upstream: forked,
    76  			},
    77  		}
    78  	case "k8s":
    79  		forkedAsset = api.Asset{
    80  			Local: &api.LocalAsset{
    81  				AssetShared: api.AssetShared{
    82  					Dest: constants.UnforkForkedBasePath,
    83  				},
    84  				Path: constants.HelmChartForkedPath,
    85  			},
    86  		}
    87  	default:
    88  		return nil, errors.Errorf("unknown forked application type %q", forkedApp.GetType())
    89  	}
    90  
    91  	var upstreamAsset api.Asset
    92  	switch upstreamApp.GetType() {
    93  	case "helm":
    94  		upstreamAsset = api.Asset{
    95  			Helm: &api.HelmAsset{
    96  				AssetShared: api.AssetShared{
    97  					Dest: constants.KustomizeBasePath,
    98  				},
    99  				Local: &api.LocalHelmOpts{
   100  					ChartRoot: constants.HelmChartPath,
   101  				},
   102  				ValuesFrom: &api.ValuesFrom{
   103  					Path: constants.HelmChartPath,
   104  				},
   105  				Upstream: upstream,
   106  			},
   107  		}
   108  	case "k8s":
   109  		upstreamAsset = api.Asset{
   110  			Local: &api.LocalAsset{
   111  				AssetShared: api.AssetShared{
   112  					Dest: constants.KustomizeBasePath,
   113  				},
   114  				Path: constants.HelmChartPath,
   115  			},
   116  		}
   117  	default:
   118  		return nil, errors.Errorf("unknown upstream application type %q", upstreamApp.GetType())
   119  	}
   120  
   121  	defaultRelease := r.DefaultHelmUnforkRelease(upstreamAsset, forkedAsset)
   122  
   123  	return r.resolveUnforkRelease(
   124  		ctx,
   125  		upstream,
   126  		forked,
   127  		upstreamApp,
   128  		forkedApp,
   129  		constants.HelmChartPath,
   130  		constants.HelmChartForkedPath,
   131  		&defaultRelease,
   132  	)
   133  }
   134  
   135  // A resolver turns a target string into a release.
   136  //
   137  // A "target string" is something like
   138  //
   139  //   github.com/helm/charts/stable/nginx-ingress
   140  //   replicated.app/cool-ci-tool?customer_id=...&installation_id=...
   141  //   file::/home/bob/apps/ship.yaml
   142  //   file::/home/luke/my-charts/proton-torpedoes
   143  func (r *Resolver) ResolveRelease(ctx context.Context, upstream string) (*api.Release, error) {
   144  	debug := log.With(level.Debug(r.Logger), "method", "ResolveRelease")
   145  	r.ui.Info(fmt.Sprintf("Reading %s ...", upstream))
   146  
   147  	r.ui.Info("Determining application type ...")
   148  	app, err := r.appTypeInspector.DetermineApplicationType(ctx, upstream)
   149  	if err != nil {
   150  		return nil, errors.Wrapf(err, "determine type of %s", upstream)
   151  	}
   152  	debug.Log("event", "applicationType.resolve", "type", app.GetType())
   153  	r.ui.Info(fmt.Sprintf("Detected application type %s", app.GetType()))
   154  
   155  	debug.Log("event", "versionedUpstream.resolve", "type", app.GetType())
   156  	versionedUpstream, err := r.maybeCreateVersionedUpstream(upstream)
   157  	if err != nil {
   158  		return nil, errors.Wrap(err, "resolve versioned upstream")
   159  	}
   160  
   161  	debug.Log("event", "upstream.Serialize", "for", app.GetLocalPath(), "upstream", versionedUpstream)
   162  
   163  	if !r.isEdit {
   164  		err = r.StateManager.SerializeUpstream(versionedUpstream)
   165  		if err != nil {
   166  			return nil, errors.Wrapf(err, "write upstream")
   167  		}
   168  	}
   169  
   170  	if app.GetType() != "replicated.app" {
   171  		debug.Log("event", "persist app state")
   172  		persistPath := app.GetLocalPath()
   173  		if app.GetType() == "runbook.replicated.app" {
   174  			persistPath = filepath.Dir(app.GetLocalPath())
   175  		}
   176  
   177  		err = r.persistToState(persistPath)
   178  		if err != nil {
   179  			return nil, errors.Wrapf(err, "persist %s to state from path %s", app.GetType(), persistPath)
   180  		}
   181  	}
   182  
   183  	switch app.GetType() {
   184  
   185  	case "helm":
   186  		defaultRelease := r.DefaultHelmRelease(app.GetLocalPath(), upstream)
   187  
   188  		return r.resolveRelease(
   189  			ctx,
   190  			upstream,
   191  			app,
   192  			constants.HelmChartPath,
   193  			&defaultRelease,
   194  			true,
   195  			true,
   196  		)
   197  
   198  	case "k8s":
   199  		defaultRelease := r.DefaultRawRelease(constants.KustomizeBasePath)
   200  
   201  		return r.resolveRelease(
   202  			ctx,
   203  			upstream,
   204  			app,
   205  			constants.KustomizeBasePath,
   206  			&defaultRelease,
   207  			false,
   208  			true,
   209  		)
   210  
   211  	case "runbook.replicated.app":
   212  		r.AppResolver.SetRunbook(app.GetLocalPath())
   213  		fallthrough
   214  	case "replicated.app":
   215  		if r.isEdit {
   216  			return r.AppResolver.ResolveEditRelease(ctx)
   217  		}
   218  
   219  		parsed, err := url.Parse(upstream)
   220  		if err != nil {
   221  			return nil, errors.Wrapf(err, "parse url %s", upstream)
   222  		}
   223  		selector := (&replicatedapp.Selector{}).UnmarshalFrom(parsed)
   224  		return r.AppResolver.ResolveAppRelease(ctx, selector, app)
   225  
   226  	case "inline.replicated.app":
   227  		return r.resolveInlineShipYAMLRelease(
   228  			ctx,
   229  			upstream,
   230  			app,
   231  		)
   232  
   233  	}
   234  
   235  	return nil, errors.Errorf("unknown application type %q for upstream %q", app.GetType(), upstream)
   236  }
   237  
   238  func (r *Resolver) resolveUnforkRelease(
   239  	ctx context.Context,
   240  	upstream string,
   241  	forked string,
   242  	upstreamApp apptype.LocalAppCopy,
   243  	forkedApp apptype.LocalAppCopy,
   244  	destUpstreamPath string,
   245  	destForkedPath string,
   246  	defaultSpec *api.Spec,
   247  ) (*api.Release, error) {
   248  	var releaseName string
   249  	debug := log.With(level.Debug(r.Logger), "method", "resolveUnforkReleases")
   250  
   251  	if r.Viper.GetBool("rm-asset-dest") {
   252  		err := r.FS.RemoveAll(destUpstreamPath)
   253  		if err != nil {
   254  			return nil, errors.Wrapf(err, "remove asset dest %s", destUpstreamPath)
   255  		}
   256  
   257  		err = r.FS.RemoveAll(destForkedPath)
   258  		if err != nil {
   259  			return nil, errors.Wrapf(err, "remove asset dest %s", destForkedPath)
   260  		}
   261  	}
   262  
   263  	err := util.BailIfPresent(r.FS, destUpstreamPath, debug)
   264  	if err != nil {
   265  		return nil, errors.Wrapf(err, "backup %s", destUpstreamPath)
   266  	}
   267  
   268  	err = r.FS.MkdirAll(filepath.Dir(destUpstreamPath), 0777)
   269  	if err != nil {
   270  		return nil, errors.Wrapf(err, "mkdir %s", destUpstreamPath)
   271  	}
   272  
   273  	err = r.FS.MkdirAll(filepath.Dir(destForkedPath), 0777)
   274  	if err != nil {
   275  		return nil, errors.Wrapf(err, "mkdir %s", destForkedPath)
   276  	}
   277  
   278  	err = r.FS.Rename(upstreamApp.GetLocalPath(), destUpstreamPath)
   279  	if err != nil {
   280  		return nil, errors.Wrapf(err, "move %s to %s", upstreamApp.GetLocalPath(), destUpstreamPath)
   281  	}
   282  
   283  	err = r.FS.Rename(forkedApp.GetLocalPath(), destForkedPath)
   284  	if err != nil {
   285  		return nil, errors.Wrapf(err, "move %s to %s", forkedApp.GetLocalPath(), destForkedPath)
   286  	}
   287  
   288  	if forkedApp.GetType() == "k8s" {
   289  		// Pre-emptively need to split here in order to get the release name before
   290  		// helm template is run on the upstream
   291  		if err := util.MaybeSplitMultidocYaml(ctx, r.FS, destForkedPath); err != nil {
   292  			return nil, errors.Wrapf(err, "maybe split multidoc in %s", destForkedPath)
   293  		}
   294  
   295  		debug.Log("event", "maybeGetReleaseName")
   296  		releaseName, err = r.maybeGetReleaseName(destForkedPath)
   297  		if err != nil {
   298  			return nil, errors.Wrap(err, "maybe get release name")
   299  		}
   300  	}
   301  
   302  	upstreamMetadata, err := r.resolveMetadata(context.Background(), upstream, destUpstreamPath, upstreamApp.GetType())
   303  	if err != nil {
   304  		return nil, errors.Wrapf(err, "resolve metadata for %s", destUpstreamPath)
   305  	}
   306  
   307  	release := &api.Release{
   308  		Metadata: api.ReleaseMetadata{
   309  			ShipAppMetadata: *upstreamMetadata,
   310  		},
   311  		Spec: *defaultSpec,
   312  	}
   313  
   314  	if releaseName == "" {
   315  		releaseName = release.Metadata.ReleaseName()
   316  	}
   317  
   318  	if err := r.StateManager.SerializeReleaseName(releaseName); err != nil {
   319  		debug.Log("event", "serialize.releaseName.fail", "err", err)
   320  		return nil, errors.Wrapf(err, "serialize helm release name")
   321  	}
   322  
   323  	return release, nil
   324  }
   325  
   326  func (r *Resolver) maybeGetReleaseName(path string) (string, error) {
   327  	type k8sReleaseMetadata struct {
   328  		Metadata struct {
   329  			Labels struct {
   330  				Release string `yaml:"release"`
   331  			} `yaml:"labels"`
   332  		} `yaml:"metadata"`
   333  	}
   334  
   335  	files, err := r.FS.ReadDir(path)
   336  	if err != nil {
   337  		return "", errors.Wrapf(err, "read dir %s", path)
   338  	}
   339  
   340  	for _, file := range files {
   341  		if filepath.Ext(file.Name()) == ".yaml" || filepath.Ext(file.Name()) == ".yml" {
   342  			fileB, err := r.FS.ReadFile(filepath.Join(path, file.Name()))
   343  			if err != nil {
   344  				return "", errors.Wrapf(err, "read file %s", path)
   345  			}
   346  
   347  			releaseMetadata := k8sReleaseMetadata{}
   348  			if err := yaml.Unmarshal(fileB, &releaseMetadata); err != nil {
   349  				return "", errors.Wrapf(err, "unmarshal for release metadata %s", path)
   350  			}
   351  
   352  			if releaseMetadata.Metadata.Labels.Release != "" {
   353  				return releaseMetadata.Metadata.Labels.Release, nil
   354  			}
   355  		}
   356  	}
   357  
   358  	return "", nil
   359  }
   360  
   361  func (r *Resolver) resolveRelease(
   362  	ctx context.Context,
   363  	upstream string,
   364  	app apptype.LocalAppCopy,
   365  	destPath string,
   366  	defaultSpec *api.Spec,
   367  	keepOriginal bool,
   368  	tryUseUpstreamShipYAML bool,
   369  ) (*api.Release, error) {
   370  	debug := log.With(level.Debug(r.Logger), "method", "resolveRelease")
   371  
   372  	if r.Viper.GetBool("rm-asset-dest") {
   373  		err := r.FS.RemoveAll(destPath)
   374  		if err != nil {
   375  			return nil, errors.Wrapf(err, "remove asset dest %s", destPath)
   376  		}
   377  	}
   378  
   379  	err := util.BailIfPresent(r.FS, destPath, debug)
   380  	if err != nil {
   381  		return nil, errors.Wrapf(err, "backup %s", destPath)
   382  	}
   383  
   384  	if !keepOriginal {
   385  		err = r.FS.Rename(app.GetLocalPath(), destPath)
   386  		if err != nil {
   387  			return nil, errors.Wrapf(err, "move %s to %s", app.GetLocalPath(), destPath)
   388  		}
   389  	} else {
   390  		// instead of renaming, copy files from localPath to destPath
   391  		err = r.recursiveCopy(app.GetLocalPath(), destPath)
   392  		if err != nil {
   393  			return nil, errors.Wrapf(err, "copy %s to %s", app.GetLocalPath(), destPath)
   394  		}
   395  	}
   396  
   397  	metadata, err := r.resolveMetadata(context.Background(), upstream, destPath, app.GetType())
   398  	if err != nil {
   399  		return nil, errors.Wrapf(err, "resolve metadata for %s", destPath)
   400  	}
   401  
   402  	var spec *api.Spec
   403  	if tryUseUpstreamShipYAML {
   404  		debug.Log("event", "check upstream for ship.yaml")
   405  		spec, err = r.maybeGetShipYAML(ctx, destPath)
   406  		if err != nil {
   407  			return nil, errors.Wrapf(err, "resolve ship.yaml release for %s", destPath)
   408  		}
   409  	}
   410  
   411  	if spec == nil {
   412  		debug.Log("event", "no ship.yaml for release")
   413  		r.ui.Info("ship.yaml not found in upstream, generating default lifecycle for application ...")
   414  		spec = defaultSpec
   415  	}
   416  
   417  	if metadata == nil {
   418  		metadata = &api.ShipAppMetadata{}
   419  	}
   420  
   421  	release := &api.Release{
   422  		Metadata: api.ReleaseMetadata{
   423  			ShipAppMetadata: *metadata,
   424  			Type:            app.GetType(),
   425  		},
   426  		Spec: *spec,
   427  	}
   428  
   429  	currentState, err := r.StateManager.CachedState()
   430  	if err != nil {
   431  		return nil, errors.Wrap(err, "try load")
   432  	}
   433  
   434  	releaseName := currentState.CurrentReleaseName()
   435  	if releaseName == "" {
   436  		debug.Log("event", "resolve.releaseName.fromRelease")
   437  		releaseName = release.Metadata.ReleaseName()
   438  	}
   439  
   440  	if err := r.StateManager.SerializeReleaseName(releaseName); err != nil {
   441  		debug.Log("event", "serialize.releaseName.fail", "err", err)
   442  		return nil, errors.Wrapf(err, "serialize helm release name")
   443  	}
   444  
   445  	return release, nil
   446  }
   447  
   448  func (r *Resolver) recursiveCopy(sourceDir, destDir string) error {
   449  	err := r.FS.MkdirAll(destDir, os.FileMode(0777))
   450  	if err != nil {
   451  		return errors.Wrapf(err, "create dest dir %s", destDir)
   452  	}
   453  	srcFiles, err := r.FS.ReadDir(sourceDir)
   454  	if err != nil {
   455  		return errors.Wrapf(err, "")
   456  	}
   457  	for _, file := range srcFiles {
   458  		if file.IsDir() {
   459  			err = r.recursiveCopy(filepath.Join(sourceDir, file.Name()), filepath.Join(destDir, file.Name()))
   460  			if err != nil {
   461  				return errors.Wrapf(err, "copy dir %s", file.Name())
   462  			}
   463  		} else {
   464  			// is file
   465  			contents, err := r.FS.ReadFile(filepath.Join(sourceDir, file.Name()))
   466  			if err != nil {
   467  				return errors.Wrapf(err, "read file %s to copy", file.Name())
   468  			}
   469  
   470  			err = r.FS.WriteFile(filepath.Join(destDir, file.Name()), contents, file.Mode())
   471  			if err != nil {
   472  				return errors.Wrapf(err, "write file %s to copy", file.Name())
   473  			}
   474  		}
   475  	}
   476  	return nil
   477  }
   478  
   479  func (r *Resolver) resolveInlineShipYAMLRelease(
   480  	ctx context.Context,
   481  	upstream string,
   482  	app apptype.LocalAppCopy,
   483  ) (*api.Release, error) {
   484  	debug := log.With(level.Debug(r.Logger), "method", "resolveInlineShipYAMLRelease")
   485  	metadata, err := r.resolveMetadata(context.Background(), upstream, app.GetLocalPath(), app.GetType())
   486  	if err != nil {
   487  		return nil, errors.Wrapf(err, "resolve metadata for %s", app.GetLocalPath())
   488  	}
   489  	debug.Log("event", "check upstream for ship.yaml")
   490  	spec, err := r.maybeGetShipYAML(ctx, app.GetLocalPath())
   491  	if err != nil || spec == nil {
   492  		return nil, errors.Wrapf(err, "resolve ship.yaml release for %s", app.GetLocalPath())
   493  	}
   494  	release := &api.Release{
   495  		Metadata: api.ReleaseMetadata{
   496  			ShipAppMetadata: *metadata,
   497  			Type:            app.GetType(),
   498  		},
   499  		Spec: *spec,
   500  	}
   501  	releaseName := release.Metadata.ReleaseName()
   502  	debug.Log("event", "resolve.releaseName")
   503  	if err := r.StateManager.SerializeReleaseName(releaseName); err != nil {
   504  		debug.Log("event", "serialize.releaseName.fail", "err", err)
   505  		return nil, errors.Wrapf(err, "serialize helm release name")
   506  	}
   507  	return release, nil
   508  }