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 }