github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/project/project.go (about)

     1  package project
     2  
     3  import (
     4  	"errors"
     5  	"log"
     6  	"path/filepath"
     7  	"regexp"
     8  	"runtime"
     9  	"strings"
    10  
    11  	"github.com/ActiveState/cli/internal/constraints"
    12  	"github.com/ActiveState/cli/internal/errs"
    13  	"github.com/ActiveState/cli/internal/keypairs"
    14  	"github.com/ActiveState/cli/internal/language"
    15  	"github.com/ActiveState/cli/internal/locale"
    16  	"github.com/ActiveState/cli/internal/logging"
    17  	"github.com/ActiveState/cli/internal/multilog"
    18  	"github.com/ActiveState/cli/internal/osutils"
    19  	"github.com/ActiveState/cli/internal/output"
    20  	secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets"
    21  	"github.com/ActiveState/cli/pkg/platform/authentication"
    22  	"github.com/ActiveState/cli/pkg/projectfile"
    23  )
    24  
    25  // Build covers the build structure
    26  type Build map[string]string
    27  
    28  var pConditional *constraints.Conditional
    29  var normalizeRx *regexp.Regexp
    30  
    31  func init() {
    32  	var err error
    33  	normalizeRx, err = regexp.Compile("[^a-zA-Z0-9]+")
    34  	if err != nil {
    35  		log.Panicf("normalizeRx: invalid regex: %v", err)
    36  	}
    37  }
    38  
    39  // RegisterConditional is a a temporary method for registering our conditional as a global
    40  // yes this is bad, but at the time of implementation refactoring the project package to not be global is out of scope
    41  func RegisterConditional(conditional *constraints.Conditional) {
    42  	pConditional = conditional
    43  }
    44  
    45  // Project covers the platform structure
    46  type Project struct {
    47  	projectfile *projectfile.Project
    48  	output.Outputer
    49  }
    50  
    51  // Source returns the source projectfile
    52  func (p *Project) Source() *projectfile.Project { return p.projectfile }
    53  
    54  // Constants returns a reference to projectfile.Constants
    55  func (p *Project) Constants() []*Constant {
    56  	constrained, err := constraints.FilterUnconstrained(pConditional, p.projectfile.Constants.AsConstrainedEntities())
    57  	if err != nil {
    58  		logging.Warning("Could not filter unconstrained constants: %v", err)
    59  	}
    60  	cs := projectfile.MakeConstantsFromConstrainedEntities(constrained)
    61  	constants := []*Constant{}
    62  	for _, c := range cs {
    63  		constants = append(constants, &Constant{c, p})
    64  	}
    65  	return constants
    66  }
    67  
    68  // ConstantByName returns a constant matching the given name (if any)
    69  func (p *Project) ConstantByName(name string) *Constant {
    70  	for _, constant := range p.Constants() {
    71  		if constant.Name() == name {
    72  			return constant
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  // Secrets returns a reference to projectfile.Secrets
    79  func (p *Project) Secrets(cfg keypairs.Configurable, auth *authentication.Auth) []*Secret {
    80  	secrets := []*Secret{}
    81  	if p.projectfile.Secrets == nil {
    82  		return secrets
    83  	}
    84  	if p.projectfile.Secrets.User != nil {
    85  		constrained, err := constraints.FilterUnconstrained(pConditional, p.projectfile.Secrets.User.AsConstrainedEntities())
    86  		if err != nil {
    87  			logging.Warning("Could not filter unconstrained user secrets: %v", err)
    88  		}
    89  		secs := projectfile.MakeSecretsFromConstrainedEntities(constrained)
    90  		for _, s := range secs {
    91  			secrets = append(secrets, p.NewSecret(s, SecretScopeUser, cfg, auth))
    92  		}
    93  	}
    94  	if p.projectfile.Secrets.Project != nil {
    95  		constrained, err := constraints.FilterUnconstrained(pConditional, p.projectfile.Secrets.Project.AsConstrainedEntities())
    96  		if err != nil {
    97  			logging.Warning("Could not filter unconstrained project secrets: %v", err)
    98  		}
    99  		secs := projectfile.MakeSecretsFromConstrainedEntities(constrained)
   100  		for _, secret := range secs {
   101  			secrets = append(secrets, p.NewSecret(secret, SecretScopeProject, cfg, auth))
   102  		}
   103  	}
   104  	return secrets
   105  }
   106  
   107  // SecretByName returns a secret matching the given name (if any)
   108  func (p *Project) SecretByName(name string, scope SecretScope, cfg keypairs.Configurable, auth *authentication.Auth) *Secret {
   109  	for _, secret := range p.Secrets(cfg, auth) {
   110  		if secret.Name() == name && secret.scope == scope {
   111  			return secret
   112  		}
   113  	}
   114  	return nil
   115  }
   116  
   117  // Events returns a reference to projectfile.Events
   118  func (p *Project) Events() []*Event {
   119  	constrained, err := constraints.FilterUnconstrained(pConditional, p.projectfile.Events.AsConstrainedEntities())
   120  	if err != nil {
   121  		logging.Warning("Could not filter unconstrained events: %v", err)
   122  	}
   123  
   124  	es := projectfile.MakeEventsFromConstrainedEntities(constrained)
   125  	events := make([]*Event, 0, len(es))
   126  	for _, e := range es {
   127  		events = append(events, &Event{e, p, false})
   128  	}
   129  	return events
   130  }
   131  
   132  // EventByName returns a reference to a projectfile.Script with a given name.
   133  func (p *Project) EventByName(name string, bashifyPaths bool) *Event {
   134  	for _, event := range p.Events() {
   135  		if strings.EqualFold(event.Name(), name) {
   136  			event.BashifyPaths = bashifyPaths
   137  			return event
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  // Scripts returns a reference to projectfile.Scripts
   144  func (p *Project) Scripts() []*Script {
   145  	constrained, err := constraints.FilterUnconstrained(pConditional, p.projectfile.Scripts.AsConstrainedEntities())
   146  	if err != nil {
   147  		logging.Warning("Could not filter unconstrained scripts: %v", err)
   148  	}
   149  	scs := projectfile.MakeScriptsFromConstrainedEntities(constrained)
   150  	scripts := make([]*Script, 0, len(scs))
   151  	for _, s := range scs {
   152  		scripts = append(scripts, &Script{s, p})
   153  	}
   154  	return scripts
   155  }
   156  
   157  // ScriptByName returns a reference to a projectfile.Script with a given name.
   158  func (p *Project) ScriptByName(name string) *Script {
   159  	for _, script := range p.Scripts() {
   160  		if script.Name() == name {
   161  			return script
   162  		}
   163  	}
   164  	return nil
   165  }
   166  
   167  // Jobs returns a reference to projectfile.Jobs
   168  func (p *Project) Jobs() []*Job {
   169  	jobs := []*Job{}
   170  	for _, j := range p.projectfile.Jobs {
   171  		jobs = append(jobs, &Job{&j, p})
   172  	}
   173  	return jobs
   174  }
   175  
   176  // URL returns the Project field of the project file
   177  func (p *Project) URL() string {
   178  	return p.projectfile.Project
   179  }
   180  
   181  // Owner returns project owner
   182  func (p *Project) Owner() string {
   183  	return p.projectfile.Owner()
   184  }
   185  
   186  // Name returns project name
   187  func (p *Project) Name() string {
   188  	return p.projectfile.Name()
   189  }
   190  
   191  func (p *Project) Private() bool {
   192  	return p.Source().Private
   193  }
   194  
   195  // BranchName returns the project branch name
   196  func (p *Project) BranchName() string {
   197  	return p.projectfile.BranchName()
   198  }
   199  
   200  // Path returns the project path
   201  func (p *Project) Path() string {
   202  	return p.projectfile.Path()
   203  }
   204  
   205  // Dir returns the project dir
   206  func (p *Project) Dir() string {
   207  	return filepath.Dir(p.projectfile.Path())
   208  }
   209  
   210  // ProjectDir is an alias for Dir() to satisfy interfaces that may also target the setup.Targeter interface.
   211  func (p *Project) ProjectDir() string {
   212  	return p.Dir()
   213  }
   214  
   215  // LegacyCommitID is for use by legacy mechanics ONLY
   216  func (p *Project) LegacyCommitID() string {
   217  	return p.projectfile.LegacyCommitID()
   218  }
   219  
   220  func (p *Project) SetLegacyCommit(commitID string) error {
   221  	return p.projectfile.SetLegacyCommit(commitID)
   222  }
   223  
   224  func (p *Project) IsHeadless() bool {
   225  	match := projectfile.CommitURLRe.FindStringSubmatch(p.URL())
   226  	return len(match) > 1
   227  }
   228  
   229  // NormalizedName returns the project name in a normalized format (alphanumeric, lowercase)
   230  func (p *Project) NormalizedName() string {
   231  	return strings.ToLower(normalizeRx.ReplaceAllString(p.Name(), ""))
   232  }
   233  
   234  // Version returns the locked state tool version
   235  func (p *Project) Version() string { return p.projectfile.Version() }
   236  
   237  // Channel returns channel that we're pinned to (useless unless version is also set)
   238  func (p *Project) Channel() string { return p.projectfile.Channel() }
   239  
   240  // IsLocked returns whether the current project is locked
   241  func (p *Project) IsLocked() bool { return p.Lock() != "" }
   242  
   243  // Lock returns the lock information for this project
   244  func (p *Project) Lock() string { return p.projectfile.Lock }
   245  
   246  // Cache returns the cache information for this project
   247  func (p *Project) Cache() string { return p.projectfile.Cache }
   248  
   249  // Namespace returns project namespace
   250  func (p *Project) Namespace() *Namespaced {
   251  	return &Namespaced{Owner: p.projectfile.Owner(), Project: p.projectfile.Name()}
   252  }
   253  
   254  // NamespaceString is a convenience function to make interfaces simpler
   255  func (p *Project) NamespaceString() string {
   256  	return p.Namespace().String()
   257  }
   258  
   259  // Environments returns project environment
   260  func (p *Project) Environments() string { return p.projectfile.Environments }
   261  
   262  // New creates a new Project struct
   263  func New(p *projectfile.Project, out output.Outputer) (*Project, error) {
   264  	project := &Project{projectfile: p, Outputer: out}
   265  	return project, nil
   266  }
   267  
   268  // NewLegacy is for legacy use-cases only, DO NOT USE
   269  func NewLegacy(p *projectfile.Project) (*Project, error) {
   270  	return New(p, output.Get())
   271  }
   272  
   273  // Parse will parse the given projectfile and instantiate a Project struct with it
   274  func Parse(fpath string) (*Project, error) {
   275  	pjfile, err := projectfile.Parse(fpath)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	return New(pjfile, output.Get())
   280  }
   281  
   282  func Get() (*Project, error) {
   283  	pjFile, err := projectfile.Get()
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  	project, err := New(pjFile, output.Get())
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	return project, nil
   293  }
   294  
   295  // GetOnce returns project struct the same as Get and GetSafe, but it avoids persisting the project
   296  func GetOnce() (*Project, error) {
   297  	wd, err := osutils.Getwd()
   298  	if err != nil {
   299  		return nil, errs.Wrap(err, "Getwd failure")
   300  	}
   301  	return FromPath(wd)
   302  }
   303  
   304  // FromPath will return the project that's located at the given path (this will walk up the directory tree until it finds the project)
   305  func FromPath(path string) (*Project, error) {
   306  	pjFile, err := projectfile.FromPath(path)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	project, err := New(pjFile, output.Get())
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  
   315  	return project, nil
   316  }
   317  
   318  // FromEnv will return the project as per the environment configuration (eg. env var, working dir, global default, ..)
   319  func FromEnv() (*Project, error) {
   320  	path, err := projectfile.GetProjectFilePath()
   321  	if err != nil {
   322  		return nil, errs.Wrap(err, "Could not get project file path")
   323  	}
   324  
   325  	return FromPath(path)
   326  }
   327  
   328  // FromExactPath will return the project that's located at the given path without walking up the directory tree
   329  func FromExactPath(path string) (*Project, error) {
   330  	pjFile, err := projectfile.FromExactPath(path)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	project, err := New(pjFile, output.Get())
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	return project, nil
   340  }
   341  
   342  // Constant covers the constant structure
   343  type Constant struct {
   344  	constant *projectfile.Constant
   345  	project  *Project
   346  }
   347  
   348  // Name returns constant name
   349  func (c *Constant) Name() string { return c.constant.Name }
   350  
   351  // Value returns constant value
   352  func (c *Constant) Value() (string, error) {
   353  	return ExpandFromProject(c.constant.Value, c.project)
   354  }
   355  
   356  // SecretScope defines the scope of a secret
   357  type SecretScope string
   358  
   359  func (s *SecretScope) toString() string {
   360  	return string(*s)
   361  }
   362  
   363  const (
   364  	// SecretScopeUser defines a secret as being a user secret
   365  	SecretScopeUser SecretScope = "user"
   366  	// SecretScopeProject defines a secret as being a Project secret
   367  	SecretScopeProject SecretScope = "project"
   368  )
   369  
   370  // NewSecretScope creates a new SecretScope from the given string name and will fail if the given string name does not
   371  // match one of the available scopes
   372  func NewSecretScope(name string) (SecretScope, error) {
   373  	var scope SecretScope
   374  	switch name {
   375  	case string(SecretScopeUser):
   376  		return SecretScopeUser, nil
   377  	case string(SecretScopeProject):
   378  		return SecretScopeProject, nil
   379  	default:
   380  		return scope, locale.NewInputError("secrets_err_invalid_namespace")
   381  	}
   382  }
   383  
   384  // Secret covers the secret structure
   385  type Secret struct {
   386  	secret  *projectfile.Secret
   387  	project *Project
   388  	scope   SecretScope
   389  	cfg     keypairs.Configurable
   390  	auth    *authentication.Auth
   391  }
   392  
   393  // InitSecret creates a new secret with the given name and all default settings
   394  func (p *Project) InitSecret(name string, scope SecretScope, cfg keypairs.Configurable, auth *authentication.Auth) *Secret {
   395  	return p.NewSecret(&projectfile.Secret{
   396  		Name: name,
   397  	}, scope, cfg, auth)
   398  }
   399  
   400  // NewSecret creates a new secret struct
   401  func (p *Project) NewSecret(s *projectfile.Secret, scope SecretScope, cfg keypairs.Configurable, auth *authentication.Auth) *Secret {
   402  	return &Secret{s, p, scope, cfg, auth}
   403  }
   404  
   405  // Source returns the source projectfile
   406  func (s *Secret) Source() *projectfile.Project { return s.project.projectfile }
   407  
   408  // Name returns secret name
   409  func (s *Secret) Name() string { return s.secret.Name }
   410  
   411  // Description returns secret description
   412  func (s *Secret) Description() string { return s.secret.Description }
   413  
   414  // IsUser returns whether this secret is user scoped
   415  func (s *Secret) IsUser() bool { return s.scope == SecretScopeUser }
   416  
   417  // Scope returns the scope as a string
   418  func (s *Secret) Scope() string { return s.scope.toString() }
   419  
   420  // IsProject returns whether this secret is project scoped
   421  func (s *Secret) IsProject() bool { return s.scope == SecretScopeProject }
   422  
   423  // ValueOrNil acts as Value() except it can return a nil
   424  func (s *Secret) ValueOrNil() (*string, error) {
   425  	secretsExpander := NewSecretExpander(secretsapi.GetClient(s.auth), nil, nil, s.cfg, s.auth)
   426  
   427  	category := ProjectCategory
   428  	if s.IsUser() {
   429  		category = UserCategory
   430  	}
   431  
   432  	value, err := secretsExpander.Expand("", category, s.secret.Name, false, NewExpansion(s.project))
   433  	if err != nil {
   434  		if errors.Is(err, ErrSecretNotFound) {
   435  			return nil, nil
   436  		}
   437  		multilog.Error("Could not expand secret %s, error: %v", s.Name(), err)
   438  		return nil, errs.Wrap(err, "secret for %s expansion failed", s.secret.Name)
   439  	}
   440  	return &value, nil
   441  }
   442  
   443  // Value returned with all secrets evaluated
   444  func (s *Secret) Value() (string, error) {
   445  	value, err := s.ValueOrNil()
   446  	if err != nil || value == nil {
   447  		return "", err
   448  	}
   449  	return *value, nil
   450  }
   451  
   452  // Event covers the hook structure
   453  type Event struct {
   454  	event        *projectfile.Event
   455  	project      *Project
   456  	BashifyPaths bool // for script path() calls, which varies by subshell
   457  }
   458  
   459  // Source returns the source projectfile
   460  func (e *Event) Source() *projectfile.Project { return e.project.projectfile }
   461  
   462  // Name returns Event name
   463  func (e *Event) Name() string { return e.event.Name }
   464  
   465  // Value returned with all secrets evaluated
   466  func (e *Event) Value() (string, error) {
   467  	if e.BashifyPaths {
   468  		return ExpandFromProjectBashifyPaths(e.event.Value, e.project)
   469  	}
   470  	return ExpandFromProject(e.event.Value, e.project)
   471  }
   472  
   473  // Scope returns the scope property of the event
   474  func (e *Event) Scope() ([]string, error) {
   475  	result := []string{}
   476  	for _, s := range e.event.Scope {
   477  		var v string
   478  		var err error
   479  		if e.BashifyPaths {
   480  			v, err = ExpandFromProjectBashifyPaths(s, e.project)
   481  		} else {
   482  			v, err = ExpandFromProject(s, e.project)
   483  		}
   484  		if err != nil {
   485  			return result, err
   486  		}
   487  		result = append(result, v)
   488  	}
   489  	return result, nil
   490  }
   491  
   492  // Script covers the command structure
   493  type Script struct {
   494  	script  *projectfile.Script
   495  	project *Project
   496  }
   497  
   498  // Source returns the source projectfile
   499  func (script *Script) Source() *projectfile.Project { return script.project.projectfile }
   500  
   501  // SourceScript returns the source script
   502  func (script *Script) SourceScript() *projectfile.Script { return script.script }
   503  
   504  // Name returns script name
   505  func (script *Script) Name() string { return script.script.Name }
   506  
   507  // Languages returns the languages of this script
   508  func (script *Script) Languages() []language.Language {
   509  	stringLanguages := strings.Split(script.script.Language, ",")
   510  	languages := make([]language.Language, 0)
   511  	for _, lang := range stringLanguages {
   512  		if lang != "" {
   513  			languages = append(languages, language.MakeByName(strings.TrimSpace(lang)))
   514  		}
   515  	}
   516  	return languages
   517  }
   518  
   519  // LanguageSafe returns the first languages of this script. The
   520  // returned languages are guaranteed to be of a known scripting language
   521  func (script *Script) LanguageSafe() []language.Language {
   522  	var langs []language.Language
   523  	for _, lang := range script.Languages() {
   524  		if !lang.Recognized() {
   525  			continue
   526  		}
   527  		langs = append(langs, lang)
   528  	}
   529  
   530  	if len(langs) == 0 {
   531  		return DefaultScriptLanguage()
   532  	}
   533  
   534  	return langs
   535  }
   536  
   537  // DefaultScriptLanguage returns the default script language for
   538  // the current platform. (ie. batch or bash)
   539  func DefaultScriptLanguage() []language.Language {
   540  	if runtime.GOOS == "windows" {
   541  		return []language.Language{language.Batch}
   542  	}
   543  	return []language.Language{language.Sh}
   544  }
   545  
   546  // Description returns script description
   547  func (script *Script) Description() string { return script.script.Description }
   548  
   549  // Value returned with all secrets evaluated
   550  func (script *Script) Value() (string, error) {
   551  	return ExpandFromScript(script.script.Value, script)
   552  }
   553  
   554  // Raw returns the script value with no secrets or constants expanded
   555  func (script *Script) Raw() string {
   556  	return script.script.Value
   557  }
   558  
   559  // Standalone returns if the script is standalone or not
   560  func (script *Script) Standalone() bool { return script.script.Standalone }
   561  
   562  // cacheFile allows this script to have an associated file
   563  func (script *Script) setCachedFile(filename string) {
   564  	script.script.Filename = filename
   565  }
   566  
   567  // filename returns the name of the file associated with this script
   568  func (script *Script) cachedFile() string {
   569  	return script.script.Filename
   570  }
   571  
   572  // Job covers the command structure
   573  type Job struct {
   574  	job     *projectfile.Job
   575  	project *Project
   576  }
   577  
   578  func (j *Job) Name() string {
   579  	return j.job.Name
   580  }
   581  
   582  func (j *Job) Constants() []*Constant {
   583  	constants := []*Constant{}
   584  	for _, constantName := range j.job.Constants {
   585  		if constant := j.project.ConstantByName(constantName); constant != nil {
   586  			constants = append(constants, constant)
   587  		}
   588  	}
   589  	return constants
   590  }
   591  
   592  func (j *Job) Scripts() []*Script {
   593  	scripts := []*Script{}
   594  	for _, scriptName := range j.job.Scripts {
   595  		if script := j.project.ScriptByName(scriptName); script != nil {
   596  			scripts = append(scripts, script)
   597  		}
   598  	}
   599  	return scripts
   600  }